Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config Management via UAP #1919

Merged
merged 31 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
51aecb7
write received custom config to config.yaml file
XuechunHou Mar 28, 2025
73826fd
when input config is empty, dont overwrite the config.yaml file
XuechunHou Mar 28, 2025
a5b3962
final impl
XuechunHou Mar 28, 2025
0283197
added an integration test
XuechunHou Mar 28, 2025
ec41a64
do not reuse the same file name
XuechunHou Mar 29, 2025
b5a092c
fixed tests
XuechunHou Mar 29, 2025
4e15443
fixed typo
XuechunHou Mar 29, 2025
019414b
test string config received from UAp
XuechunHou Mar 31, 2025
7715493
test invalid struct config
XuechunHou Mar 31, 2025
672ddb4
added additional tests
XuechunHou Mar 31, 2025
9eb60d6
fixed string formatting
XuechunHou Mar 31, 2025
ad6a184
fixed typo
XuechunHou Mar 31, 2025
5aee724
stringify yaml
XuechunHou Mar 31, 2025
0148e06
removed unecessary test
XuechunHou Mar 31, 2025
ee0b203
fixed yaml strigify
XuechunHou Mar 31, 2025
0cbb3b7
stringify struct proto
XuechunHou Mar 31, 2025
b960062
fixed typo
XuechunHou Mar 31, 2025
8f6d743
wrap string with double quote
XuechunHou Mar 31, 2025
2db100c
fixed typo
XuechunHou Mar 31, 2025
6aa3454
added additional unit tests
XuechunHou Mar 31, 2025
59f6b81
removed unnecessary struct fields
XuechunHou Mar 31, 2025
eb76e7f
fixed typo
XuechunHou Mar 31, 2025
adade48
removed comments
XuechunHou Mar 31, 2025
6511e2e
escape double quote
XuechunHou Mar 31, 2025
7ceff27
removed escaping
XuechunHou Mar 31, 2025
85d125e
removed tests
XuechunHou Mar 31, 2025
23f0d74
updated test
XuechunHou Mar 31, 2025
f530e23
simplify invalid config input
XuechunHou Apr 1, 2025
4701faf
skip windows
XuechunHou Apr 1, 2025
d399170
updated unit tests
XuechunHou Apr 1, 2025
dff9d50
updated docker template
XuechunHou Apr 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ RUN ./pkg/rpm/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache centos8


Expand Down Expand Up @@ -290,7 +289,6 @@ RUN ./pkg/rpm/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache rockylinux9


Expand Down Expand Up @@ -399,7 +397,6 @@ RUN ./pkg/deb/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache bookworm


Expand Down Expand Up @@ -508,7 +505,6 @@ RUN ./pkg/deb/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache bullseye


Expand Down Expand Up @@ -636,7 +632,6 @@ RUN ./pkg/rpm/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache sles12


Expand Down Expand Up @@ -750,7 +745,6 @@ RUN ./pkg/rpm/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache sles15


Expand Down Expand Up @@ -859,7 +853,6 @@ RUN ./pkg/deb/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache focal


Expand Down Expand Up @@ -968,7 +961,6 @@ RUN ./pkg/deb/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache jammy


Expand Down Expand Up @@ -1077,7 +1069,6 @@ RUN ./pkg/deb/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache noble


Expand Down Expand Up @@ -1186,7 +1177,6 @@ RUN ./pkg/deb/build.sh
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache oracular


Expand Down
1 change: 1 addition & 0 deletions builds/ops_agent_plugin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

set -x -e
DESTDIR=$1
source VERSION && echo $PKG_VERSION > "$DESTDIR/OPS_AGENT_VERSION"
mkdir -p "$DESTDIR/opt/google-cloud-ops-agent"
go build -buildvcs=false -ldflags "-s -w" -o "$DESTDIR/opt/google-cloud-ops-agent/plugin" \
github.com/GoogleCloudPlatform/ops-agent/cmd/ops_agent_uap_plugin
37 changes: 37 additions & 0 deletions cmd/ops_agent_uap_plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (
"net"
"os"
"os/exec"
"path/filepath"
"sync"

"buf.build/go/protoyaml"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"

Expand Down Expand Up @@ -96,3 +98,38 @@ func main() {
os.Exit(1)
}
}

func writeCustomConfigToFile(req *pb.StartRequest, configPath string) error {
customConfig := []byte{}
switch req.GetServiceConfig().(type) {
case *pb.StartRequest_StringConfig:
customConfig = []byte(req.GetStringConfig())
case *pb.StartRequest_StructConfig:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user inputs a custom yaml and it's able to parse into a StructConfig, would the StringConfig also be sent, or only the parsed version?

If both are sent, wouldn't this case never trigger?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, do we even want to use the parsed config?

Since we simply write the config to a file and let the Ops Agent handle it, do we need to check for proper yaml at this stage?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://source.corp.google.com/piper///depot/google3/third_party/guest_agent/dev/internal/plugin/proto/plugin_comm.proto;l=53 service_config is a oneof field, so either string_config or struct_config is sent per StartRequest, never both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, do we even want to use the parsed config?

Since we simply write the config to a file and let the Ops Agent handle it, do we need to check for proper yaml at this stage?

"Also, do we even want to use the parsed config?" If the FE allows both, we will have to handle both.

struct_config is populated when users upload a config.yaml file at the FE, it will be parsed, validated and converted to a struct proto at the FE before us receiving it.

string_config is populated when users paste a chunk of string at the FE, the string will be forwarded to us as is without any validation.

I am not doing any validation at this stage really, I am just converting the struct proto back to a yaml and writes it to config.yaml file. Whether it's a valid Ops Agent config yaml is left to the conf generator...

structConfig := req.GetStructConfig()
yamlBytes, err := protoyaml.Marshal(structConfig)
if err != nil {
return fmt.Errorf("failed to parse the custom Ops Agent config: %v", err)
}
customConfig = yamlBytes
}

if len(customConfig) > 0 {
parentDir := filepath.Dir(configPath)
if _, err := os.Stat(parentDir); os.IsNotExist(err) {
err := os.MkdirAll(parentDir, 0755)
if err != nil {
return fmt.Errorf("failed to create parent directory %s: %v", parentDir, err)
}
}

file, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open the config.yaml file at location: %s, error: %v", configPath, err)
}
defer file.Close()
if _, err := file.Write(customConfig); err != nil {
return fmt.Errorf("failed to write to the config.yaml file at location: %s, error: %v", configPath, err)
}
}
return nil
}
152 changes: 152 additions & 0 deletions cmd/ops_agent_uap_plugin/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main

import (
"context"
"fmt"
"os"
"path/filepath"
"testing"

"buf.build/go/protoyaml" // Import the protoyaml-go package
"github.com/GoogleCloudPlatform/ops-agent/apps"
"github.com/GoogleCloudPlatform/ops-agent/confgenerator"
"github.com/GoogleCloudPlatform/ops-agent/internal/platform"

pb "github.com/GoogleCloudPlatform/ops-agent/cmd/ops_agent_uap_plugin/google_guest_agent/plugin"
spb "google.golang.org/protobuf/types/known/structpb"
)

func customLogPathByOsType(ctx context.Context) string {
osType := platform.FromContext(ctx).Name()
if osType == "linux" {
return "/var/log"
}
return `C:\mylog`
}
func TestWriteCustomConfigToFile(t *testing.T) {
yamlConfig := fmt.Sprintf(`logging:
receivers:
mylog_source:
type: files
include_paths:
- %s
exporters:
google:
type: google_cloud_logging
processors:
my_exclude:
type: exclude_logs
match_any:
- jsonPayload.missing_field = "value"
- jsonPayload.message =~ "test pattern"
service:
pipelines:
my_pipeline:
receivers: [mylog_source]
processors: [my_exclude]
exporters: [google]`, customLogPathByOsType(context.Background()))
structConfig := &spb.Struct{}
err := protoyaml.Unmarshal([]byte(yamlConfig), structConfig)
if err != nil {
t.Fatalf("Failed to unmarshal YAML into structpb.Struct: %v", err)
}

tests := []struct {
name string
req *pb.StartRequest
}{
{
name: "Received a valid StringConfig from UAP, the output should be a valid Ops agent yaml",
req: &pb.StartRequest{
ServiceConfig: &pb.StartRequest_StringConfig{
StringConfig: yamlConfig,
},
},
},
{
name: "Received a valid StructConfig from UAP, the output should be a valid Ops agent yaml",
req: &pb.StartRequest{
ServiceConfig: &pb.StartRequest_StructConfig{
StructConfig: structConfig,
},
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create a temporary directory for the test file
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "ops-agent-config", fmt.Sprintf("%sconfig.yaml", tc.name))

err := writeCustomConfigToFile(tc.req, configPath)

if err != nil {
t.Errorf("%v: writeCustomConfigToFile got error: %v, want nil error", tc.name, err)
}

_, err = confgenerator.MergeConfFiles(context.Background(), configPath, apps.BuiltInConfStructs)
if err != nil {
t.Errorf("%v: conf generator fails to validate the output Ops agent yaml: %v", tc.name, err)
}
})
}
}

func TestWriteCustomConfigToFile_receivedEmptyCustomConfig(t *testing.T) {
tests := []struct {
name string
req *pb.StartRequest
}{
{
name: "The ops agent config.yaml file should not be modified if UAP does not send any StringConfig",
req: &pb.StartRequest{},
},
{
name: "The ops agent config.yaml file should not be modified if UAP does not send any StructConfig",
req: &pb.StartRequest{},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
configFile, err := os.CreateTemp("", "config.yaml")
if err != nil {
t.Fatalf("%v: failed to create the config.yaml file at location: %s, error: %v", tc.name, configFile.Name(), err)
}
configPath := configFile.Name()
wantFileContent := "1234"
if _, err := configFile.WriteString(wantFileContent); err != nil {
t.Fatalf("%v: failed to write to the config.yaml file at location: %s, error: %v", tc.name, configPath, err)
}

err = writeCustomConfigToFile(tc.req, configPath)
if err != nil {
t.Errorf("%v: writeCustomConfigToFile got error: %v, want nil error", tc.name, err)
}

gotContent, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("%s: failed to read the config.yaml file content: %v", tc.name, err)
}
if string(gotContent) != wantFileContent {
t.Errorf("%s: got config.yaml content: %v, want: %v", tc.name, string(gotContent), wantFileContent)
}
configFile.Close()
os.Remove(configPath)
})
}
}
18 changes: 12 additions & 6 deletions cmd/ops_agent_uap_plugin/service_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ func (ps *OpsAgentPluginServer) Start(ctx context.Context, msg *pb.StartRequest)
if err != nil {
ps.Stop(ctx, &pb.StopRequest{Cleanup: false})
log.Printf("Start() failed, because it cannot determine the plugin install location: %s", err)
return nil, status.Error(1, err.Error())
return nil, status.Error(13, err.Error()) // Internal
}
pluginInstallPath, err = filepath.EvalSymlinks(pluginInstallPath)
if err != nil {
ps.Stop(ctx, &pb.StopRequest{Cleanup: false})
log.Printf("Start() failed, because it cannot determine the plugin install location: %s", err)
return nil, status.Error(1, err.Error())
return nil, status.Error(13, err.Error()) // Internal
}
pluginInstallDir := filepath.Dir(pluginInstallPath)

Expand All @@ -108,21 +108,27 @@ func (ps *OpsAgentPluginServer) Start(ctx context.Context, msg *pb.StartRequest)
if foundConflictingInstallations || err != nil {
ps.Stop(ctx, &pb.StopRequest{Cleanup: false})
log.Printf("Start() failed: %s", err)
return nil, status.Error(1, err.Error())
return nil, status.Error(9, err.Error()) // FailedPrecondition
}

// Receive config from the Start request and write it to the Ops Agent config file.
if err := writeCustomConfigToFile(msg, OpsAgentConfigLocationLinux); err != nil {
log.Printf("Start() failed: %s", err)
ps.Stop(ctx, &pb.StopRequest{Cleanup: false})
return nil, status.Errorf(13, "failed to write the custom Ops Agent config to file: %s", err) // Internal
}

// Ops Agent config validation
if err := validateOpsAgentConfig(pContext, pluginInstallDir, pluginStateDir, ps.runCommand); err != nil {
log.Printf("Start() failed: %s", err)
ps.Stop(ctx, &pb.StopRequest{Cleanup: false})
return nil, status.Errorf(1, "failed to validate Ops Agent config: %s", err)
return nil, status.Errorf(9, "failed to validate Ops Agent config: %s", err) // FailedPrecondition
}

// Subagent config generation
if err := generateSubagentConfigs(pContext, ps.runCommand, pluginInstallDir, pluginStateDir); err != nil {
log.Printf("Start() failed: %s", err)
ps.Stop(ctx, &pb.StopRequest{Cleanup: false})
return nil, status.Errorf(1, "failed to generate subagent configs: %s", err)
return nil, status.Errorf(9, "failed to generate subagent configs: %s", err) // FailedPrecondition
}

// the diagnostics service and subagent startups
Expand Down
8 changes: 8 additions & 0 deletions cmd/ops_agent_uap_plugin/service_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ func (ps *OpsAgentPluginServer) Start(ctx context.Context, msg *pb.StartRequest)
return nil, status.Error(13, err.Error()) // Internal
}

// Receive config from the Start request and write it to the Ops Agent config file.
if err := writeCustomConfigToFile(msg, OpsAgentConfigLocationWindows); err != nil {
ps.Stop(ctx, &pb.StopRequest{Cleanup: false})
windowsEventLogger.Close()
log.Printf("Start() failed, because it failed to write the custom Ops Agent config to file: %s", err)
return nil, status.Errorf(13, "failed to write the custom Ops Agent config to file: %s", err) // Internal
}

// Subagents config validation and generation.
if err := generateSubAgentConfigs(ctx, OpsAgentConfigLocationWindows, pluginStateDir, windowsEventLogger); err != nil {
ps.Stop(ctx, &pb.StopRequest{Cleanup: false})
Expand Down
1 change: 0 additions & 1 deletion dockerfiles/template
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ COPY --from={target_name}-build-wrapper /work/cache /work/cache
COPY cmd/ops_agent_uap_plugin cmd/ops_agent_uap_plugin
COPY ./builds/ops_agent_plugin.sh .
RUN ./ops_agent_plugin.sh /work/cache/
RUN source VERSION && echo $PKG_VERSION > /work/cache/OPS_AGENT_VERSION
RUN ./pkg/plugin/build.sh /work/cache {target_name}


Expand Down
Loading