From fd14043ad78454f7ed6d67d63293f77c97d84d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20=C5=9Awi=C4=85tek?= Date: Fri, 1 Mar 2024 18:54:09 +0100 Subject: [PATCH] test: add basic Windows install script tests --- pkg/scripts_test/check.go | 2 - pkg/scripts_test/check_windows.go | 65 +++++++++ pkg/scripts_test/command_unix.go | 2 +- pkg/scripts_test/command_windows.go | 167 +++++++++++++++++++++++ pkg/scripts_test/common.go | 2 - pkg/scripts_test/common_windows.go | 89 ++++++++++++ pkg/scripts_test/config.go | 2 - pkg/scripts_test/consts_windows.go | 32 +++++ pkg/scripts_test/install_windows_test.go | 131 +++++++++++++++++- 9 files changed, 483 insertions(+), 9 deletions(-) create mode 100644 pkg/scripts_test/check_windows.go create mode 100644 pkg/scripts_test/command_windows.go create mode 100644 pkg/scripts_test/common_windows.go create mode 100644 pkg/scripts_test/consts_windows.go diff --git a/pkg/scripts_test/check.go b/pkg/scripts_test/check.go index b580ff8da1..44a1c12f67 100644 --- a/pkg/scripts_test/check.go +++ b/pkg/scripts_test/check.go @@ -1,5 +1,3 @@ -//go:build !windows - package sumologic_scripts_tests import ( diff --git a/pkg/scripts_test/check_windows.go b/pkg/scripts_test/check_windows.go new file mode 100644 index 0000000000..029de9cbad --- /dev/null +++ b/pkg/scripts_test/check_windows.go @@ -0,0 +1,65 @@ +package sumologic_scripts_tests + +import ( + "os/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func checkAbortedDueToNoToken(c check) { + require.Greater(c.test, len(c.output), 1) + require.Greater(c.test, len(c.errorOutput), 1) + require.Contains(c.test, c.errorOutput[0], "Installation token has not been provided.") + require.Contains(c.test, c.errorOutput[1], "Please set the SUMOLOGIC_INSTALLATION_TOKEN environment variable.") +} + +func checkEphemeralNotInConfig(p string) func(c check) { + return func(c check) { + assert.False(c.test, c.installOptions.ephemeral, "ephemeral was specified") + + conf, err := getConfig(p) + require.NoError(c.test, err, "error while reading configuration") + + assert.False(c.test, conf.Extensions.Sumologic.Ephemeral, "ephemeral is true") + } +} + +func checkEphemeralInConfig(p string) func(c check) { + return func(c check) { + assert.True(c.test, c.installOptions.ephemeral, "ephemeral was not specified") + + conf, err := getConfig(p) + require.NoError(c.test, err, "error while reading configuration") + + assert.True(c.test, conf.Extensions.Sumologic.Ephemeral, "ephemeral is not true") + } +} + +func checkTokenInConfig(c check) { + require.NotEmpty(c.test, c.installOptions.installToken, "installation token has not been provided") + + conf, err := getConfig(userConfigPath) + require.NoError(c.test, err, "error while reading configuration") + + require.Equal(c.test, c.installOptions.installToken, conf.Extensions.Sumologic.InstallationToken, "installation token is different than expected") +} + +func checkTokenInSumoConfig(c check) { + require.NotEmpty(c.test, c.installOptions.installToken, "installation token has not been provided") + + conf, err := getConfig(configPath) + require.NoError(c.test, err, "error while reading configuration") + + require.Equal(c.test, c.installOptions.installToken, conf.Extensions.Sumologic.InstallationToken, "installation token is different than expected") +} + +func checkUserExists(c check) { + _, err := user.Lookup(systemUser) + require.NoError(c.test, err, "user has not been created") +} + +func checkUserNotExists(c check) { + _, err := user.Lookup(systemUser) + require.Error(c.test, err, "user has been created") +} diff --git a/pkg/scripts_test/command_unix.go b/pkg/scripts_test/command_unix.go index ebaba6b629..38a3537c3b 100644 --- a/pkg/scripts_test/command_unix.go +++ b/pkg/scripts_test/command_unix.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build linux || darwin package sumologic_scripts_tests diff --git a/pkg/scripts_test/command_windows.go b/pkg/scripts_test/command_windows.go new file mode 100644 index 0000000000..47ad5b86cd --- /dev/null +++ b/pkg/scripts_test/command_windows.go @@ -0,0 +1,167 @@ +package sumologic_scripts_tests + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "strings" + + "github.com/stretchr/testify/require" +) + +type installOptions struct { + installToken string + tags map[string]string + fips bool + envs map[string]string + apiBaseURL string + installHostmetrics bool + remotelyManaged bool + ephemeral bool +} + +func (io *installOptions) string() []string { + opts := []string{ + "-Command", + scriptPath, + } + + if io.fips { + opts = append(opts, "-Fips", "1") + } + + if io.installHostmetrics { + opts = append(opts, "-InstallHostMetrics", "1") + } + + if io.remotelyManaged { + opts = append(opts, "-RemotelyManaged", "1") + } + + if io.ephemeral { + opts = append(opts, "-Ephemeral", "1") + } + + if len(io.tags) > 0 { + opts = append(opts, "-Tags", getTagOptValue(io.tags)) + } + + if io.apiBaseURL != "" { + opts = append(opts, "-Api", io.apiBaseURL) + } + + return opts +} + +func (io *installOptions) buildEnvs() []string { + e := os.Environ() + + for k, v := range io.envs { + e = append(e, fmt.Sprintf("%s=%s", k, v)) + } + + if io.installToken != "" { + e = append(e, fmt.Sprintf("%s=%s", installTokenEnv, io.installToken)) + } + + return e +} + +func exitCode(cmd *exec.Cmd) (int, error) { + err := cmd.Wait() + + if err == nil { + return cmd.ProcessState.ExitCode(), nil + } + + if exiterr, ok := err.(*exec.ExitError); ok { + return exiterr.ExitCode(), nil + } + + return 0, fmt.Errorf("cannot obtain exit code: %v", err) +} + +func runScript(ch check) (int, []string, []string, error) { + cmd := exec.Command("powershell", ch.installOptions.string()...) + cmd.Env = ch.installOptions.buildEnvs() + output := []string{} + + in, err := cmd.StdinPipe() + if err != nil { + require.NoError(ch.test, err) + } + + defer in.Close() + + out, err := cmd.StdoutPipe() + if err != nil { + require.NoError(ch.test, err) + } + defer out.Close() + + errOut, err := cmd.StderrPipe() + if err != nil { + require.NoError(ch.test, err) + } + defer errOut.Close() + + // We want to read line by line + bufOut := bufio.NewReader(out) + + // Start the process + if err = cmd.Start(); err != nil { + require.NoError(ch.test, err) + } + + // Read the results from the process + for { + line, _, err := bufOut.ReadLine() + strLine := strings.TrimSpace(string(line)) + + if len(strLine) > 0 { + output = append(output, strLine) + } + ch.test.Log(strLine) + + // exit if script finished + if err == io.EOF { + break + } + + // otherwise ensure there is no error + require.NoError(ch.test, err) + + } + + // Handle stderr separately + bufErrOut := bufio.NewReader(errOut) + errorOutput := []string{} + for { + line, _, err := bufErrOut.ReadLine() + strLine := strings.TrimSpace(string(line)) + + if len(strLine) > 0 { + errorOutput = append(errorOutput, strLine) + } + ch.test.Log(strLine) + + // exit if script finished + if err == io.EOF { + break + } + } + + code, err := exitCode(cmd) + return code, output, errorOutput, err +} + +func getTagOptValue(tags map[string]string) string { + tagOpts := []string{} + for k, v := range tags { + tagOpts = append(tagOpts, fmt.Sprintf("%s = \"%s\"", k, v)) + } + tagOptString := strings.Join(tagOpts, " ; ") + return fmt.Sprintf("@{ %s }", tagOptString) +} diff --git a/pkg/scripts_test/common.go b/pkg/scripts_test/common.go index e805ffd719..f82b3aa8cc 100644 --- a/pkg/scripts_test/common.go +++ b/pkg/scripts_test/common.go @@ -1,5 +1,3 @@ -//go:build !windows - package sumologic_scripts_tests type testSpec struct { diff --git a/pkg/scripts_test/common_windows.go b/pkg/scripts_test/common_windows.go new file mode 100644 index 0000000000..58a5c58545 --- /dev/null +++ b/pkg/scripts_test/common_windows.go @@ -0,0 +1,89 @@ +//go:build windows + +package sumologic_scripts_tests + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "os/exec" + "testing" + + "github.com/stretchr/testify/require" +) + +// These checks always have to be true after a script execution +var commonPostChecks = []checkFunc{checkNoBakFilesPresent} + +func runTest(t *testing.T, spec *testSpec) { + ch := check{ + test: t, + installOptions: spec.options, + expectedInstallCode: spec.installCode, + } + + t.Log("Running conditional checks") + for _, a := range spec.conditionalChecks { + if !a(ch) { + t.SkipNow() + } + } + + defer tearDown(t) + + t.Log("Starting HTTP server") + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, err := io.WriteString(w, "200 OK\n") + require.NoError(t, err) + }) + + listener, err := net.Listen("tcp", ":3333") + require.NoError(t, err) + + httpServer := &http.Server{ + Handler: mux, + } + go func() { + err := httpServer.Serve(listener) + if err != nil && err != http.ErrServerClosed { + require.NoError(t, err) + } + }() + defer func() { + require.NoError(t, httpServer.Shutdown(context.Background())) + }() + + t.Log("Running pre actions") + for _, a := range spec.preActions { + a(ch) + } + + t.Log("Running pre checks") + for _, c := range spec.preChecks { + c(ch) + } + + ch.code, ch.output, ch.errorOutput, ch.err = runScript(ch) + + checkRun(ch) + + t.Log("Running common post checks") + for _, c := range commonPostChecks { + c(ch) + } + + t.Log("Running post checks") + for _, c := range spec.postChecks { + c(ch) + } +} + +func tearDown(t *testing.T) { + cmd := exec.Command("powershell", "Uninstall-Package", "-Name", fmt.Sprintf(`"%s"`, packageName)) + if out, err := cmd.CombinedOutput(); err != nil { + t.Log(string(out)) + } +} diff --git a/pkg/scripts_test/config.go b/pkg/scripts_test/config.go index 04775084f5..eb2bf82faf 100644 --- a/pkg/scripts_test/config.go +++ b/pkg/scripts_test/config.go @@ -1,5 +1,3 @@ -//go:build !windows - package sumologic_scripts_tests import ( diff --git a/pkg/scripts_test/consts_windows.go b/pkg/scripts_test/consts_windows.go new file mode 100644 index 0000000000..2bd009c718 --- /dev/null +++ b/pkg/scripts_test/consts_windows.go @@ -0,0 +1,32 @@ +//go:build windows + +package sumologic_scripts_tests + +const ( + systemGroup string = "otelcol-sumo" + systemUser string = "otelcol-sumo" + + packageName string = "OpenTelemetry Collector" + + binaryPath string = `C:\Program Files\Sumo Logic\OpenTelemetry Collector\bin\otelcol-sumo.exe` + libPath string = `C:\ProgramData\Sumo Logic\OpenTelemetry Collector\data` + fileStoragePath string = libPath + `\file_storage` + etcPath string = `C:\ProgramData\Sumo Logic\OpenTelemetry Collector\config` + scriptPath string = "../../scripts/install.ps1" + configPath = etcPath + `\sumologic.yaml` + confDPath = etcPath + `\conf.d` + opampDPath = etcPath + `\opamp.d` + userConfigPath = confDPath + `\common.yaml` + hostmetricsConfigPath = confDPath + `\hostmetrics.yaml` + + installToken string = "token" + installTokenEnv string = "SUMOLOGIC_INSTALLATION_TOKEN" + apiBaseURL string = "https://open-collectors.sumologic.com" + + commonConfigPathFilePermissions uint32 = 0550 + configPathDirPermissions uint32 = 0550 + configPathFilePermissions uint32 = 0440 + confDPathFilePermissions uint32 = 0644 + etcPathPermissions uint32 = 0551 + opampDPermissions uint32 = 0750 +) diff --git a/pkg/scripts_test/install_windows_test.go b/pkg/scripts_test/install_windows_test.go index 4cef785d1e..6e9896f946 100644 --- a/pkg/scripts_test/install_windows_test.go +++ b/pkg/scripts_test/install_windows_test.go @@ -7,7 +7,134 @@ import ( "testing" ) +// TODO: Check file ownership +// TODO: Set up file permissions to be able to modify config files on Windows + func TestInstallScript(t *testing.T) { - // no-op for now to make tests pass - t.Skip("not implemented for windows yet") + for _, spec := range []testSpec{ + { + name: "no arguments", + options: installOptions{}, + preChecks: []checkFunc{checkBinaryNotCreated, checkConfigNotCreated, checkUserConfigNotCreated, checkUserNotExists}, + postChecks: []checkFunc{checkBinaryNotCreated, checkConfigNotCreated, checkUserConfigNotCreated, checkAbortedDueToNoToken, checkUserNotExists}, + installCode: 1, + }, + { + name: "installation token only", + options: installOptions{ + installToken: installToken, + }, + preChecks: []checkFunc{checkBinaryNotCreated, checkConfigNotCreated, checkUserConfigNotCreated, checkUserNotExists}, + postChecks: []checkFunc{ + checkBinaryCreated, + checkBinaryIsRunning, + checkConfigCreated, + // checkConfigFilesOwnershipAndPermissions(rootUser, rootGroup), + checkUserConfigCreated, + checkEphemeralNotInConfig(userConfigPath), + checkTokenInConfig, + checkUserNotExists, + checkHostmetricsConfigNotCreated, + }, + }, + { + name: "installation token and ephemeral", + options: installOptions{ + installToken: installToken, + ephemeral: true, + }, + preChecks: []checkFunc{checkBinaryNotCreated, checkConfigNotCreated, checkUserConfigNotCreated, checkUserNotExists}, + postChecks: []checkFunc{ + checkBinaryCreated, + checkBinaryIsRunning, + checkConfigCreated, + // checkConfigFilesOwnershipAndPermissions(rootUser, rootGroup), + checkUserConfigCreated, + checkTokenInConfig, + checkEphemeralInConfig(userConfigPath), + checkUserNotExists, + checkHostmetricsConfigNotCreated, + }, + }, + { + name: "installation token and hostmetrics", + options: installOptions{ + installToken: installToken, + installHostmetrics: true, + }, + preChecks: []checkFunc{checkBinaryNotCreated, checkConfigNotCreated, checkUserConfigNotCreated, checkUserNotExists}, + postChecks: []checkFunc{ + checkBinaryCreated, + checkBinaryIsRunning, + checkConfigCreated, + // checkConfigFilesOwnershipAndPermissions(rootUser, rootGroup), + checkUserConfigCreated, + checkTokenInConfig, + checkUserNotExists, + checkHostmetricsConfigCreated, + }, + }, + { + name: "installation token and remotely-managed", + options: installOptions{ + installToken: installToken, + remotelyManaged: true, + }, + preChecks: []checkFunc{checkBinaryNotCreated, checkConfigNotCreated, checkUserConfigNotCreated, checkUserNotExists}, + postChecks: []checkFunc{ + checkBinaryCreated, + checkBinaryIsRunning, + checkConfigCreated, + // checkConfigFilesOwnershipAndPermissions(rootUser, rootGroup), + checkRemoteConfigDirectoryCreated, + checkTokenInSumoConfig, + checkEphemeralNotInConfig(configPath), + checkUserNotExists, + }, + }, + { + name: "installation token, remotely-managed, and ephemeral", + options: installOptions{ + installToken: installToken, + remotelyManaged: true, + ephemeral: true, + }, + preChecks: []checkFunc{checkBinaryNotCreated, checkConfigNotCreated, checkUserConfigNotCreated, checkUserNotExists}, + postChecks: []checkFunc{ + checkBinaryCreated, + checkBinaryIsRunning, + checkConfigCreated, + // checkConfigFilesOwnershipAndPermissions(rootUser, rootGroup), + checkRemoteConfigDirectoryCreated, + checkTokenInSumoConfig, + checkEphemeralInConfig(configPath), + checkUserNotExists, + }, + }, + { + name: "configuration with tags", + options: installOptions{ + installToken: installToken, + tags: map[string]string{ + "lorem": "ipsum", + "foo": "bar", + "escape_me": "'\\/", + "slash": "a/b", + "numeric": "1_024", + }, + }, + preChecks: []checkFunc{checkBinaryNotCreated, checkConfigNotCreated, checkUserConfigNotCreated, checkUserNotExists}, + postChecks: []checkFunc{ + checkBinaryCreated, + checkBinaryIsRunning, + checkConfigCreated, + // checkConfigFilesOwnershipAndPermissions(rootUser, rootGroup), + checkTags, + }, + }, + } { + t.Run(spec.name, func(t *testing.T) { + runTest(t, &spec) + }) + } }