diff --git a/pkg/updater/install.go b/pkg/updater/install.go index b18b000a4a354c..092013a087f67a 100644 --- a/pkg/updater/install.go +++ b/pkg/updater/install.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "strings" + "sync" oci "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/types" @@ -27,11 +28,15 @@ const ( datadogPackageConfigLayerMediaType types.MediaType = "application/vnd.datadog.package.config.layer.v1.tar+zstd" datadogPackageMaxSize = 3 << 30 // 3GiB defaultConfigsDir = "/etc" + + packageDatadogAgent = "datadog-agent" + packageAPMInjector = "datadog-apm-inject" ) type installer struct { repositories *repository.Repositories configsDir string + installLock sync.Mutex } func newInstaller(repositories *repository.Repositories) *installer { @@ -56,10 +61,17 @@ func (i *installer) installStable(pkg string, version string, image oci.Image) e if err != nil { return fmt.Errorf("could not create repository: %w", err) } - if pkg == "datadog-agent" { + + i.installLock.Lock() + defer i.installLock.Unlock() + switch pkg { + case packageDatadogAgent: return service.SetupAgentUnits() + case packageAPMInjector: + return service.SetupAPMInjector() + default: + return nil } - return nil } func (i *installer) installExperiment(pkg string, version string, image oci.Image) error { @@ -100,19 +112,25 @@ func (i *installer) uninstallExperiment(pkg string) error { } func (i *installer) startExperiment(pkg string) error { - // TODO(arthur): currently we only support the datadog-agent package - if pkg != "datadog-agent" { + i.installLock.Lock() + defer i.installLock.Unlock() + switch pkg { + case packageDatadogAgent: + return service.StartAgentExperiment() + default: return nil } - return service.StartAgentExperiment() } func (i *installer) stopExperiment(pkg string) error { - // TODO(arthur): currently we only support the datadog-agent package - if pkg != "datadog-agent" { + i.installLock.Lock() + defer i.installLock.Unlock() + switch pkg { + case packageDatadogAgent: + return service.StopAgentExperiment() + default: return nil } - return service.StopAgentExperiment() } func extractPackageLayers(image oci.Image, configDir string, packageDir string) error { diff --git a/pkg/updater/service/apm_inject.go b/pkg/updater/service/apm_inject.go new file mode 100644 index 00000000000000..4982b61c2a826d --- /dev/null +++ b/pkg/updater/service/apm_inject.go @@ -0,0 +1,356 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build !windows + +// Package service provides a way to interact with os services +package service + +import ( + "bytes" + "fmt" + "os" + "path" + "strings" + + "github.com/DataDog/datadog-agent/pkg/util/log" +) + +var ( + injectorConfigPrefix = []byte("# BEGIN LD PRELOAD CONFIG") + injectorConfigSuffix = []byte("# END LD PRELOAD CONFIG") +) + +const ( + injectorConfigTemplate = ` +apm_config: + receiver_socket: %s +use_dogstatsd: true +dogstatsd_socket: %s +` + datadogConfigPath = "/etc/datadog-agent/datadog.yaml" + ldSoPreloadPath = "/etc/ld.so.preload" +) + +// SetupAPMInjector sets up the injector at bootstrap +func SetupAPMInjector() error { + // Enforce dd-installer is in the dd-agent group + if err := setInstallerAgentGroup(); err != nil { + return err + } + + installer := &apmInjectorInstaller{ + installPath: "/opt/datadog-packages/datadog-apm-inject/stable", + } + return installer.Setup() +} + +// RemoveAPMInjector removes the APM injector +func RemoveAPMInjector() error { + installer := &apmInjectorInstaller{ + installPath: "/opt/datadog-packages/datadog-apm-inject/stable", + } + return installer.Remove() +} + +type apmInjectorInstaller struct { + installPath string +} + +// Setup sets up the APM injector +func (a *apmInjectorInstaller) Setup() error { + var err error + defer func() { + if err != nil { + removeErr := a.Remove() + if removeErr != nil { + log.Warnf("Failed to remove APM injector: %v", removeErr) + } + } + }() + if err := a.setAgentConfig(); err != nil { + return err + } + if err := a.setRunPermissions(); err != nil { + return err + } + if err := a.setLDPreloadConfig(); err != nil { + return err + } + if err := a.setDockerConfig(); err != nil { + return err + } + return nil +} + +func (a *apmInjectorInstaller) Remove() error { + if err := a.deleteAgentConfig(); err != nil { + return err + } + if err := a.deleteLDPreloadConfig(); err != nil { + return err + } + if err := a.deleteDockerConfig(); err != nil { + return err + } + return nil +} + +func (a *apmInjectorInstaller) setRunPermissions() error { + return os.Chmod(path.Join(a.installPath, "inject", "run"), 0777) +} + +// setLDPreloadConfig adds preload options on /etc/ld.so.preload, overriding existing ones +func (a *apmInjectorInstaller) setLDPreloadConfig() error { + var ldSoPreload []byte + stat, err := os.Stat(ldSoPreloadPath) + if err == nil { + ldSoPreload, err = os.ReadFile(ldSoPreloadPath) + if err != nil { + return err + } + } else if !os.IsNotExist(err) { + return err + } + + newLdSoPreload, err := a.setLDPreloadConfigContent(ldSoPreload) + if err != nil { + return err + } + if bytes.Equal(ldSoPreload, newLdSoPreload) { + // No changes needed + return nil + } + + perms := os.FileMode(0644) + if stat != nil { + perms = stat.Mode() + } + err = os.WriteFile("/tmp/ld.so.preload.tmp", newLdSoPreload, perms) + if err != nil { + return err + } + + return executeCommand(string(replaceLDPreloadCommand)) +} + +// setLDPreloadConfigContent sets the content of the LD preload configuration +func (a *apmInjectorInstaller) setLDPreloadConfigContent(ldSoPreload []byte) ([]byte, error) { + launcherPreloadPath := path.Join(a.installPath, "inject", "launcher.preload.so") + + if strings.Contains(string(ldSoPreload), launcherPreloadPath) { + // If the line of interest is already in /etc/ld.so.preload, return fast + return ldSoPreload, nil + } + + // Append the launcher preload path to the file + if len(ldSoPreload) > 0 && ldSoPreload[len(ldSoPreload)-1] != '\n' { + ldSoPreload = append(ldSoPreload, '\n') + } + ldSoPreload = append(ldSoPreload, []byte(launcherPreloadPath+"\n")...) + return ldSoPreload, nil +} + +// deleteLDPreloadConfig removes the preload options from /etc/ld.so.preload +func (a *apmInjectorInstaller) deleteLDPreloadConfig() error { + var ldSoPreload []byte + stat, err := os.Stat(ldSoPreloadPath) + if err == nil { + ldSoPreload, err = os.ReadFile(ldSoPreloadPath) + if err != nil { + return err + } + } else if !os.IsNotExist(err) { + return err + } else { + return nil + } + + newLdSoPreload, err := a.deleteLDPreloadConfigContent(ldSoPreload) + if err != nil { + return err + } + if bytes.Equal(ldSoPreload, newLdSoPreload) { + // No changes needed + return nil + } + + perms := os.FileMode(0644) + if stat != nil { + perms = stat.Mode() + } + err = os.WriteFile("/tmp/ld.so.preload.tmp", newLdSoPreload, perms) + if err != nil { + return err + } + + return executeCommand(string(replaceLDPreloadCommand)) +} + +// deleteLDPreloadConfigContent deletes the content of the LD preload configuration +func (a *apmInjectorInstaller) deleteLDPreloadConfigContent(ldSoPreload []byte) ([]byte, error) { + launcherPreloadPath := path.Join(a.installPath, "inject", "launcher.preload.so") + + if !strings.Contains(string(ldSoPreload), launcherPreloadPath) { + // If the line of interest isn't there, return fast + return ldSoPreload, nil + } + + // Possible configurations of the preload path, order matters + replacementsToTest := [][]byte{ + []byte(launcherPreloadPath + "\n"), + []byte("\n" + launcherPreloadPath), + []byte(launcherPreloadPath + " "), + []byte(" " + launcherPreloadPath), + } + for _, replacement := range replacementsToTest { + ldSoPreloadNew := bytes.Replace(ldSoPreload, replacement, []byte{}, 1) + if !bytes.Equal(ldSoPreloadNew, ldSoPreload) { + return ldSoPreloadNew, nil + } + } + if bytes.Equal(ldSoPreload, []byte(launcherPreloadPath)) { + // If the line is the only one in the file without newlines, return an empty file + return []byte{}, nil + } + + return nil, fmt.Errorf("failed to remove %s from %s", launcherPreloadPath, ldSoPreloadPath) +} + +// setAgentConfig adds the agent configuration for the APM injector if it is not there already +// We assume that the agent file has been created by the installer's postinst script +// +// Note: This is not safe, as it assumes there were no changes to the agent configuration made without +// restart by the user. This means that the agent can crash on restart. This is a limitation of the current +// installer system and this will be replaced by a proper experiment when available. This is a temporary +// solution to allow the APM injector to be installed, and if the agent crashes, we try to detect it and +// restore the previous configuration +func (a *apmInjectorInstaller) setAgentConfig() (err error) { + err = backupAgentConfig() + if err != nil { + return err + } + defer func() { + if err != nil { + restoreErr := restoreAgentConfig() + if restoreErr != nil { + log.Warnf("Failed to restore agent config: %v", restoreErr) + } + } + }() + + content, err := os.ReadFile(datadogConfigPath) + if err != nil { + return err + } + + newContent := a.setAgentConfigContent(content) + if bytes.Equal(content, newContent) { + // No changes needed + return nil + } + + err = os.WriteFile(datadogConfigPath, newContent, 0644) + if err != nil { + return err + } + + err = restartTraceAgent() + return +} + +func (a *apmInjectorInstaller) setAgentConfigContent(content []byte) []byte { + runPath := path.Join(a.installPath, "inject", "run") + apmSocketPath := path.Join(runPath, "apm.socket") + dsdSocketPath := path.Join(runPath, "dsd.socket") + + if !bytes.Contains(content, injectorConfigPrefix) { + content = append(content, []byte("\n")...) + content = append(content, injectorConfigPrefix...) + content = append(content, []byte( + fmt.Sprintf(injectorConfigTemplate, apmSocketPath, dsdSocketPath), + )...) + content = append(content, injectorConfigSuffix...) + content = append(content, []byte("\n")...) + } + return content +} + +// deleteAgentConfig removes the agent configuration for the APM injector +func (a *apmInjectorInstaller) deleteAgentConfig() (err error) { + err = backupAgentConfig() + if err != nil { + return err + } + defer func() { + if err != nil { + restoreErr := restoreAgentConfig() + if restoreErr != nil { + log.Warnf("Failed to restore agent config: %v", restoreErr) + } + } + }() + + content, err := os.ReadFile(datadogConfigPath) + if err != nil { + return err + } + + newContent := a.deleteAgentConfigContent(content) + if bytes.Equal(content, newContent) { + // No changes needed + return nil + } + + err = os.WriteFile(datadogConfigPath, content, 0644) + if err != nil { + return err + } + + return restartTraceAgent() +} + +// deleteAgentConfigContent deletes the agent configuration for the APM injector +func (a *apmInjectorInstaller) deleteAgentConfigContent(content []byte) []byte { + start := bytes.Index(content, injectorConfigPrefix) + end := bytes.Index(content, injectorConfigSuffix) + len(injectorConfigSuffix) + if start == -1 || end == -1 || start >= end { + // Config not found + return content + } + + return append(content[:start], content[end:]...) +} + +// backupAgentConfig backs up the agent configuration +func backupAgentConfig() error { + return executeCommandStruct(privilegeCommand{ + Command: string(backupCommand), + Path: datadogConfigPath, + }) +} + +// restoreAgentConfig restores the agent configuration & restarts the agent +func restoreAgentConfig() error { + err := executeCommandStruct(privilegeCommand{ + Command: string(restoreCommand), + Path: datadogConfigPath, + }) + if err != nil { + return err + } + return restartTraceAgent() +} + +// restartTraceAgent restarts the trace agent, both stable and experimental +func restartTraceAgent() error { + if err := restartUnit("datadog-agent-trace.service"); err != nil { + return err + } + if err := restartUnit("datadog-agent-trace-exp.service"); err != nil { + return err + } + return nil +} diff --git a/pkg/updater/service/apm_inject_test.go b/pkg/updater/service/apm_inject_test.go new file mode 100644 index 00000000000000..813f800a0ee749 --- /dev/null +++ b/pkg/updater/service/apm_inject_test.go @@ -0,0 +1,155 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build !windows + +// Package service provides a way to interact with os services +package service + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetLDPreloadConfig(t *testing.T) { + a := &apmInjectorInstaller{ + installPath: "/tmp/stable", + } + + for input, expected := range map[string]string{ + // File doesn't exist + "": "/tmp/stable/inject/launcher.preload.so\n", + // File contains unrelated entries + "/abc/def/preload.so\n": "/abc/def/preload.so\n/tmp/stable/inject/launcher.preload.so\n", + // File contains unrelated entries with no newline + "/abc/def/preload.so": "/abc/def/preload.so\n/tmp/stable/inject/launcher.preload.so\n", + } { + output, err := a.setLDPreloadConfigContent([]byte(input)) + assert.Nil(t, err) + assert.Equal(t, expected, string(output)) + } +} + +func TestRemoveLDPreloadConfig(t *testing.T) { + a := &apmInjectorInstaller{ + installPath: "/tmp/stable", + } + + for input, expected := range map[string]string{ + // File doesn't exist + "": "", + // File only contains the entry to remove + "/tmp/stable/inject/launcher.preload.so\n": "", + // File only contains the entry to remove without newline + "/tmp/stable/inject/launcher.preload.so": "", + // File contains unrelated entries + "/abc/def/preload.so\n/tmp/stable/inject/launcher.preload.so\n": "/abc/def/preload.so\n", + // File contains unrelated entries at the end + "/tmp/stable/inject/launcher.preload.so\n/def/abc/preload.so": "/def/abc/preload.so", + // File contains multiple unrelated entries + "/abc/def/preload.so\n/tmp/stable/inject/launcher.preload.so\n/def/abc/preload.so": "/abc/def/preload.so\n/def/abc/preload.so", + // File contains unrelated entries with no newline (reformatted by customer?) + "/abc/def/preload.so /tmp/stable/inject/launcher.preload.so": "/abc/def/preload.so", + // File contains unrelated entries with no newline (reformatted by customer?) + "/abc/def/preload.so /tmp/stable/inject/launcher.preload.so /def/abc/preload.so": "/abc/def/preload.so /def/abc/preload.so", + // File contains unrelated entries with no newline (reformatted by customer?) + "/tmp/stable/inject/launcher.preload.so /def/abc/preload.so": "/def/abc/preload.so", + // File doesn't contain the entry to remove (removed by customer?) + "/abc/def/preload.so /def/abc/preload.so": "/abc/def/preload.so /def/abc/preload.so", + } { + output, err := a.deleteLDPreloadConfigContent([]byte(input)) + assert.Nil(t, err) + assert.Equal(t, expected, string(output)) + } + + // File is badly formatted (non-breaking space instead of space) + input := "/tmp/stable/inject/launcher.preload.so\u00a0/def/abc/preload.so" + output, err := a.deleteLDPreloadConfigContent([]byte(input)) + assert.NotNil(t, err) + assert.Equal(t, "", string(output)) +} + +func TestSetAgentConfig(t *testing.T) { + a := &apmInjectorInstaller{ + installPath: "/tmp/stable", + } + + for input, expected := range map[string]string{ + // File doesn't exist + "": ` +# BEGIN LD PRELOAD CONFIG +apm_config: + receiver_socket: /tmp/stable/inject/run/apm.socket +use_dogstatsd: true +dogstatsd_socket: /tmp/stable/inject/run/dsd.socket +# END LD PRELOAD CONFIG +`, + // File contains unrelated entries + `api_key: 000000000 +site: datad0g.com`: `api_key: 000000000 +site: datad0g.com +# BEGIN LD PRELOAD CONFIG +apm_config: + receiver_socket: /tmp/stable/inject/run/apm.socket +use_dogstatsd: true +dogstatsd_socket: /tmp/stable/inject/run/dsd.socket +# END LD PRELOAD CONFIG +`, + // File already contains the agent config + `# BEGIN LD PRELOAD CONFIG +apm_config: + receiver_socket: /tmp/stable/inject/run/apm.socket +use_dogstatsd: true +dogstatsd_socket: /tmp/stable/inject/run/dsd.socket +# END LD PRELOAD CONFIG`: `# BEGIN LD PRELOAD CONFIG +apm_config: + receiver_socket: /tmp/stable/inject/run/apm.socket +use_dogstatsd: true +dogstatsd_socket: /tmp/stable/inject/run/dsd.socket +# END LD PRELOAD CONFIG`, + } { + output := a.setAgentConfigContent([]byte(input)) + assert.Equal(t, expected, string(output)) + } +} + +func TestRemoveAgentConfig(t *testing.T) { + a := &apmInjectorInstaller{ + installPath: "/tmp/stable", + } + + for input, expected := range map[string]string{ + // File doesn't exist + "": "", + // File only contains the agent config + `# BEGIN LD PRELOAD CONFIG + apm_config: + receiver_socket: /tmp/stable/inject/run/apm.socket + use_dogstatsd: true + dogstatsd_socket: /tmp/stable/inject/run/dsd.socket + # END LD PRELOAD CONFIG`: "", + // File contains unrelated entries + `api_key: 000000000 +site: datad0g.com +# BEGIN LD PRELOAD CONFIG +apm_config: + receiver_socket: /tmp/stable/inject/run/apm.socket +use_dogstatsd: true +dogstatsd_socket: /tmp/stable/inject/run/dsd.socket +# END LD PRELOAD CONFIG +`: `api_key: 000000000 +site: datad0g.com + +`, + // File **only** contains unrelated entries somehow + `api_key: 000000000 +site: datad0g.com`: `api_key: 000000000 +site: datad0g.com`, + } { + output := a.deleteAgentConfigContent([]byte(input)) + assert.Equal(t, expected, string(output)) + } +} diff --git a/pkg/updater/service/apm_inject_windows.go b/pkg/updater/service/apm_inject_windows.go new file mode 100644 index 00000000000000..8bbb49c5c70956 --- /dev/null +++ b/pkg/updater/service/apm_inject_windows.go @@ -0,0 +1,19 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build windows + +// Package service provides a way to interact with os services +package service + +// SetupAPMInjector noop +func SetupAPMInjector() error { + return nil +} + +// RemoveAPMInjector noop +func RemoveAPMInjector() error { + return nil +} diff --git a/pkg/updater/service/datadog_agent.go b/pkg/updater/service/datadog_agent.go index e183f9f5f5229b..8767e7a20d864d 100644 --- a/pkg/updater/service/datadog_agent.go +++ b/pkg/updater/service/datadog_agent.go @@ -9,6 +9,9 @@ package service import ( + "os/exec" + "strings" + "github.com/DataDog/datadog-agent/pkg/util/installinfo" "github.com/DataDog/datadog-agent/pkg/util/log" ) @@ -52,6 +55,10 @@ func SetupAgentUnits() (err error) { } }() + if err = setInstallerAgentGroup(); err != nil { + return + } + for _, unit := range stableUnits { if err = loadUnit(unit); err != nil { return @@ -132,3 +139,16 @@ func StartAgentExperiment() error { func StopAgentExperiment() error { return startUnit(agentUnit) } + +// setInstallerAgentGroup adds the dd-installer to the dd-agent group if it's not already in it +func setInstallerAgentGroup() error { + // Get groups of dd-installer + out, err := exec.Command("id", "-Gn", "dd-installer").Output() + if err != nil { + return err + } + if strings.Contains(string(out), "dd-agent") { + return nil + } + return executeCommand(string(addInstallerToAgentGroup)) +} diff --git a/pkg/updater/service/docker.go b/pkg/updater/service/docker.go new file mode 100644 index 00000000000000..c4cdb3fc0de209 --- /dev/null +++ b/pkg/updater/service/docker.go @@ -0,0 +1,196 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build !windows + +// Package service provides a way to interact with os services +package service + +import ( + "bytes" + "encoding/json" + "os" + "os/exec" + "path" + + "github.com/DataDog/datadog-agent/pkg/util/log" +) + +type dockerDaemonConfig map[string]interface{} + +const ( + tmpDockerDaemonPath = "/tmp/daemon.json.tmp" + dockerDaemonPath = "/etc/docker/daemon.json" +) + +// setDockerConfig sets up the docker daemon to use the APM injector +// even if docker isn't installed, to prepare for if it is installed +// later +func (a *apmInjectorInstaller) setDockerConfig() error { + // Create docker dir if it doesn't exist + err := executeCommand(createDockerDirCommand) + if err != nil { + return err + } + + var file []byte + stat, err := os.Stat(dockerDaemonPath) + if err == nil { + // Read the existing configuration + file, err = os.ReadFile(dockerDaemonPath) + if err != nil { + return err + } + } else if !os.IsNotExist(err) { + return err + } + + dockerConfigJSON, err := a.setDockerConfigContent(file) + if err != nil { + return err + } + + // Write the new configuration to a temporary file + perms := os.FileMode(0644) + if stat != nil { + perms = stat.Mode() + } + err = os.WriteFile(tmpDockerDaemonPath, dockerConfigJSON, perms) + if err != nil { + return err + } + + // Move the temporary file to the final location + err = executeCommand(string(replaceDockerCommand)) + if err != nil { + return err + } + + return restartDocker() +} + +// setDockerConfigContent sets the content of the docker daemon configuration +func (a *apmInjectorInstaller) setDockerConfigContent(previousContent []byte) ([]byte, error) { + dockerConfig := dockerDaemonConfig{} + + if len(previousContent) > 0 { + err := json.Unmarshal(previousContent, &dockerConfig) + if err != nil { + return nil, err + } + } + + if _, ok := dockerConfig["default-runtime"]; ok { + dockerConfig["default-runtime-backup"] = dockerConfig["default-runtime"] + } + dockerConfig["default-runtime"] = "dd-shim" + runtimes, ok := dockerConfig["runtimes"].(map[string]interface{}) + if !ok { + runtimes = map[string]interface{}{} + } + runtimes["dd-shim"] = map[string]interface{}{ + "path": path.Join(a.installPath, "inject", "auto_inject_runc"), + } + dockerConfig["runtimes"] = runtimes + + dockerConfigJSON, err := json.MarshalIndent(dockerConfig, "", " ") + if err != nil { + return nil, err + } + + return dockerConfigJSON, nil +} + +// deleteDockerConfig restores the docker daemon configuration +func (a *apmInjectorInstaller) deleteDockerConfig() error { + var file []byte + stat, err := os.Stat(dockerDaemonPath) + if err == nil { + // Read the existing configuration + file, err = os.ReadFile(dockerDaemonPath) + if err != nil { + return err + } + } else if os.IsNotExist(err) { + // If the file doesn't exist, there's nothing to do + return nil + } + + dockerConfigJSON, err := a.deleteDockerConfigContent(file) + if err != nil { + return err + } + + // Write the new configuration to a temporary file + perms := os.FileMode(0644) + if stat != nil { + perms = stat.Mode() + } + err = os.WriteFile(tmpDockerDaemonPath, dockerConfigJSON, perms) + if err != nil { + return err + } + + // Move the temporary file to the final location + err = executeCommand(string(replaceDockerCommand)) + if err != nil { + return err + } + return restartDocker() +} + +// deleteDockerConfigContent restores the content of the docker daemon configuration +func (a *apmInjectorInstaller) deleteDockerConfigContent(previousContent []byte) ([]byte, error) { + dockerConfig := dockerDaemonConfig{} + + if len(previousContent) > 0 { + err := json.Unmarshal(previousContent, &dockerConfig) + if err != nil { + return nil, err + } + } + + if _, ok := dockerConfig["default-runtime-backup"]; ok { + dockerConfig["default-runtime"] = dockerConfig["default-runtime-backup"] + delete(dockerConfig, "default-runtime-backup") + } else { + dockerConfig["default-runtime"] = "runc" + } + runtimes, ok := dockerConfig["runtimes"].(map[string]interface{}) + if !ok { + runtimes = map[string]interface{}{} + } + delete(runtimes, "dd-shim") + dockerConfig["runtimes"] = runtimes + + dockerConfigJSON, err := json.MarshalIndent(dockerConfig, "", " ") + if err != nil { + return nil, err + } + + return dockerConfigJSON, nil +} + +// restartDocker reloads the docker daemon if it exists +func restartDocker() error { + if !isDockerInstalled() { + log.Info("updater: docker is not installed, skipping reload") + return nil + } + return executeCommand(restartDockerCommand) +} + +// isDockerInstalled checks if docker is installed on the system +func isDockerInstalled() bool { + cmd := exec.Command("which", "docker") + var outb bytes.Buffer + cmd.Stdout = &outb + err := cmd.Run() + if err != nil { + log.Warn("updater: failed to check if docker is installed, assuming it isn't: ", err) + return false + } + return len(outb.String()) != 0 +} diff --git a/pkg/updater/service/docker_test.go b/pkg/updater/service/docker_test.go new file mode 100644 index 00000000000000..912a4d680a6061 --- /dev/null +++ b/pkg/updater/service/docker_test.go @@ -0,0 +1,137 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build !windows + +// Package service provides a way to interact with os services +package service + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetDockerConfig(t *testing.T) { + a := &apmInjectorInstaller{ + installPath: "/tmp/stable", + } + + for input, expected := range map[string]string{ + // File doesn't exist + "": `{ + "default-runtime": "dd-shim", + "runtimes": { + "dd-shim": { + "path": "/tmp/stable/inject/auto_inject_runc" + } + } +}`, + // File contains unrelated entries + `{ + "cgroup-parent": "abc", + "raw-logs": false +}`: `{ + "cgroup-parent": "abc", + "default-runtime": "dd-shim", + "raw-logs": false, + "runtimes": { + "dd-shim": { + "path": "/tmp/stable/inject/auto_inject_runc" + } + } +}`, + // File has already overridden the default runtime + `{ + "default-runtime": "containerd", + "runtimes": { + "containerd": { + "path": "/usr/bin/containerd" + } + } +}`: `{ + "default-runtime": "dd-shim", + "default-runtime-backup": "containerd", + "runtimes": { + "containerd": { + "path": "/usr/bin/containerd" + }, + "dd-shim": { + "path": "/tmp/stable/inject/auto_inject_runc" + } + } +}`, + } { + output, err := a.setDockerConfigContent([]byte(input)) + assert.Nil(t, err) + assert.Equal(t, expected, string(output)) + } +} + +func TestRemoveDockerConfig(t *testing.T) { + a := &apmInjectorInstaller{ + installPath: "/tmp/stable", + } + + for input, expected := range map[string]string{ + // Empty file, shouldn't happen but still tested + "": `{ + "default-runtime": "runc", + "runtimes": {} +}`, + // File only contains the injected content + `{ + "default-runtime": "dd-shim", + "runtimes": { + "dd-shim": { + "path": "/tmp/stable/inject/auto_inject_runc" + } + } + }`: `{ + "default-runtime": "runc", + "runtimes": {} +}`, + // File contained unrelated entries + `{ + "cgroup-parent": "abc", + "default-runtime": "dd-shim", + "raw-logs": false, + "runtimes": { + "dd-shim": { + "path": "/tmp/stable/inject/auto_inject_runc" + } + } +}`: `{ + "cgroup-parent": "abc", + "default-runtime": "runc", + "raw-logs": false, + "runtimes": {} +}`, + // File had already overridden the default runtime + `{ + "default-runtime": "dd-shim", + "default-runtime-backup": "containerd", + "runtimes": { + "containerd": { + "path": "/usr/bin/containerd" + }, + "dd-shim": { + "path": "/tmp/stable/inject/auto_inject_runc" + } + } +}`: `{ + "default-runtime": "containerd", + "runtimes": { + "containerd": { + "path": "/usr/bin/containerd" + } + } +}`, + } { + output, err := a.deleteDockerConfigContent([]byte(input)) + assert.Nil(t, err) + assert.Equal(t, expected, string(output)) + } +} diff --git a/pkg/updater/service/helper/main.go b/pkg/updater/service/helper/main.go index 37f9ac13a06c63..a20a1de7a99c12 100644 --- a/pkg/updater/service/helper/main.go +++ b/pkg/updater/service/helper/main.go @@ -8,6 +8,7 @@ package main import ( + "bytes" "encoding/json" "fmt" "log" @@ -25,6 +26,8 @@ var ( installPath string systemdPath = "/lib/systemd/system" // todo load it at build time from omnibus pkgDir = "/opt/datadog-packages" + agentDir = "/etc/datadog-agent" + dockerDir = "/etc/docker" testSkipUID = "" ) @@ -36,6 +39,7 @@ type privilegeCommand struct { Command string `json:"command,omitempty"` Unit string `json:"unit,omitempty"` Path string `json:"path,omitempty"` + Content string `json:"content,omitempty"` } func isValidUnitChar(c rune) bool { @@ -66,6 +70,16 @@ func buildCommand(inputCommand privilegeCommand) (*exec.Cmd, error) { return exec.Command("ln", "-sf", "/opt/datadog-packages/datadog-agent/stable/bin/agent/agent", "/usr/bin/datadog-agent"), nil case "rm-agent-symlink": return exec.Command("rm", "-f", "/usr/bin/datadog-agent"), nil + case "create-docker-dir": + return exec.Command("mkdir", "-p", "/etc/docker"), nil + case "replace-docker": + return exec.Command("mv", "/tmp/daemon.json.tmp", "/etc/docker/daemon.json"), nil + case "restart-docker": + return exec.Command("systemctl", "restart", "docker"), nil + case "replace-ld-preload": + return exec.Command("mv", "/tmp/ld.so.preload.tmp", "/etc/ld.so.preload"), nil + case "add-installer-to-agent-group": + return exec.Command("usermod", "-aG", "dd-agent", "dd-installer"), nil default: return nil, fmt.Errorf("invalid command") } @@ -99,7 +113,7 @@ func buildPathCommand(inputCommand privilegeCommand) (*exec.Cmd, error) { if absPath != path || err != nil { return nil, fmt.Errorf("invalid path") } - if !strings.HasPrefix(path, pkgDir) { + if !strings.HasPrefix(path, pkgDir) && !strings.HasPrefix(path, agentDir) { return nil, fmt.Errorf("invalid path") } switch inputCommand.Command { @@ -107,6 +121,10 @@ func buildPathCommand(inputCommand privilegeCommand) (*exec.Cmd, error) { return exec.Command("chown", "-R", "dd-agent:dd-agent", path), nil case "rm": return exec.Command("rm", "-rf", path), nil + case "backup-file": + return exec.Command("cp", "-f", path, path+".bak"), nil + case "restore-file": + return exec.Command("mv", path+".bak", path), nil default: return nil, fmt.Errorf("invalid command") } @@ -121,7 +139,7 @@ func executeCommand() error { var pc privilegeCommand err := json.Unmarshal([]byte(inputCommand), &pc) if err != nil { - return fmt.Errorf("decoding command") + return fmt.Errorf("decoding command %s", inputCommand) } currentUser := syscall.Getuid() @@ -150,8 +168,14 @@ func executeCommand() error { }() } + commandErr := new(bytes.Buffer) + command.Stderr = commandErr log.Printf("Running command: %s", command.String()) - return command.Run() + err = command.Run() + if err != nil { + return fmt.Errorf("running command (%s): %s", err.Error(), commandErr.String()) + } + return nil } func main() { diff --git a/pkg/updater/service/systemd.go b/pkg/updater/service/systemd.go index 21f70d94b0fefe..2f384b010ad2ef 100644 --- a/pkg/updater/service/systemd.go +++ b/pkg/updater/service/systemd.go @@ -10,25 +10,58 @@ package service import ( "encoding/json" + "os" + "path" + + "github.com/DataDog/datadog-agent/pkg/util/log" ) type unitCommand string +var ( + systemdPath = "/lib/systemd/system" // todo load it at build time from omnibus +) + const ( - startCommand unitCommand = "start" - stopCommand unitCommand = "stop" - enableCommand unitCommand = "enable" - disableCommand unitCommand = "disable" - loadCommand unitCommand = "load-unit" - removeCommand unitCommand = "remove-unit" - systemdReloadCommand = `{"command":"systemd-reload"}` - adminExecutor = "datadog-updater-admin.service" + startCommand unitCommand = "start" + stopCommand unitCommand = "stop" + enableCommand unitCommand = "enable" + disableCommand unitCommand = "disable" + loadCommand unitCommand = "load-unit" + removeCommand unitCommand = "remove-unit" + addInstallerToAgentGroup unitCommand = "add-installer-to-agent-group" + backupCommand unitCommand = `backup-file` + restoreCommand unitCommand = `restore-file` + replaceDockerCommand = `{"command":"replace-docker"}` + restartDockerCommand = `{"command":"restart-docker"}` + createDockerDirCommand = `{"command":"create-docker-dir"}` + replaceLDPreloadCommand = `{"command":"replace-ld-preload"}` + systemdReloadCommand = `{"command":"systemd-reload"}` + adminExecutor = "datadog-updater-admin.service" ) type privilegeCommand struct { Command string `json:"command,omitempty"` Unit string `json:"unit,omitempty"` Path string `json:"path,omitempty"` + Content string `json:"content,omitempty"` +} + +// restartUnit restarts a systemd unit +func restartUnit(unit string) error { + // check that the unit exists first + if _, err := os.Stat(path.Join(systemdPath, unit)); os.IsNotExist(err) { + log.Infof("Unit %s does not exist, skipping restart", unit) + return nil + } + + if err := stopUnit(unit); err != nil { + return err + } + if err := startUnit(unit); err != nil { + return err + } + return nil } func stopUnit(unit string) error { @@ -68,3 +101,12 @@ func wrapUnitCommand(command unitCommand, unit string) string { } return string(rawJSON) } + +func executeCommandStruct(command privilegeCommand) error { + rawJSON, err := json.Marshal(command) + if err != nil { + return err + } + privilegeCommandJSON := string(rawJSON) + return executeCommand(privilegeCommandJSON) +} diff --git a/pkg/updater/service/systemd_test.go b/pkg/updater/service/systemd_test.go index 85f48151561e4e..51212f6caa0151 100644 --- a/pkg/updater/service/systemd_test.go +++ b/pkg/updater/service/systemd_test.go @@ -26,8 +26,8 @@ func TestInvalidCommands(t *testing.T) { // assert wrong commands for input, expected := range map[string]string{ // fail assert_command characters assertion - ";": "error: decoding command\n", - "&": "error: decoding command\n", + ";": "error: decoding command ;\n", + "&": "error: decoding command &\n", `{"command":"start", "unit":"does-not-exist"}`: "error: invalid unit\n", `{"command":"start", "unit":"datadog-//"}`: "error: invalid unit\n", `{"command":"does-not-exist", "unit":"datadog-"}`: "error: invalid command\n", @@ -55,4 +55,13 @@ func TestAssertWorkingCommands(t *testing.T) { assert.Equal(t, successErr, removeUnit("datadog-agent").Error()) assert.Equal(t, successErr, createAgentSymlink().Error()) assert.Equal(t, successErr, rmAgentSymlink().Error()) + assert.Equal(t, successErr, backupAgentConfig().Error()) + assert.Equal(t, successErr, restoreAgentConfig().Error()) + + a := &apmInjectorInstaller{ + installPath: "/tmp/stable", + } + assert.Equal(t, successErr, a.setLDPreloadConfig().Error()) + assert.Equal(t, successErr, a.setAgentConfig().Error()) + assert.Equal(t, successErr, a.setDockerConfig().Error()) } diff --git a/pkg/updater/updater.go b/pkg/updater/updater.go index 76ca92b255d319..766952a8ab57b9 100644 --- a/pkg/updater/updater.go +++ b/pkg/updater/updater.go @@ -109,6 +109,9 @@ func Purge() { func purge(locksPath, repositoryPath string) { service.RemoveAgentUnits() + if err := service.RemoveAPMInjector(); err != nil { + log.Warnf("updater: could not remove APM injector: %v", err) + } cleanDir(locksPath, os.RemoveAll) cleanDir(repositoryPath, service.RemoveAll) } @@ -220,7 +223,7 @@ func (u *updaterImpl) BootstrapDefault(ctx context.Context, pkg string) (err err stablePackage, ok := u.catalog.getDefaultPackage(u.bootstrapVersions, pkg, runtime.GOARCH, runtime.GOOS) if !ok { - return fmt.Errorf("could not get default package %s for %s, %s", pkg, runtime.GOARCH, runtime.GOOS) + return fmt.Errorf("could not get default package '%s' for arch '%s' and platform '%s'", pkg, runtime.GOARCH, runtime.GOOS) } return u.boostrapPackage(ctx, stablePackage.URL, stablePackage.Name, stablePackage.Version) } @@ -236,7 +239,7 @@ func (u *updaterImpl) BootstrapVersion(ctx context.Context, pkg string, version stablePackage, ok := u.catalog.getPackage(pkg, version, runtime.GOARCH, runtime.GOOS) if !ok { - return fmt.Errorf("could not get package %s version %s for %s, %s", pkg, version, runtime.GOARCH, runtime.GOOS) + return fmt.Errorf("could not get package '%s' version '%s' for arch '%s' and platform '%s'", pkg, version, runtime.GOARCH, runtime.GOOS) } return u.boostrapPackage(ctx, stablePackage.URL, stablePackage.Name, stablePackage.Version) } diff --git a/test/new-e2e/tests/updater/docker.go b/test/new-e2e/tests/updater/docker.go new file mode 100644 index 00000000000000..3762f53f589d4b --- /dev/null +++ b/test/new-e2e/tests/updater/docker.go @@ -0,0 +1,91 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +// Package updater contains tests for the updater package +package updater + +import ( + "testing" + "time" + + "github.com/DataDog/datadog-agent/test/new-e2e/pkg/components" + "github.com/DataDog/test-infra-definitions/components/os" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// installDocker installs docker on the host +func installDocker(distro os.Descriptor, t *testing.T, host *components.RemoteHost) { + switch distro { + case os.UbuntuDefault: + _, err := host.WriteFile("/tmp/install-docker.sh", []byte(` +sudo apt-get update +sudo apt-get install ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt-get update +sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + `)) + require.Nil(t, err) + host.MustExecute(`sudo chmod +x /tmp/install-docker.sh`) + host.MustExecute(`sudo /tmp/install-docker.sh`) + err = host.Remove("/tmp/install-docker.sh") + require.Nil(t, err) + case os.DebianDefault: + _, err := host.WriteFile("/tmp/install-docker.sh", []byte(` +sudo apt-get update +sudo apt-get install ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +# Add the repository to Apt sources: +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt-get update +sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + `)) + require.Nil(t, err) + host.MustExecute(`sudo chmod +x /tmp/install-docker.sh`) + host.MustExecute(`sudo /tmp/install-docker.sh`) + err = host.Remove("/tmp/install-docker.sh") + require.Nil(t, err) + case os.CentOSDefault, os.RedHatDefault: + _, err := host.WriteFile("/tmp/install-docker.sh", []byte(` +sudo yum install -y yum-utils +sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +sudo systemctl start docker + `)) + require.Nil(t, err) + host.MustExecute(`sudo chmod +x /tmp/install-docker.sh`) + host.MustExecute(`sudo /tmp/install-docker.sh`) + err = host.Remove("/tmp/install-docker.sh") + require.Nil(t, err) + default: + t.Fatalf("unsupported distro: %s", distro.String()) + } +} + +// launchJavaDockerContainer launches a small Java HTTP server in a docker container +// and make a call to it +func launchJavaDockerContainer(t *testing.T, host *components.RemoteHost) { + host.MustExecute(`sudo docker run -d -p8887:8888 baptistefoy702/message-server:latest`) + // for i := 0; i < 10; i++ { + assert.Eventually(t, + func() bool { + _, err := host.Execute(`curl -m 1 localhost:8887/messages`) + return err == nil + }, 10*time.Second, 100*time.Millisecond, + ) + // } +} diff --git a/test/new-e2e/tests/updater/linux_test.go b/test/new-e2e/tests/updater/linux_test.go index 4d49c168ef9557..59280eaf1f0f7d 100644 --- a/test/new-e2e/tests/updater/linux_test.go +++ b/test/new-e2e/tests/updater/linux_test.go @@ -12,6 +12,7 @@ import ( "regexp" "strings" "testing" + "time" "github.com/DataDog/test-infra-definitions/components/os" "github.com/DataDog/test-infra-definitions/scenarios/aws/ec2" @@ -37,12 +38,14 @@ const ( type vmUpdaterSuite struct { e2e.BaseSuite[environments.Host] packageManager string + distro os.Descriptor + arch os.Architecture } func runTest(t *testing.T, pkgManager string, arch os.Architecture, distro os.Descriptor) { reg := regexp.MustCompile(`[^a-zA-Z0-9_\-.]`) testName := reg.ReplaceAllString(distro.String()+"-"+string(arch), "_") - e2e.Run(t, &vmUpdaterSuite{packageManager: pkgManager}, e2e.WithProvisioner(awshost.ProvisionerNoFakeIntake( + e2e.Run(t, &vmUpdaterSuite{packageManager: pkgManager, distro: distro, arch: arch}, e2e.WithProvisioner(awshost.ProvisionerNoFakeIntake( awshost.WithUpdater(), awshost.WithEC2InstanceOptions(ec2.WithOSArch(distro, arch)), )), @@ -202,6 +205,142 @@ func (v *vmUpdaterSuite) TestPurgeAndInstallAgent() { } } +func (v *vmUpdaterSuite) TestPurgeAndInstallAPMInjector() { + // Temporarily disable CentOS & Redhat, as there is a bug in the APM injector + if v.distro == os.CentOSDefault || v.distro == os.RedHatDefault { + v.T().Skip("APM injector not available for CentOS or RedHat yet") + } + if v.distro == os.DebianDefault || v.distro == os.UbuntuDefault && v.arch == os.AMD64Arch { + // TODO (baptiste): Fix test + v.T().Skip("Test has been temporarily disabled") + } + + host := v.Env().RemoteHost + + /////////////////// + // Setup machine // + /////////////////// + + host.MustExecute(fmt.Sprintf("sudo %v/bin/installer/installer purge", bootUpdaterDir)) + // Install docker + installDocker(v.distro, v.T(), host) + defer func() { + // Best effort to stop any running container at the end of the test + host.Execute(`sudo docker ps -aq | xargs sudo docker stop | xargs sudo docker rm`) + }() + + ///////////////////////// + // Check initial state // + ///////////////////////// + + // packages dir exists; but there are no packages installed + host.MustExecute(`test -d /opt/datadog-packages`) + _, err := host.Execute(`test -d /opt/datadog-packages/datadog-apm-inject`) + require.NotNil(v.T(), err) + _, err = host.Execute(`test -d /opt/datadog-packages/datadog-agent`) + require.NotNil(v.T(), err) + _, err = host.Execute(`test -d /opt/datadog-packages/datadog-apm-library-java`) + require.NotNil(v.T(), err) + + // /etc/ld.so.preload does not contain the injector + _, err = host.Execute(`grep "/opt/datadog-packages/datadog-apm-inject" /etc/ld.so.preload`) + require.NotNil(v.T(), err) + + // docker daemon does not contain the injector + _, err = host.Execute(`grep "/opt/datadog-packages/datadog-apm-inject" /etc/docker/daemon.json`) + require.NotNil(v.T(), err) + + //////////////////////// + // Bootstrap packages // + //////////////////////// + + host.MustExecute(fmt.Sprintf(`sudo %v/bin/installer/installer bootstrap --url "oci://docker.io/datadog/agent-package-dev:7.54.0-devel.git.247.f92fbc1.pipeline.31778392-1"`, bootUpdaterDir)) + host.MustExecute(fmt.Sprintf(`sudo %v/bin/installer/installer bootstrap --url "oci://docker.io/datadog/apm-library-java-package-dev:1.32.0-SNAPSHOT-8708864e8e-pipeline.30373268.beta.8708864e-1"`, bootUpdaterDir)) + host.MustExecute(fmt.Sprintf(`sudo %v/bin/installer/installer bootstrap --url "oci://docker.io/datadog/apm-inject-package-dev:0.12.3-dev.bddec85.glci481808135.g8acdc698-1"`, bootUpdaterDir)) + + //////////////////////////////// + // Check post-bootstrap state // + //////////////////////////////// + + // assert packages dir exist + host.MustExecute(`test -L /opt/datadog-packages/datadog-agent/stable`) + host.MustExecute(`test -L /opt/datadog-packages/datadog-apm-library-java/stable`) + host.MustExecute(`test -L /opt/datadog-packages/datadog-apm-inject/stable`) + + // assert /etc/ld.so.preload contains the injector + res, err := host.Execute(`grep "/opt/datadog-packages/datadog-apm-inject" /etc/ld.so.preload`) + require.Nil(v.T(), err) + require.Equal(v.T(), "/opt/datadog-packages/datadog-apm-inject/stable/inject/launcher.preload.so\n", res) + + // assert docker daemon contains the injector (removing blank spaces for easier comparison) + res, err = host.Execute(`grep "/opt/datadog-packages/datadog-apm-inject" /etc/docker/daemon.json | sed -re 's/^[[:blank:]]+|[[:blank:]]+$//g' -e 's/[[:blank:]]+/ /g'`) + require.Nil(v.T(), err) + require.Equal(v.T(), "\"path\": \"/opt/datadog-packages/datadog-apm-inject/stable/inject/auto_inject_runc\"\n", res) + + // assert agent config has been changed + raw, err := host.ReadFile("/etc/datadog-agent/datadog.yaml") + require.Nil(v.T(), err) + require.True(v.T(), strings.Contains(string(raw), "# BEGIN LD PRELOAD CONFIG"), "missing LD_PRELOAD config, config:\n%s", string(raw)) + + // assert agent is running + host.MustExecute("sudo systemctl status datadog-agent.service") + + _, err = host.Execute("sudo systemctl status datadog-agent-trace.service") + require.Nil(v.T(), err) + + // assert required files exist + requiredFiles := []string{ + "auto_inject_runc", + "launcher.preload.so", + "ld.so.preload", + "musl-launcher.preload.so", + "process", + } + for _, file := range requiredFiles { + host.MustExecute(fmt.Sprintf("test -f /opt/datadog-packages/datadog-apm-inject/stable/inject/%s", file)) + } + + // assert file ownerships + injectorDir := "/opt/datadog-packages/datadog-apm-inject" + require.Equal(v.T(), "dd-installer\n", host.MustExecute(`stat -c "%U" `+injectorDir)) + require.Equal(v.T(), "dd-installer\n", host.MustExecute(`stat -c "%G" `+injectorDir)) + require.Equal(v.T(), "drwxr-xr-x\n", host.MustExecute(`stat -c "%A" `+injectorDir)) + require.Equal(v.T(), "1\n", host.MustExecute(`sudo ls -l /opt/datadog-packages/datadog-apm-inject | awk '$9 != "stable" && $3 == "dd-installer" && $4 == "dd-installer"' | wc -l`)) + + ///////////////////////////////////// + // Check injection with a real app // + ///////////////////////////////////// + + launchJavaDockerContainer(v.T(), host) + + // check "Dropping Payload due to non-retryable error" in trace agent logs + // as we don't have an API key the payloads can't be flushed successfully, + // but this log indicates that the trace agent managed to receive the payload + require.Eventually(v.T(), func() bool { + _, err := host.Execute(`cat /var/log/datadog/trace-agent.log | grep "Dropping Payload due to non-retryable error"`) + return err == nil + }, 30*time.Second, 100*time.Millisecond) + + /////////////////////// + // Check purge state // + /////////////////////// + + host.MustExecute(fmt.Sprintf("sudo %v/bin/installer/installer purge", bootUpdaterDir)) + + _, err = host.Execute(`test -d /opt/datadog-packages/datadog-apm-inject`) + require.NotNil(v.T(), err) + _, err = host.Execute(`test -d /opt/datadog-packages/datadog-agent`) + require.NotNil(v.T(), err) + _, err = host.Execute(`test -d /opt/datadog-packages/datadog-apm-library-java`) + require.NotNil(v.T(), err) + _, err = host.Execute(`grep "/opt/datadog-packages/datadog-apm-inject" /etc/ld.so.preload`) + require.NotNil(v.T(), err) + _, err = host.Execute(`grep "/opt/datadog-packages/datadog-apm-inject" /etc/docker/daemon.json`) + require.NotNil(v.T(), err) + _, err = host.Execute(`test -f /etc/docker/daemon.json.bak`) + require.NotNil(v.T(), err) +} + func assertInstallMethod(v *vmUpdaterSuite, t *testing.T, host *components.RemoteHost) { rawYaml, err := host.ReadFile(filepath.Join(confDir, "install_info")) assert.Nil(t, err)