From 56544d487bdda9e46e16d3f08807ed3a61047d20 Mon Sep 17 00:00:00 2001 From: Zachary Zabetakis Date: Thu, 18 Sep 2025 11:17:23 -0700 Subject: [PATCH] Detect and stop the package managed agent when launching in extension mode. PiperOrigin-RevId: 808660229 --- .github/workflows/go-build-and-test.yml | 2 +- .github/workflows/go-build-protos.yml | 2 +- build.sh | 2 +- go.mod | 4 +- go.sum | 10 +- internal/configuration/configuration.go | 59 ++++++++++- internal/configuration/configuration_test.go | 100 ++++++++++++++++++ .../defaultconfigs/configuration.json | 15 +++ .../startdaemon/test_data/default_config.json | 11 ++ .../test_data/overwritten_config.json | 9 ++ 10 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 internal/configuration/defaultconfigs/configuration.json create mode 100644 internal/startdaemon/test_data/default_config.json create mode 100644 internal/startdaemon/test_data/overwritten_config.json diff --git a/.github/workflows/go-build-and-test.yml b/.github/workflows/go-build-and-test.yml index dae663e8..9d5cf53b 100644 --- a/.github/workflows/go-build-and-test.yml +++ b/.github/workflows/go-build-and-test.yml @@ -23,7 +23,7 @@ jobs: cd workloadagentplatform # this is the hash of the workloadagentplatform submodule # get the hash by running: go list -m -json github.com/GoogleCloudPlatform/workloadagentplatform@main - git checkout d263b695432a9439950984306e9f15c6ac5f0cd7 + git checkout e00b74940f449310713ac0ef882538be101c7044 cd .. find workloadagentplatform/sharedprotos -type f -exec sed -i 's|"sharedprotos|"workloadagentplatform/sharedprotos|g' {} + env: diff --git a/.github/workflows/go-build-protos.yml b/.github/workflows/go-build-protos.yml index faad970c..8112f6e9 100644 --- a/.github/workflows/go-build-protos.yml +++ b/.github/workflows/go-build-protos.yml @@ -31,7 +31,7 @@ jobs: cd workloadagentplatform # this is the hash of the workloadagentplatform submodule # get the hash by running: go list -m -json github.com/GoogleCloudPlatform/workloadagentplatform@main - git checkout d263b695432a9439950984306e9f15c6ac5f0cd7 + git checkout e00b74940f449310713ac0ef882538be101c7044 cd .. find workloadagentplatform/sharedprotos -type f -exec sed -i 's|"sharedprotos|"workloadagentplatform/sharedprotos|g' {} + env: diff --git a/build.sh b/build.sh index 95a02152..9ef0ed87 100755 --- a/build.sh +++ b/build.sh @@ -55,7 +55,7 @@ if [ "${COMPILE_PROTOS}" == "TRUE" ] && [ ! -d "workloadagentplatform" ]; then cd workloadagentplatform # this is the hash of the workloadagentplatform submodule # get the hash by running: go list -m -json github.com/GoogleCloudPlatform/workloadagentplatform@main - git checkout d263b695432a9439950984306e9f15c6ac5f0cd7 + git checkout e00b74940f449310713ac0ef882538be101c7044 cd .. # replace the proto imports in the platform that reference the platform find workloadagentplatform/sharedprotos -type f -exec sed -i 's|"sharedprotos|"workloadagentplatform/sharedprotos|g' {} + diff --git a/go.mod b/go.mod index 60eace97..11f644a2 100644 --- a/go.mod +++ b/go.mod @@ -18,10 +18,10 @@ require ( cloud.google.com/go/storage v1.50.0 // Get the version by running: // go list -m -json github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries@main - github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries v0.0.0-20250708193908-d263b695432a + github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries v0.0.0-20250912122010-e00b74940f44 // Get the version by running: // go list -m -json github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos@main - github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos v0.0.0-20250708193908-d263b695432a + github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos v0.0.0-20250912122010-e00b74940f44 github.com/SAP/go-hdb v1.12.12 github.com/cenkalti/backoff/v4 v4.3.0 github.com/fsouza/fake-gcs-server v1.52.1 diff --git a/go.sum b/go.sum index b7ff8cea..7b8a3446 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.118.0 h1:tvZe1mgqRxpiVa3XlIGMiPcEUbP1gNXELgD4y/IXmeQ= cloud.google.com/go v0.118.0/go.mod h1:zIt2pkedt/mo+DQjcT4/L3NDxzHPR29j5HcclNH+9PM= -cloud.google.com/go/aiplatform v1.70.0 h1:vnqsPkgcwlDEpWl9t6C3/HLfHeweuGXs2gcYTzH6dMs= -cloud.google.com/go/aiplatform v1.70.0/go.mod h1:1cewyC4h+yvRs0qVvlCuU3V6j1pJ41doIcroYX3uv8o= cloud.google.com/go/artifactregistry v1.16.1 h1:ZNXGB6+T7VmWdf6//VqxLdZ/sk0no8W0ujanHeJwDRw= cloud.google.com/go/artifactregistry v1.16.1/go.mod h1:sPvFPZhfMavpiongKwfg93EOwJ18Tnj9DIwTU9xWUgs= cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= @@ -43,10 +41,10 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0/go.mod h1:l2fIqmwB+FKSfvn3bAD/0i+AXAxhIZjTK2svT/mgUXs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 h1:GYUJLfvd++4DMuMhCFLgLXvFwofIxh/qOwoGuS/LTew= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0/go.mod h1:wRbFgBQUVm1YXrvWKofAEmq9HNJTDphbAaJSSX01KUI= -github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries v0.0.0-20250708193908-d263b695432a h1:h4I+QAuiTzcvHkl5OESfP+CV/Y48h0/9YXhC7poPPwU= -github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries v0.0.0-20250708193908-d263b695432a/go.mod h1:kd9KRERnc/KakQgqX8jwmVwdMjVcficQIQ1FJ2j6EAQ= -github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos v0.0.0-20250708193908-d263b695432a h1:FFQ+XGRY3PT3kfw7dKVfKHCUPKIpCKma5cOoIXR9yDM= -github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos v0.0.0-20250708193908-d263b695432a/go.mod h1:8Ea8vdBuPsWhhwzL9sNK7BFQE9qbkPLZUHxcucWHXaM= +github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries v0.0.0-20250912122010-e00b74940f44 h1:lS40AYFAtQmu5EBO7aX1278DuUx815OIziyuFtSbYhQ= +github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries v0.0.0-20250912122010-e00b74940f44/go.mod h1:WrwTr9HFkp+nbHba/tv36eCLkjKAeCx7mqiG/wL3PNU= +github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos v0.0.0-20250912122010-e00b74940f44 h1:FrAq6nq+qkx8Z3KmxyBvihx54OEyFWIFP0d6bWL5cvw= +github.com/GoogleCloudPlatform/workloadagentplatform/sharedprotos v0.0.0-20250912122010-e00b74940f44/go.mod h1:8Ea8vdBuPsWhhwzL9sNK7BFQE9qbkPLZUHxcucWHXaM= github.com/SAP/go-hdb v1.12.12 h1:pZtsnUU7VNNobksc13F5pGr7W3abiJq/W4v7g7GZpKk= github.com/SAP/go-hdb v1.12.12/go.mod h1:R6RDbzvPk9gTraxYbzfNcy3XRp3vXFGd5vEopvzr0zQ= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index e247568b..7bf974b6 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "runtime" "strings" "time" @@ -43,8 +44,17 @@ type ReadConfigFile func(string) ([]byte, error) // WriteConfigFile abstracts os.WriteFile function for testability. type WriteConfigFile func(string, []byte, os.FileMode) error +// StatFile abstracts os.Stat function for testability. +type StatFile func(string) (os.FileInfo, error) + +// MkdirAll abstracts os.MkdirAll function for testability. +type MkdirAll func(string, os.FileMode) error + var ros = runtime.GOOS +//go:embed defaultconfigs/configuration.json +var defaultConfigurationContent []byte + //go:embed defaultconfigs/hanamonitoring/default_queries.json var defaultHMQueriesContent []byte @@ -83,6 +93,14 @@ func StorageAgentName() string { return fmt.Sprintf("google-cloud-sap-agent/%s (GPN: Agent for SAP)", AgentVersion) } +// Path returns the default configuration file path based on the OS. +func Path() string { + if runtime.GOOS == "windows" { + return WindowsConfigPath + } + return LinuxConfigPath +} + // Read just reads configuration from given file and parses it into config proto. func Read(path string, read ReadConfigFile) (*cpb.Configuration, error) { content, err := read(path) @@ -102,6 +120,18 @@ func Read(path string, read ReadConfigFile) (*cpb.Configuration, error) { return config, err } +// Write writes the contents of a configuration proto to a file at the given path. +func Write(config *cpb.Configuration, path string, write WriteConfigFile) error { + content, err := protojson.MarshalOptions{ + Multiline: true, + UseProtoNames: true, + }.Marshal(config) + if err != nil { + log.Logger.Errorw("Failed to marshal configuration proto to JSON", "error", err) + } + return write(path, content, 0644) +} + // ReadFromFile reads the final configuration from the given file. Besides parsing the file, // it consists of the final HANA Monitoring configuration after parsing all the enabled // HANA Monitoring queries, by applying overrides wherever necessary, into a proto. @@ -364,7 +394,7 @@ func prepareHMConf(config *cpb.HANAMonitoringConfiguration) *cpb.HANAMonitoringC // enabled/disabled. In case of default queries if there is no override item in the custom query list // then default query is treated as enabled. func applyOverrides(defaultHMQueriesList, customHMQueriesList []*cpb.Query) []*cpb.Query { - result := []*cpb.Query{} + var result []*cpb.Query for _, query := range defaultHMQueriesList { q := query q.Enabled = true @@ -467,3 +497,30 @@ func validateColumnTypes(col *cpb.Column) error { } return nil } + +// EnsureConfigExists ensures that the sapagent configuration file exists. +// If the file does not exist, it creates the file and its parent directories. +func EnsureConfigExists(stat StatFile, mkdirAll MkdirAll, write WriteConfigFile) error { + path := Path() + _, err := stat(path) + if err == nil { + return nil + } + + // We expect to see os.ErrNotExist if the file does not exist. + // Any other error is unexpected and should be returned. + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to stat configuration file: %w", err) + } + + dir := filepath.Dir(path) + if err := mkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create configuration file directory %s: %w", dir, err) + } + if err := write(path, defaultConfigurationContent, 0644); err != nil { + return fmt.Errorf("failed to write default configuration to file %s: %w", path, err) + } + + log.Logger.Infow("Default configuration file created", "path", path) + return nil +} diff --git a/internal/configuration/configuration_test.go b/internal/configuration/configuration_test.go index fe4e391e..93a56a76 100644 --- a/internal/configuration/configuration_test.go +++ b/internal/configuration/configuration_test.go @@ -18,11 +18,13 @@ package configuration import ( _ "embed" + "errors" "os" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/testing/protocmp" "go.uber.org/zap/zapcore" @@ -76,6 +78,34 @@ var ( testConfigWithSapSystemConfigJSON []byte ) +func TestWrite(t *testing.T) { + config := &cpb.Configuration{ + LogLevel: cpb.Configuration_INFO, + LogToCloud: &wpb.BoolValue{Value: true}, + ProvideSapHostAgentMetrics: &wpb.BoolValue{Value: true}, + } + f, err := os.CreateTemp("", "test_config") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(f.Name()) + err = Write(config, f.Name(), os.WriteFile) + if err != nil { + t.Fatalf("Write() got err: %v, want error: %t", err, false) + } + contents, err := os.ReadFile(f.Name()) + if err != nil { + t.Fatalf("Failed to read file: %v", err) + } + got := &cpb.Configuration{} + if err := protojson.Unmarshal(contents, got); err != nil { + t.Fatalf("Failed to unmarshal config: %v", err) + } + if diff := cmp.Diff(config, got, protocmp.Transform()); diff != "" { + t.Errorf("Write() returned unexpected diff (-want +got):\n%s", diff) + } +} + func TestReadFromFile(t *testing.T) { tests := []struct { name string @@ -1505,3 +1535,73 @@ func TestValidateAgentConfiguration(t *testing.T) { }) } } + +func TestEnsureConfigExists(t *testing.T) { + tests := []struct { + name string + stat StatFile + mkdirAll MkdirAll + write WriteConfigFile + wantErr error + }{ + { + name: "ConfigExists", + stat: func(string) (os.FileInfo, error) { + return nil, nil + }, + wantErr: nil, + }, + { + name: "StatUnexpectedError", + stat: func(string) (os.FileInfo, error) { + return nil, os.ErrPermission + }, + wantErr: os.ErrPermission, + }, + { + name: "MkdirAllError", + stat: func(string) (os.FileInfo, error) { + return nil, os.ErrNotExist + }, + mkdirAll: func(string, os.FileMode) error { + return os.ErrPermission + }, + wantErr: os.ErrPermission, + }, + { + name: "WriteError", + stat: func(string) (os.FileInfo, error) { + return nil, os.ErrNotExist + }, + mkdirAll: func(string, os.FileMode) error { + return nil + }, + write: func(string, []byte, os.FileMode) error { + return os.ErrPermission + }, + wantErr: os.ErrPermission, + }, + { + name: "CreateConfigSuccess", + stat: func(string) (os.FileInfo, error) { + return nil, os.ErrNotExist + }, + mkdirAll: func(string, os.FileMode) error { + return nil + }, + write: func(string, []byte, os.FileMode) error { + return nil + }, + wantErr: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := EnsureConfigExists(test.stat, test.mkdirAll, test.write) + if !errors.Is(err, test.wantErr) { + t.Errorf("EnsureConfigExists() returned error: %v, want error: %v", err, test.wantErr) + } + }) + } +} diff --git a/internal/configuration/defaultconfigs/configuration.json b/internal/configuration/defaultconfigs/configuration.json new file mode 100644 index 00000000..f4efe427 --- /dev/null +++ b/internal/configuration/defaultconfigs/configuration.json @@ -0,0 +1,15 @@ +{ + "provide_sap_host_agent_metrics": true, + "log_level": "INFO", + "log_to_cloud": true, + "collection_configuration": { + "collect_workload_validation_metrics": true, + "collect_process_metrics": false + }, + "discovery_configuration": { + "enable_discovery": true + }, + "hana_monitoring_configuration": { + "enabled": false + } +} diff --git a/internal/startdaemon/test_data/default_config.json b/internal/startdaemon/test_data/default_config.json new file mode 100644 index 00000000..57f68896 --- /dev/null +++ b/internal/startdaemon/test_data/default_config.json @@ -0,0 +1,11 @@ +{ + "provide_sap_host_agent_metrics": true, + "log_level": "INFO", + "log_to_cloud": true, + "collection_configuration": { + "collect_workload_validation_metrics": true + }, + "discovery_configuration": { + "enable_discovery": true + } +} diff --git a/internal/startdaemon/test_data/overwritten_config.json b/internal/startdaemon/test_data/overwritten_config.json new file mode 100644 index 00000000..09981a19 --- /dev/null +++ b/internal/startdaemon/test_data/overwritten_config.json @@ -0,0 +1,9 @@ +{ + "provide_sap_host_agent_metrics": true, + "log_level": "DEBUG", + "log_to_cloud": true, + "collection_configuration": { + "collect_workload_validation_metrics": false, + "collect_process_metrics": true + } +}