pkg/testing/fixture_install.go (630 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package testing import ( "archive/zip" "context" "errors" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" gotesting "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/elastic/elastic-agent-libs/mapstr" agentsystemprocess "github.com/elastic/elastic-agent-system-metrics/metric/system/process" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" "github.com/elastic/elastic-agent/pkg/control/v2/client" "github.com/elastic/elastic-agent/pkg/core/process" ) // ErrNotInstalled is returned in cases where Agent isn't installed var ErrNotInstalled = errors.New("Elastic Agent is not installed") //nolint:stylecheck // Elastic Agent is a proper noun // CmdOpts creates vectors of command arguments for different agent commands type CmdOpts interface { toCmdArgs() []string } // EnrollOpts specifies the options for the enroll command type EnrollOpts struct { ID string // --id URL string // --url EnrollmentToken string // --enrollment-token ReplaceToken string // --replace-token // SSL/TLS options CertificateAuthorities []string // --certificate-authorities Certificate string // --elastic-agent-cert Key string // --elastic-agent-cert-key KeyPassphrasePath string // --elastic-agent-cert-key-passphrase } func (e EnrollOpts) toCmdArgs() []string { var args []string if e.ID != "" { args = append(args, "--id", e.ID) } if e.URL != "" { args = append(args, "--url", e.URL) } if e.EnrollmentToken != "" { args = append(args, "--enrollment-token", e.EnrollmentToken) } if e.ReplaceToken != "" { args = append(args, "--replace-token", e.ReplaceToken) } if len(e.CertificateAuthorities) > 0 { args = append(args, "--certificate-authorities="+strings.Join(e.CertificateAuthorities, ",")) } if e.Certificate != "" { args = append(args, "--elastic-agent-cert="+e.Certificate) } if e.Key != "" { args = append(args, "--elastic-agent-cert-key="+e.Key) } if e.KeyPassphrasePath != "" { args = append(args, "--elastic-agent-cert-key-passphrase="+e.KeyPassphrasePath) } return args } type FleetBootstrapOpts struct { ESHost string // --fleet-server-es ServiceToken string // --fleet-server-service-token Policy string // --fleet-server-policy Port int // --fleet-server-port } func (f FleetBootstrapOpts) toCmdArgs() []string { var args []string if f.ESHost != "" { args = append(args, "--fleet-server-es", f.ESHost) } if f.ServiceToken != "" { args = append(args, "--fleet-server-service-token", f.ServiceToken) } if f.Policy != "" { args = append(args, "--fleet-server-policy", f.Policy) } if f.Port > 0 { args = append(args, "--fleet-server-port", fmt.Sprintf("%d", f.Port)) } return args } // InstallOpts specifies the options for the install command type InstallOpts struct { BasePath string // --base-path Force bool // --force Insecure bool // --insecure NonInteractive bool // --non-interactive ProxyURL string // --proxy-url DelayEnroll bool // --delay-enroll Develop bool // --develop, not supported for DEB and RPM. Calling Install() sets Namespace to the development namespace so that checking only for a Namespace is sufficient. Namespace string // --namespace, not supported for DEB and RPM. InstallServers bool // --install-servers Privileged bool // inverse of --unprivileged (as false is the default) Username string Group string EnrollOpts FleetBootstrapOpts } func (i *InstallOpts) ToCmdArgs() []string { var args []string if i.BasePath != "" { args = append(args, "--base-path", i.BasePath) } if i.Force { args = append(args, "--force") } if i.Insecure { args = append(args, "--insecure") } if i.NonInteractive { args = append(args, "--non-interactive") } if i.ProxyURL != "" { args = append(args, "--proxy-url="+i.ProxyURL) } if i.DelayEnroll { args = append(args, "--delay-enroll") } if !i.Privileged { args = append(args, "--unprivileged") } if i.Namespace != "" { args = append(args, "--namespace="+i.Namespace) } if i.Develop { args = append(args, "--develop") if i.Namespace == "" { // If --namespace was used it will override the development namespace. i.Namespace = paths.DevelopmentNamespace } } if i.InstallServers { args = append(args, "--install-servers") } if i.Username != "" { args = append(args, "--user", i.Username) } if i.Group != "" { args = append(args, "--group", i.Group) } args = append(args, i.EnrollOpts.toCmdArgs()...) args = append(args, i.FleetBootstrapOpts.toCmdArgs()...) return args } // Install installs the prepared Elastic Agent binary and registers a t.Cleanup // function to uninstall the agent if it hasn't been uninstalled. It also takes // care of collecting a diagnostics when AGENT_COLLECT_DIAG=true or the test // has failed. // It returns: // - the combined output of Install command stdout and stderr // - an error if any. func (f *Fixture) Install(ctx context.Context, installOpts *InstallOpts, opts ...process.CmdOption) ([]byte, error) { return f.installFunc(ctx, installOpts, true, opts...) } func (f *Fixture) InstallWithoutEnroll(ctx context.Context, installOpts *InstallOpts, opts ...process.CmdOption) ([]byte, error) { return f.installFunc(ctx, installOpts, false, opts...) } func (f *Fixture) installFunc(ctx context.Context, installOpts *InstallOpts, shouldEnroll bool, opts ...process.CmdOption) ([]byte, error) { f.t.Logf("[test %s] Inside fixture install function", f.t.Name()) // check for running agents before installing, but only if not installed into a namespace whose point is allowing two agents at once. if installOpts != nil && !installOpts.Develop && installOpts.Namespace == "" { assert.Empty(f.t, getElasticAgentAndAgentbeatProcesses(f.t), "there should be no running agent at beginning of Install()") } switch f.packageFormat { case "targz", "zip": return f.installNoPkgManager(ctx, installOpts, shouldEnroll, opts) case "deb": return f.installDeb(ctx, installOpts, shouldEnroll, opts) case "rpm": return f.installRpm(ctx, installOpts, shouldEnroll, opts) default: return nil, fmt.Errorf("package format %s isn't supported yet", f.packageFormat) } } // installNoPkgManager installs the prepared Elastic Agent binary from // the tgz or zip archive and registers a t.Cleanup function to // uninstall the agent if it hasn't been uninstalled. It also takes // care of collecting a diagnostics when AGENT_COLLECT_DIAG=true or // the test has failed. // It returns: // - the combined output of Install command stdout and stderr // - an error if any. func (f *Fixture) installNoPkgManager(ctx context.Context, installOpts *InstallOpts, shouldEnroll bool, opts []process.CmdOption) ([]byte, error) { f.t.Logf("[test %s] Inside fixture installNoPkgManager function", f.t.Name()) if installOpts == nil { // default options when not provided installOpts = &InstallOpts{} } // Removes install params to prevent enrollment removeEnrollParams := func(installOpts *InstallOpts) { installOpts.URL = "" installOpts.EnrollmentToken = "" installOpts.ESHost = "" } installArgs := []string{"install"} if !shouldEnroll { removeEnrollParams(installOpts) } installArgs = append(installArgs, installOpts.ToCmdArgs()...) out, err := f.Exec(ctx, installArgs, opts...) if err != nil { f.DumpProcesses("-install") return out, fmt.Errorf("error running agent install command: %w", err) } f.installed = true f.installOpts = installOpts installDir := "Agent" socketRunSymlink := paths.ControlSocketRunSymlink("") if installOpts.Namespace != "" { installDir = paths.InstallDirNameForNamespace(installOpts.Namespace) socketRunSymlink = paths.ControlSocketRunSymlink(installOpts.Namespace) } if installOpts.BasePath == "" { f.workDir = filepath.Join(paths.DefaultBasePath, "Elastic", installDir) } else { f.workDir = filepath.Join(installOpts.BasePath, "Elastic", installDir) } // we just installed agent, the control socket is at a well-known location socketPath := fmt.Sprintf("unix://%s", socketRunSymlink) // use symlink as that works for all versions if runtime.GOOS == "windows" { // Windows uses a fixed named pipe, that is always the same. // It is the same even running in unprivileged mode. socketPath = paths.WindowsControlSocketInstalledPath } else if !installOpts.Privileged { // Unprivileged versions move the socket to inside the installed directory // of the Elastic Agent. socketPath = paths.ControlSocketFromPath(runtime.GOOS, f.workDir) } c := client.New(client.WithAddress(socketPath)) f.setClient(c) f.t.Cleanup(func() { if f.t.Failed() { f.DumpProcesses("-cleanup") } }) f.t.Cleanup(func() { // check for running agents after uninstall had a chance to run // there can be a single agent left when using --develop mode if f.installOpts != nil && f.installOpts.Namespace != "" { // Only consider the main agent process and not sub-processes so that we can detect when // multiple agents are running without needing to know the number of input sub-processes to expect. agentProcesses := getElasticAgentProcesses(f.t) assert.LessOrEqualf(f.t, len(agentProcesses), 1, "More than one agent left running at the end of the test when second agent in namespace %s was used: %v", f.installOpts.Namespace, agentProcesses) // The agent left running has to be the non-development agent. The development agent should be uninstalled first as a convention. if len(agentProcesses) > 0 { assert.NotContainsf(f.t, agentProcesses[0].Cmdline, paths.InstallDirNameForNamespace(f.installOpts.Namespace), "The agent installed into namespace %s was left running at the end of the test or was not uninstalled first: %v", f.installOpts.Namespace, agentProcesses) } return } // If not using an installation namespace, there should be no elastic-agent or agentbeat processes left running. processes := getElasticAgentAndAgentbeatProcesses(f.t) assert.Empty(f.t, processes, "there should be no running agent at the end of the test") }) f.t.Cleanup(func() { f.t.Logf("[test %s] Inside fixture cleanup function", f.t.Name()) if !f.installed { f.t.Logf("skipping uninstall; agent not installed (fixture.installed is false)") // not installed; no need to clean up or collect diagnostics return } // diagnostics is collected when either the environment variable // AGENT_COLLECT_DIAG=true or the test is marked failed collect := collectDiagFlag() failed := f.t.Failed() if collect || failed { if collect { f.t.Logf("collecting diagnostics; AGENT_COLLECT_DIAG=true") } else if failed { f.t.Logf("collecting diagnostics; test failed") } f.collectDiagnostics() } // environment variable AGENT_KEEP_INSTALLED=true will skip the uninstallation // useful to debug the issue with the Elastic Agent if f.t.Failed() && keepInstalledFlag() { f.t.Logf("skipping uninstall; test failed and AGENT_KEEP_INSTALLED=true") return } if keepInstalledFlag() { f.t.Logf("ignoring AGENT_KEEP_INSTALLED=true as test succeeded, " + "keeping the agent installed will jeopardise other tests") } // 5 minute timeout, to ensure that it at least doesn't get stuck. // original context is not used as it could have a timeout on the context // for the install and we don't want that context to prevent the uninstall uninstallCtx, uninstallCancel := context.WithTimeout(context.Background(), 5*time.Minute) defer uninstallCancel() out, err := f.Uninstall(uninstallCtx, &UninstallOpts{Force: true, UninstallToken: f.uninstallToken}) f.setClient(nil) if err != nil && (errors.Is(err, ErrNotInstalled) || strings.Contains( err.Error(), "elastic-agent: no such file or directory")) { f.t.Logf("fixture.Install Cleanup: agent was already uninstalled, skipping uninstall") // Agent fixture has already been uninstalled, perhaps by // an explicit call to fixture.Uninstall, so nothing needs // to be done here. return } require.NoErrorf(f.t, err, "uninstalling agent failed. Output: %q", out) }) return out, nil } type runningProcess struct { // Basic Process data Name string `json:"name,omitempty"` State agentsystemprocess.PidState `json:"state,omitempty"` Username string `json:"username,omitempty"` Pid int `json:"pid"` Ppid int `json:"ppid"` Pgid int `json:"pgid"` // Extended Process Data Args []string `json:"args,omitempty"` Cmdline string `json:"cmdline,omitempty"` Cwd string `json:"cwd,omitempty"` Exe string `json:"exe,omitempty"` Env mapstr.M `json:"env,omitempty"` } func (p runningProcess) String() string { return fmt.Sprintf("{PID:%v, PPID: %v, Cwd: %s, Exe: %s, Cmdline: %s, Args: %v}", p.Pid, p.Ppid, p.Cwd, p.Exe, p.Cmdline, p.Args) } func mapProcess(p agentsystemprocess.ProcState) runningProcess { mappedProcess := runningProcess{ Name: p.Name, State: p.State, Username: p.Username, // map pid/gid to int and default to an obvious impossible pid if we don't have a value Pid: p.Pid.ValueOr(-1), Ppid: p.Ppid.ValueOr(-1), Pgid: p.Pgid.ValueOr(-1), Cmdline: p.Cmdline, Cwd: p.Cwd, Exe: p.Exe, Args: make([]string, len(p.Args)), Env: make(mapstr.M), } copy(mappedProcess.Args, p.Args) for k, v := range p.Env { mappedProcess.Env[k] = v } return mappedProcess } func getElasticAgentProcesses(t *gotesting.T) []runningProcess { return getProcesses(t, `.*elastic\-agent.*`) } // Includes both the main elastic-agent process and the agentbeat sub-processes for ensuring // that no sub-processes are orphaned from their parent process and left running. This // primarily tests that Windows Job Object assignment works. func getElasticAgentAndAgentbeatProcesses(t *gotesting.T) []runningProcess { return getProcesses(t, `.*(elastic\-agent|agentbeat).*`) } func getProcesses(t *gotesting.T, regex string) []runningProcess { procStats := agentsystemprocess.Stats{ Procs: []string{regex}, } err := procStats.Init() if !assert.NoError(t, err, "error initializing process.Stats") { // we failed return nil } _, pids, err := procStats.FetchPids() if err != nil && assert.Truef(t, errors.Is(err, agentsystemprocess.NonFatalErr{}), "error fetching process information: %v", err) { // we failed a bit further return nil } processes := make([]runningProcess, 0, len(pids)) for _, p := range pids { processes = append(processes, mapProcess(p)) } return processes } // installDeb installs the prepared Elastic Agent binary from the deb // package and registers a t.Cleanup function to uninstall the agent if // it hasn't been uninstalled. It also takes care of collecting a // diagnostics when AGENT_COLLECT_DIAG=true or the test has failed. // It returns: // - the combined output of Install command stdout and stderr // - an error if any. func (f *Fixture) installDeb(ctx context.Context, installOpts *InstallOpts, shouldEnroll bool, opts []process.CmdOption) ([]byte, error) { f.t.Logf("[test %s] Inside fixture installDeb function", f.t.Name()) // Prepare so that the f.srcPackage string is populated err := f.EnsurePrepared(ctx) if err != nil { return nil, fmt.Errorf("failed to prepare: %w", err) } // sudo apt-get install the deb cmd := exec.CommandContext(ctx, "sudo", "-E", "apt-get", "install", "-y", f.srcPackage) // #nosec G204 -- Need to pass in name of package if installOpts.InstallServers { cmd.Env = append(cmd.Env, "ELASTIC_AGENT_FLAVOR=servers") } out, err := cmd.CombinedOutput() // #nosec G204 -- Need to pass in name of package if err != nil { return out, fmt.Errorf("apt install failed: %w output:%s", err, string(out)) } f.t.Cleanup(func() { f.t.Logf("[test %s] Inside fixture installDeb cleanup function", f.t.Name()) uninstallCtx, uninstallCancel := context.WithTimeout(context.Background(), 5*time.Minute) defer uninstallCancel() // stop elastic-agent, non fatal if error, might have been stopped before this. f.t.Logf("running 'sudo systemctl stop elastic-agent'") out, err := exec.CommandContext(uninstallCtx, "sudo", "systemctl", "stop", "elastic-agent").CombinedOutput() if err != nil { f.t.Logf("error systemctl stop elastic-agent: %s, output: %s", err, string(out)) } if keepInstalledFlag() { f.t.Logf("skipping uninstall; test failed and AGENT_KEEP_INSTALLED=true") return } // apt-get purge elastic-agent f.t.Logf("running 'sudo apt-get -y -q purge elastic-agent'") out, err = exec.CommandContext(uninstallCtx, "sudo", "apt-get", "-y", "-q", "purge", "elastic-agent").CombinedOutput() if err != nil { f.t.Logf("failed to apt-get purge elastic-agent: %s, output: %s", err, string(out)) f.t.FailNow() } }) // start elastic-agent out, err = exec.CommandContext(ctx, "sudo", "systemctl", "start", "elastic-agent").CombinedOutput() if err != nil { return out, fmt.Errorf("systemctl start elastic-agent failed: %w", err) } if !shouldEnroll { return nil, nil } // apt install doesn't enroll, so need to do that enrollArgs := []string{"elastic-agent", "enroll"} if installOpts.Force { enrollArgs = append(enrollArgs, "--force") } if installOpts.Insecure { enrollArgs = append(enrollArgs, "--insecure") } if installOpts.ProxyURL != "" { enrollArgs = append(enrollArgs, "--proxy-url="+installOpts.ProxyURL) } if installOpts.DelayEnroll { enrollArgs = append(enrollArgs, "--delay-enroll") } if installOpts.EnrollOpts.URL != "" { enrollArgs = append(enrollArgs, "--url", installOpts.EnrollOpts.URL) } if installOpts.EnrollOpts.EnrollmentToken != "" { enrollArgs = append(enrollArgs, "--enrollment-token", installOpts.EnrollOpts.EnrollmentToken) } out, err = exec.CommandContext(ctx, "sudo", enrollArgs...).CombinedOutput() if err != nil { return out, fmt.Errorf("elastic-agent enroll failed: %w, output: %s args: %v", err, string(out), enrollArgs) } return nil, nil } // installRpm installs the prepared Elastic Agent binary from the rpm // package and registers a t.Cleanup function to uninstall the agent if // it hasn't been uninstalled. It also takes care of collecting a // diagnostics when AGENT_COLLECT_DIAG=true or the test has failed. // It returns: // - the combined output of Install command stdout and stderr // - an error if any. func (f *Fixture) installRpm(ctx context.Context, installOpts *InstallOpts, shouldEnroll bool, opts []process.CmdOption) ([]byte, error) { f.t.Logf("[test %s] Inside fixture installRpm function", f.t.Name()) // Prepare so that the f.srcPackage string is populated err := f.EnsurePrepared(ctx) if err != nil { return nil, fmt.Errorf("failed to prepare: %w", err) } // sudo rpm -iv elastic-agent rpm cmd := exec.CommandContext(ctx, "sudo", "-E", "rpm", "-i", "-v", f.srcPackage) // #nosec G204 -- Need to pass in name of package if installOpts.InstallServers { cmd.Env = append(cmd.Env, "ELASTIC_AGENT_FLAVOR=servers") } out, err := cmd.CombinedOutput() if err != nil { return out, fmt.Errorf("rpm install failed: %w output:%s", err, string(out)) } f.t.Cleanup(func() { f.t.Logf("[test %s] Inside fixture installRpm cleanup function", f.t.Name()) uninstallCtx, uninstallCancel := context.WithTimeout(context.Background(), 5*time.Minute) defer uninstallCancel() // stop elastic-agent, non fatal if error, might have been stopped before this. f.t.Logf("running 'sudo systemctl stop elastic-agent'") out, err := exec.CommandContext(uninstallCtx, "sudo", "systemctl", "stop", "elastic-agent").CombinedOutput() if err != nil { f.t.Logf("error systemctl stop elastic-agent: %s, output: %s", err, string(out)) } // rpm -e elastic-agent rpm f.t.Logf("running 'sudo rpm -e elastic-agent'") out, err = exec.CommandContext(uninstallCtx, "sudo", "rpm", "-e", "elastic-agent").CombinedOutput() if err != nil { f.t.Logf("failed to 'sudo rpm -e elastic-agent': %s, output: %s", err, string(out)) f.t.FailNow() } f.t.Logf("removing installed agent files") out, err = exec.CommandContext(uninstallCtx, "sudo", "rm", "-rf", "/var/lib/elastic-agent", "/var/log/elastic-agent", "/etc/elastic-agent").CombinedOutput() if err != nil { f.t.Log(string(out)) f.t.Logf("failed to 'sudo rm -rf /var/lib/elastic-agent /var/log/elastic-agent/ /etc/elastic-agent'") f.t.FailNow() } }) // start elastic-agent out, err = exec.CommandContext(ctx, "sudo", "systemctl", "start", "elastic-agent").CombinedOutput() if err != nil { return out, fmt.Errorf("systemctl start elastic-agent failed: %w", err) } if !shouldEnroll { return nil, nil } // rpm install doesn't enroll, so need to do that enrollArgs := []string{"elastic-agent", "enroll"} if installOpts.Force { enrollArgs = append(enrollArgs, "--force") } if installOpts.Insecure { enrollArgs = append(enrollArgs, "--insecure") } if installOpts.ProxyURL != "" { enrollArgs = append(enrollArgs, "--proxy-url="+installOpts.ProxyURL) } if installOpts.DelayEnroll { enrollArgs = append(enrollArgs, "--delay-enroll") } if installOpts.EnrollOpts.URL != "" { enrollArgs = append(enrollArgs, "--url", installOpts.EnrollOpts.URL) } if installOpts.EnrollOpts.EnrollmentToken != "" { enrollArgs = append(enrollArgs, "--enrollment-token", installOpts.EnrollOpts.EnrollmentToken) } // run sudo elastic-agent enroll out, err = exec.CommandContext(ctx, "sudo", enrollArgs...).CombinedOutput() if err != nil { return out, fmt.Errorf("elastic-agent enroll failed: %w, output: %s args: %v", err, string(out), enrollArgs) } return nil, nil } type UninstallOpts struct { Force bool // --force UninstallToken string // --uninstall-token SkipFleetAudit bool // --skip-fleet-audit } func (i UninstallOpts) toCmdArgs() []string { var args []string if i.Force { args = append(args, "--force") } if i.UninstallToken != "" { args = append(args, "--uninstall-token", i.UninstallToken) } if i.SkipFleetAudit { args = append(args, "--skip-fleet-audit") } return args } // Uninstall uninstalls the installed Elastic Agent binary func (f *Fixture) Uninstall(ctx context.Context, uninstallOpts *UninstallOpts, opts ...process.CmdOption) ([]byte, error) { switch f.packageFormat { case "targz", "zip": return f.uninstallNoPkgManager(ctx, uninstallOpts, opts) case "deb": return f.uninstallDeb(ctx, uninstallOpts, opts) case "rpm": return f.uninstallRpm(ctx, uninstallOpts, opts) default: return nil, fmt.Errorf("uninstall of package format '%s' not supported yet", f.packageFormat) } } func (f *Fixture) uninstallDeb(ctx context.Context, uninstallOpts *UninstallOpts, opts []process.CmdOption) ([]byte, error) { // stop elastic-agent, non fatal if error, might have been stopped before this. out, err := exec.CommandContext(ctx, "sudo", "systemctl", "stop", "elastic-agent").CombinedOutput() if err != nil { f.t.Logf("error systemctl stop elastic-agent: %s, output: %s", err, string(out)) } out, err = exec.CommandContext(ctx, "sudo", "apt-get", "-y", "-q", "purge", "elastic-agent").CombinedOutput() if err != nil { return out, fmt.Errorf("error removing apt: %w", err) } return out, nil } func (f *Fixture) uninstallRpm(ctx context.Context, uninstallOpts *UninstallOpts, opts []process.CmdOption) ([]byte, error) { // stop elastic-agent, non fatal if error, might have been stopped before this. out, err := exec.CommandContext(ctx, "sudo", "systemctl", "stop", "elastic-agent").CombinedOutput() if err != nil { f.t.Logf("error systemctl stop elastic-agent: %s, output: %s", err, string(out)) } out, err = exec.CommandContext(ctx, "sudo", "rpm", "-e", "elastic-agent").CombinedOutput() if err != nil { return out, fmt.Errorf("error running 'sudo rpm -e elastic-agent': %w", err) } return out, nil } func (f *Fixture) uninstallNoPkgManager(ctx context.Context, uninstallOpts *UninstallOpts, opts []process.CmdOption) ([]byte, error) { if !f.installed { return nil, ErrNotInstalled } uninstallArgs := []string{"uninstall"} if uninstallOpts != nil { uninstallArgs = append(uninstallArgs, uninstallOpts.toCmdArgs()...) } out, err := f.Exec(ctx, uninstallArgs, opts...) if err != nil { return out, fmt.Errorf("error running uninstall command: %w", err) } f.installed = false f.workDir = f.extractDir // Check that Elastic Agent files are actually removed basePath := f.installOpts.BasePath if basePath == "" { basePath = paths.DefaultBasePath } topPath := filepath.Join(basePath, "Elastic", "Agent") topPathStats, err := os.Stat(topPath) if errors.Is(err, fs.ErrNotExist) { // the path does not exist anymore, all good! return out, nil } if err != nil { return out, fmt.Errorf("error stating agent path: %w", err) } if err != nil && topPathStats != nil { return out, fmt.Errorf("Elastic Agent is still installed at [%s]", topPath) //nolint:stylecheck // Elastic Agent is a proper noun } return out, nil } func (f *Fixture) collectDiagnostics() { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() diagPath, err := f.DiagnosticsDir() if err != nil { f.t.Logf("failed to collect diagnostics: %v", err) return } err = os.MkdirAll(diagPath, 0755) if err != nil { f.t.Logf("failed to collect diagnostics; failed to create %s: %s", diagPath, err) return } prefix := f.FileNamePrefix() outputPath := filepath.Join(diagPath, prefix+"-diagnostics.zip") output, err := f.Exec(ctx, []string{"diagnostics", "-f", outputPath}) if err != nil { f.t.Logf("failed to collect diagnostics to %s (%s): %s", outputPath, err, output) // possible that the test was so fast that the Elastic Agent was just installed, the control protocol is // not fully running yet. wait 15 seconds to try again, ensuring that best effort is performed in fetching // diagnostics if strings.Contains(string(output), "connection error") { f.t.Logf("retrying in 15 seconds due to connection error; possible Elastic Agent was not fully started") time.Sleep(15 * time.Second) output, err = f.Exec(ctx, []string{"diagnostics", "-f", outputPath}) if err != nil { f.t.Logf("failed to collect diagnostics a second time at %s (%s): %s", outputPath, err, output) } } if err != nil { // If collecting diagnostics fails, zip up the entire installation directory with the hope that it will contain logs. f.t.Logf("creating zip archive of the installation directory: %s", f.workDir) zipPath := filepath.Join(diagPath, fmt.Sprintf("%s-install-directory.zip", prefix)) err = f.archiveInstallDirectory(f.workDir, zipPath) if err != nil { f.t.Logf("failed to zip install directory to %s: %s", zipPath, err) } } } } func (f *Fixture) archiveInstallDirectory(installPath string, outputPath string) error { file, err := os.Create(outputPath) if err != nil { return fmt.Errorf("creating zip output file %s: %w", outputPath, err) } defer file.Close() w := zip.NewWriter(file) defer w.Close() walker := func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } file, err := os.Open(path) if err != nil { f.t.Logf("failed to add %s to zip, continuing: %s", path, err) return nil } defer file.Close() f, err := w.Create(path) if err != nil { return err } _, err = io.Copy(f, file) if err != nil { return err } return nil } err = filepath.WalkDir(f.workDir, walker) if err != nil { return fmt.Errorf("walking %s to create zip: %w", f.workDir, err) } return nil } func collectDiagFlag() bool { // failure reports false (ignore error) v, _ := strconv.ParseBool(os.Getenv("AGENT_COLLECT_DIAG")) return v } func keepInstalledFlag() bool { // failure reports false (ignore error) v, _ := strconv.ParseBool(os.Getenv("AGENT_KEEP_INSTALLED")) return v }