From e1e3d8277332bfcd8a2f23f338a7c1dba54d2a23 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 3 Feb 2020 10:16:23 +0100 Subject: [PATCH 01/23] Create exectuor interface and LocalExecutor for interchangable executors --- pkg/runtime/executor.go | 5 ++ pkg/runtime/local_executor.go | 57 ++++++++++++++++ ..._test.go => local_executor_darwin_test.go} | 6 +- ...x_test.go => local_executor_linux_test.go} | 6 +- pkg/runtime/local_executor_test.go | 66 +++++++++++++++++++ ...test.go => local_executor_windows_test.go} | 6 +- pkg/runtime/runtime.go | 52 +-------------- pkg/runtime/runtime_test.go | 57 ---------------- pkg/runtime/ssh_executor.go | 8 +++ 9 files changed, 150 insertions(+), 113 deletions(-) create mode 100644 pkg/runtime/executor.go create mode 100644 pkg/runtime/local_executor.go rename pkg/runtime/{runtime_darwin_test.go => local_executor_darwin_test.go} (90%) rename pkg/runtime/{runtime_linux_test.go => local_executor_linux_test.go} (90%) create mode 100644 pkg/runtime/local_executor_test.go rename pkg/runtime/{runtime_windows_test.go => local_executor_windows_test.go} (90%) create mode 100644 pkg/runtime/ssh_executor.go diff --git a/pkg/runtime/executor.go b/pkg/runtime/executor.go new file mode 100644 index 00000000..3abe5932 --- /dev/null +++ b/pkg/runtime/executor.go @@ -0,0 +1,5 @@ +package runtime + +type Executor interface { + Execute(test TestCase) TestResult +} diff --git a/pkg/runtime/local_executor.go b/pkg/runtime/local_executor.go new file mode 100644 index 00000000..373e55a6 --- /dev/null +++ b/pkg/runtime/local_executor.go @@ -0,0 +1,57 @@ +package runtime + +import ( + "github.com/SimonBaeumer/cmd" + "log" + "strings" +) + +type LocalExecutor struct { +} + +func (e LocalExecutor) Execute(test TestCase) TestResult { + timeoutOpt, err := createTimeoutOption(test.Command.Timeout) + if err != nil { + test.Result = CommandResult{Error: err} + return TestResult{ + TestCase: test, + } + } + + envOpt := createEnvVarsOption(test) + + // cut = command under test + cut := cmd.NewCommand( + test.Command.Cmd, + cmd.WithWorkingDir(test.Command.Dir), + timeoutOpt, + envOpt) + + if err := cut.Execute(); err != nil { + log.Println(test.Title, " failed ", err.Error()) + test.Result = CommandResult{ + Error: err, + } + + return TestResult{ + TestCase: test, + } + } + + log.Println("title: '"+test.Title+"'", " Command: ", test.Command.Cmd) + log.Println("title: '"+test.Title+"'", " Directory: ", cut.Dir) + log.Println("title: '"+test.Title+"'", " Env: ", cut.Env) + + // Write test result + test.Result = CommandResult{ + ExitCode: cut.ExitCode(), + Stdout: strings.TrimSpace(strings.Replace(cut.Stdout(), "\r\n", "\n", -1)), + Stderr: strings.TrimSpace(strings.Replace(cut.Stderr(), "\r\n", "\n", -1)), + } + + log.Println("title: '"+test.Title+"'", " ExitCode: ", test.Result.ExitCode) + log.Println("title: '"+test.Title+"'", " Stdout: ", test.Result.Stdout) + log.Println("title: '"+test.Title+"'", " Stderr: ", test.Result.Stderr) + + return Validate(test) +} diff --git a/pkg/runtime/runtime_darwin_test.go b/pkg/runtime/local_executor_darwin_test.go similarity index 90% rename from pkg/runtime/runtime_darwin_test.go rename to pkg/runtime/local_executor_darwin_test.go index 7e4b2cdd..7a839c2d 100644 --- a/pkg/runtime/runtime_darwin_test.go +++ b/pkg/runtime/local_executor_darwin_test.go @@ -17,7 +17,8 @@ func TestRuntime_WithInheritFromShell(t *testing.T) { }, } - got := runTest(test) + e := LocalExecutor{} + got := e.Execute(test) assert.Equal(t, "test", got.TestCase.Result.Stdout) } @@ -38,7 +39,8 @@ func TestRuntime_WithInheritFromShell_Overwrite(t *testing.T) { }, } - got := runTest(test) + e := LocalExecutor{} + got := e.Execute(test) assert.Equal(t, "overwrite from-parent", got.TestCase.Result.Stdout) } diff --git a/pkg/runtime/runtime_linux_test.go b/pkg/runtime/local_executor_linux_test.go similarity index 90% rename from pkg/runtime/runtime_linux_test.go rename to pkg/runtime/local_executor_linux_test.go index 7e4b2cdd..7a839c2d 100644 --- a/pkg/runtime/runtime_linux_test.go +++ b/pkg/runtime/local_executor_linux_test.go @@ -17,7 +17,8 @@ func TestRuntime_WithInheritFromShell(t *testing.T) { }, } - got := runTest(test) + e := LocalExecutor{} + got := e.Execute(test) assert.Equal(t, "test", got.TestCase.Result.Stdout) } @@ -38,7 +39,8 @@ func TestRuntime_WithInheritFromShell_Overwrite(t *testing.T) { }, } - got := runTest(test) + e := LocalExecutor{} + got := e.Execute(test) assert.Equal(t, "overwrite from-parent", got.TestCase.Result.Stdout) } diff --git a/pkg/runtime/local_executor_test.go b/pkg/runtime/local_executor_test.go new file mode 100644 index 00000000..dbd9b3ce --- /dev/null +++ b/pkg/runtime/local_executor_test.go @@ -0,0 +1,66 @@ +package runtime + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "runtime" + "testing" +) + +func TestRuntime_WithEnvVariables(t *testing.T) { + envVar := "$KEY" + if runtime.GOOS == "windows" { + envVar = "%KEY%" + } + + s := TestCase{ + Command: CommandUnderTest{ + Cmd: fmt.Sprintf("echo %s", envVar), + Timeout: "2s", + Env: map[string]string{"KEY": "value"}, + }, + Expected: Expected{ + Stdout: ExpectedOut{ + Contains: []string{"value"}, + }, + ExitCode: 0, + }, + Title: "Output env variable", + } + + e := LocalExecutor{} + got := e.Execute(s) + assert.True(t, got.ValidationResult.Success) +} + +func Test_runTestShouldReturnError(t *testing.T) { + test := TestCase{ + Command: CommandUnderTest{ + Cmd: "pwd", + Dir: "/home/invalid", + }, + } + + e := LocalExecutor{} + got := e.Execute(test) + + if runtime.GOOS == "windows" { + assert.Contains(t, got.TestCase.Result.Error.Error(), "chdir /home/invalid") + } else { + assert.Equal(t, "chdir /home/invalid: no such file or directory", got.TestCase.Result.Error.Error()) + } +} + +func TestRuntime_WithInvalidDuration(t *testing.T) { + test := TestCase{ + Command: CommandUnderTest{ + Cmd: "echo test", + Timeout: "600lightyears", + }, + } + + e := LocalExecutor{} + got := e.Execute(test) + + assert.Equal(t, "time: unknown unit lightyears in duration 600lightyears", got.TestCase.Result.Error.Error()) +} diff --git a/pkg/runtime/runtime_windows_test.go b/pkg/runtime/local_executor_windows_test.go similarity index 90% rename from pkg/runtime/runtime_windows_test.go rename to pkg/runtime/local_executor_windows_test.go index 0293e549..580d3c0a 100644 --- a/pkg/runtime/runtime_windows_test.go +++ b/pkg/runtime/local_executor_windows_test.go @@ -17,7 +17,8 @@ func TestRuntime_WithInheritFromShell(t *testing.T) { }, } - got := runTest(test) + e := LocalExecutor{} + got := e.Execute(test) assert.Equal(t, "test", got.TestCase.Result.Stdout) } @@ -38,7 +39,8 @@ func TestRuntime_WithInheritFromShell_Overwrite(t *testing.T) { }, } - got := runTest(test) + e := LocalExecutor{} + got := e.Execute(test) assert.Equal(t, "overwrite from-parent", got.TestCase.Result.Stdout) } diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index fd389dda..e8d25f62 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -3,7 +3,6 @@ package runtime import ( "fmt" "github.com/SimonBaeumer/cmd" - "log" "os" "runtime" "strings" @@ -126,7 +125,8 @@ func Start(tests []TestCase, maxConcurrent int) <-chan TestResult { for t := range tests { result := TestResult{} for i := 1; i <= t.Command.GetRetries(); i++ { - result = runTest(t) + e := LocalExecutor{} + result = e.Execute(t) result.Tries = i if result.ValidationResult.Success { break @@ -158,54 +158,6 @@ func executeRetryInterval(t TestCase) { } } -// runTest executes the current test case -func runTest(test TestCase) TestResult { - timeoutOpt, err := createTimeoutOption(test.Command.Timeout) - if err != nil { - test.Result = CommandResult{Error: err} - return TestResult{ - TestCase: test, - } - } - - envOpt := createEnvVarsOption(test) - - // cut = command under test - cut := cmd.NewCommand( - test.Command.Cmd, - cmd.WithWorkingDir(test.Command.Dir), - timeoutOpt, - envOpt) - - if err := cut.Execute(); err != nil { - log.Println(test.Title, " failed ", err.Error()) - test.Result = CommandResult{ - Error: err, - } - - return TestResult{ - TestCase: test, - } - } - - log.Println("title: '"+test.Title+"'", " Command: ", test.Command.Cmd) - log.Println("title: '"+test.Title+"'", " Directory: ", cut.Dir) - log.Println("title: '"+test.Title+"'", " Env: ", cut.Env) - - // Write test result - test.Result = CommandResult{ - ExitCode: cut.ExitCode(), - Stdout: strings.TrimSpace(strings.Replace(cut.Stdout(), "\r\n", "\n", -1)), - Stderr: strings.TrimSpace(strings.Replace(cut.Stderr(), "\r\n", "\n", -1)), - } - - log.Println("title: '"+test.Title+"'", " ExitCode: ", test.Result.ExitCode) - log.Println("title: '"+test.Title+"'", " Stdout: ", test.Result.Stdout) - log.Println("title: '"+test.Title+"'", " Stderr: ", test.Result.Stderr) - - return Validate(test) -} - func createEnvVarsOption(test TestCase) func(c *cmd.Command) { return func(c *cmd.Command) { // Add all env variables from parent process diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 34b27af6..03a78545 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -1,9 +1,7 @@ package runtime import ( - "fmt" "github.com/stretchr/testify/assert" - "runtime" "testing" "time" ) @@ -63,61 +61,6 @@ func TestRuntime_WithRetriesAndInterval(t *testing.T) { assert.True(t, duration.Seconds() > 0.15, "Retry interval did not work") } -func TestRuntime_WithEnvVariables(t *testing.T) { - envVar := "$KEY" - if runtime.GOOS == "windows" { - envVar = "%KEY%" - } - - s := TestCase{ - Command: CommandUnderTest{ - Cmd: fmt.Sprintf("echo %s", envVar), - Timeout: "2s", - Env: map[string]string{"KEY": "value"}, - }, - Expected: Expected{ - Stdout: ExpectedOut{ - Contains: []string{"value"}, - }, - ExitCode: 0, - }, - Title: "Output env variable", - } - - got := runTest(s) - assert.True(t, got.ValidationResult.Success) -} - -func Test_runTestShouldReturnError(t *testing.T) { - test := TestCase{ - Command: CommandUnderTest{ - Cmd: "pwd", - Dir: "/home/invalid", - }, - } - - got := runTest(test) - - if runtime.GOOS == "windows" { - assert.Contains(t, got.TestCase.Result.Error.Error(), "chdir /home/invalid") - } else { - assert.Equal(t, "chdir /home/invalid: no such file or directory", got.TestCase.Result.Error.Error()) - } -} - -func TestRuntime_WithInvalidDuration(t *testing.T) { - test := TestCase{ - Command: CommandUnderTest{ - Cmd: "echo test", - Timeout: "600lightyears", - }, - } - - got := runTest(test) - - assert.Equal(t, "time: unknown unit lightyears in duration 600lightyears", got.TestCase.Result.Error.Error()) -} - func getExampleTestSuite() []TestCase { tests := []TestCase{ { diff --git a/pkg/runtime/ssh_executor.go b/pkg/runtime/ssh_executor.go new file mode 100644 index 00000000..ad0da88c --- /dev/null +++ b/pkg/runtime/ssh_executor.go @@ -0,0 +1,8 @@ +package runtime + +type SSHExecutor struct { + Host string + User string + Password string + PrivateKey string +} From 967f783edfafd53dabcf89dcc042f7c99cdc94b2 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 3 Feb 2020 11:16:30 +0100 Subject: [PATCH 02/23] Add SSH executor --- pkg/runtime/ssh_executor.go | 93 ++++++++++++++++++++++++++++++-- pkg/runtime/ssh_executor_test.go | 79 +++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 pkg/runtime/ssh_executor_test.go diff --git a/pkg/runtime/ssh_executor.go b/pkg/runtime/ssh_executor.go index ad0da88c..328b5835 100644 --- a/pkg/runtime/ssh_executor.go +++ b/pkg/runtime/ssh_executor.go @@ -1,8 +1,93 @@ package runtime +import ( + "bytes" + "fmt" + "golang.org/x/crypto/ssh" + "log" + "net" + "strings" +) + +// SSHExecutor type SSHExecutor struct { - Host string - User string - Password string - PrivateKey string + Host string + User string + Password string +} + +// Execute executes a command on a remote host viá SSH +func (e SSHExecutor) Execute(test TestCase) TestResult { + if test.Command.InheritEnv { + log.Fatal("Inhereit env is not supported viá SSH") + } + + sshConf := &ssh.ClientConfig{ + User: e.User, + Auth: []ssh.AuthMethod{ + ssh.Password(e.Password), + }, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + return nil + }, + } + + conn, err := ssh.Dial("tcp", e.Host, sshConf) + if err != nil { + log.Fatal(err) + } + + session, err := conn.NewSession() + defer session.Close() + if err != nil { + log.Fatal(err) + } + + var stdoutBuffer bytes.Buffer + var stderrBuffer bytes.Buffer + session.Stdout = &stdoutBuffer + session.Stderr = &stderrBuffer + + for k, v := range test.Command.Env { + err := session.Setenv(k, v) + if err != nil { + log.Fatal(fmt.Sprintf("Failed, maybe ssh server is configured to only accept LC_ prefixed env variables. Error: %s", err)) + } + } + + dirCmd := "" + if test.Command.Dir != "" { + dirCmd = fmt.Sprintf("cd %s; ", test.Command.Dir) + } + + exitCode := 0 + err = session.Run(fmt.Sprintf("%s %s", dirCmd, test.Command.Cmd)) + switch err.(type) { + case *ssh.ExitError: + ee, _ := err.(*ssh.ExitError) + exitCode = ee.Waitmsg.ExitStatus() + case nil: + break + default: + log.Println(test.Title, " failed ", err.Error()) + test.Result = CommandResult{ + Error: err, + } + + return TestResult{ + TestCase: test, + } + } + + test.Result = CommandResult{ + ExitCode: exitCode, + Stdout: strings.TrimSpace(strings.Replace(stdoutBuffer.String(), "\r\n", "\n", -1)), + Stderr: strings.TrimSpace(strings.Replace(stderrBuffer.String(), "\r\n", "\n", -1)), + } + + log.Println("title: '"+test.Title+"'", " ExitCode: ", test.Result.ExitCode) + log.Println("title: '"+test.Title+"'", " Stdout: ", test.Result.Stdout) + log.Println("title: '"+test.Title+"'", " Stderr: ", test.Result.Stderr) + + return Validate(test) } diff --git a/pkg/runtime/ssh_executor_test.go b/pkg/runtime/ssh_executor_test.go new file mode 100644 index 00000000..e92d6f37 --- /dev/null +++ b/pkg/runtime/ssh_executor_test.go @@ -0,0 +1,79 @@ +package runtime + +import ( + "github.com/stretchr/testify/assert" + "log" + "os" + "testing" +) + +func TestMain(m *testing.M) { + v := os.Getenv("COMMANDER_SSH_TEST") + if v != "1" { + log.Println("Skip ssh_executor_test, set env COMMANDER_SSH_TEST to 1") + return + } + + m.Run() +} + +func createExecutor() SSHExecutor { + s := SSHExecutor{ + Host: "localhost:2222", + User: "vagrant", + Password: "vagrant", + } + return s +} + +func Test_SSHExecutor(t *testing.T) { + s := createExecutor() + + test := TestCase{ + Command: CommandUnderTest{ + Cmd: "echo test", + }, + Expected: Expected{ + ExitCode: 0, + Stdout: ExpectedOut{Exactly: "test"}, + }, + } + got := s.Execute(test) + + assert.True(t, got.ValidationResult.Success) + assert.Equal(t, "test", got.TestCase.Result.Stdout) +} + +func Test_SSHExecutor_WithDir(t *testing.T) { + s := createExecutor() + + test := TestCase{ + Command: CommandUnderTest{ + Cmd: "echo $LC_TEST_KEY1; echo $LC_TEST_KEY2", + Env: map[string]string{ + "LC_TEST_KEY1": "ENV_VALUE1", + "LC_TEST_KEY2": "ENV_VALUE2", + }, + }, + } + got := s.Execute(test) + + assert.True(t, got.ValidationResult.Success) + assert.Equal(t, "ENV_VALUE1\nENV_VALUE2", got.TestCase.Result.Stdout) + assert.Equal(t, 0, got.TestCase.Result.ExitCode) +} + +func Test_SSHExecutor_ExitCode(t *testing.T) { + s := createExecutor() + + test := TestCase{ + Command: CommandUnderTest{ + Cmd: "exit 2;", + }, + } + + got := s.Execute(test) + + assert.False(t, got.ValidationResult.Success) + assert.Equal(t, 2, got.TestCase.Result.ExitCode) +} From f549814d823b82a2a80b192ecbe2cbdc6dc30e70 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 3 Feb 2020 17:02:21 +0100 Subject: [PATCH 03/23] Add yaml configuration --- README.md | 11 +++++++++++ pkg/runtime/runtime.go | 2 ++ pkg/suite/yaml_suite.go | 23 +++++++++++++++++++++++ pkg/suite/yaml_suite_test.go | 29 +++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/README.md b/README.md index 80ac9270..cf7dee8d 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,17 @@ tests: timeout: 1s # Overwrite timeout retries: 5 exit-code: 0 + + it targets another host: + ssh: + publickey: /home/user/.ssh/id_rsa.pub + user: commander + password: commander + docker: + image: alpine:latest + name: alpine_instance_1 + kubernetes: + ``` ### Executing diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index e8d25f62..a2226303 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -46,6 +46,7 @@ type TestConfig struct { Retries int Interval string InheritEnv bool + SSH SSHExecutor } // ResultStatus represents the status code of a test result @@ -89,6 +90,7 @@ type CommandUnderTest struct { Timeout string Retries int Interval string + SSH SSHExecutor } // TestResult represents the TestCase and the ValidationResult diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go index 480f8b38..f9adb409 100644 --- a/pkg/suite/yaml_suite.go +++ b/pkg/suite/yaml_suite.go @@ -22,6 +22,14 @@ type YAMLTestConfig struct { Timeout string `yaml:"timeout,omitempty"` Retries int `yaml:"retries,omitempty"` Interval string `yaml:"interval,omitempty"` + SSH SSHConf `yaml:"ssh,omitempty"` +} + +// SSHConf represents the target host of the system +type SSHConf struct { + Host string `yaml:"host"` + User string `yaml:"user"` + Password string `yaml:"password,omitempty"` } // YAMLTest represents a test in the yaml test suite @@ -78,6 +86,11 @@ func ParseYAML(content []byte) Suite { Timeout: yamlConfig.Config.Timeout, Retries: yamlConfig.Config.Retries, Interval: yamlConfig.Config.Interval, + SSH: runtime.SSHExecutor{ + User: yamlConfig.Config.SSH.User, + Password: yamlConfig.Config.SSH.Password, + Host: yamlConfig.Config.SSH.Host, + }, }, } } @@ -96,6 +109,11 @@ func convertYAMLConfToTestCases(conf YAMLConfig) []runtime.TestCase { Timeout: t.Config.Timeout, Retries: t.Config.Retries, Interval: t.Config.Interval, + SSH: runtime.SSHExecutor{ + User: t.Config.SSH.User, + Password: t.Config.SSH.Password, + Host: t.Config.SSH.Host, + }, }, Expected: runtime.Expected{ ExitCode: t.ExitCode, @@ -153,6 +171,7 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { Timeout: params.Config.Timeout, Retries: params.Config.Retries, Interval: params.Config.Interval, + SSH: params.Config.SSH, } return nil @@ -267,6 +286,10 @@ func (y *YAMLConfig) mergeConfigs(local YAMLTestConfig, global YAMLTestConfig) Y conf.InheritEnv = local.InheritEnv } + if local.SSH.Host != "" { + conf.SSH = local.SSH + } + return conf } diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go index 18f6ad35..e881afd8 100644 --- a/pkg/suite/yaml_suite_test.go +++ b/pkg/suite/yaml_suite_test.go @@ -321,3 +321,32 @@ func Test_convertExpectedOut_ReturnFullStruct(t *testing.T) { assert.Equal(t, out, r) } + +func TestYAMLSuite_should_parse_ssh(t *testing.T) { + yaml := []byte(` +config: + ssh: + host: localhost + user: commander + password: 12345! + +tests: + echo hello: + exit-code: 0 + config: + ssh: + host: 192.168.0.1 + user: root + password: admin +`) + + got := ParseYAML(yaml) + assert.Equal(t, "192.168.0.1", got.GetTests()[0].Command.SSH.Host) + assert.Equal(t, "root", got.GetTests()[0].Command.SSH.User) + assert.Equal(t, "admin", got.GetTests()[0].Command.SSH.Password) + + assert.Equal(t, "localhost", got.GetGlobalConfig().SSH.Host) + assert.Equal(t, "commander", got.GetGlobalConfig().SSH.User) + assert.Equal(t, "12345!", got.GetGlobalConfig().SSH.Password) + +} From 3f58d46f90e1cb615dcf66a12a4c674c67876e72 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Feb 2020 11:02:54 +0100 Subject: [PATCH 04/23] Remove YAMLTestSuite with Suite --- README.md | 29 ++++++++------ pkg/runtime/runtime.go | 2 +- pkg/suite/suite.go | 44 ++++++++++++++++++--- pkg/suite/suite_test.go | 1 + pkg/suite/yaml_suite.go | 74 ++++++++++++------------------------ pkg/suite/yaml_suite_test.go | 44 +++++++++------------ 6 files changed, 103 insertions(+), 91 deletions(-) create mode 100644 pkg/suite/suite_test.go diff --git a/README.md b/README.md index cf7dee8d..2f40ea33 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,24 @@ Count: 1, Failed: 0 Here you can see an example with all features for a quick reference ```yaml +nodes: + ssh-host1: + type: ssh + addr: 192.168.0.1:22 + user: root + pass: pass + ssh-host2: + type: ssh + addr: 192.168.0.1:22 + user: root + public-key: /home/user/id_rsa.pub + docker-host1: + type: docker + image: alpine:2.4 + docker-host2: + type: docker + instance: alpine_instance_1 + config: # Config for all executed tests dir: /tmp #Set working directory env: # Environment variables @@ -153,17 +171,6 @@ tests: timeout: 1s # Overwrite timeout retries: 5 exit-code: 0 - - it targets another host: - ssh: - publickey: /home/user/.ssh/id_rsa.pub - user: commander - password: commander - docker: - image: alpine:latest - name: alpine_instance_1 - kubernetes: - ``` ### Executing diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index a2226303..4dec2be3 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -90,7 +90,7 @@ type CommandUnderTest struct { Timeout string Retries int Interval string - SSH SSHExecutor + Executors []Executor } // TestResult represents the TestCase and the ValidationResult diff --git a/pkg/suite/suite.go b/pkg/suite/suite.go index 344ee05b..9e586574 100644 --- a/pkg/suite/suite.go +++ b/pkg/suite/suite.go @@ -1,10 +1,44 @@ package suite -import "github.com/SimonBaeumer/commander/pkg/runtime" +import ( + "fmt" + "github.com/SimonBaeumer/commander/pkg/runtime" +) // Suite represents the current tests and configs -type Suite interface { - GetTests() []runtime.TestCase - GetTestByTitle(title string) (runtime.TestCase, error) - GetGlobalConfig() runtime.TestConfig +type Suite struct { + TestCases []runtime.TestCase + Config runtime.TestConfig +} + +func NewSuite(config runtime.TestConfig, tests ...runtime.TestCase) *Suite { + return &Suite{ + TestCases: tests, + Config: config, + } +} + +func (s Suite) AddTest(t runtime.TestCase) { + if _, err := s.GetTestByTitle(t.Title); err != nil { + panic(fmt.Sprintf("Tests %s was already added to the suite", t.Title)) + } + s.TestCases = append(s.TestCases, t) +} + +// GetTests returns all tests of the test suite +func (s Suite) GetTests() []runtime.TestCase { + return s.TestCases +} + +func (s Suite) GetTestByTitle(title string) (runtime.TestCase, error) { + for _, t := range s.GetTests() { + if t.Title == title { + return t, nil + } + } + return runtime.TestCase{}, fmt.Errorf("Could not find test " + title) +} + +func (s Suite) GetGlobalConfig() runtime.TestConfig { + return s.Config } diff --git a/pkg/suite/suite_test.go b/pkg/suite/suite_test.go new file mode 100644 index 00000000..792b0335 --- /dev/null +++ b/pkg/suite/suite_test.go @@ -0,0 +1 @@ +package suite diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go index f9adb409..282d51de 100644 --- a/pkg/suite/yaml_suite.go +++ b/pkg/suite/yaml_suite.go @@ -12,6 +12,7 @@ import ( type YAMLConfig struct { Tests map[string]YAMLTest `yaml:"tests"` Config YAMLTestConfig `yaml:"config,omitempty"` + Nodes map[string]NodeConf `yaml:"nodes,omitempty"` } // YAMLTestConfig is a struct to represent the test config @@ -22,7 +23,21 @@ type YAMLTestConfig struct { Timeout string `yaml:"timeout,omitempty"` Retries int `yaml:"retries,omitempty"` Interval string `yaml:"interval,omitempty"` - SSH SSHConf `yaml:"ssh,omitempty"` +} + +type NodeConf struct { + Name string `yaml:"-"` + Type string `yaml:"type"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + Addr string `yaml:"addr,omitempty"` + //SSH []SSHConf `yaml:"ssh,omitempty"` + //Docker []DockerConf `yaml:"docker,omitempty"` +} + +type DockerConf struct { + Image string `yaml:"image"` + Name string `yaml:"name"` } // SSHConf represents the target host of the system @@ -34,38 +49,13 @@ type SSHConf struct { // YAMLTest represents a test in the yaml test suite type YAMLTest struct { - Title string `yaml:"-"` - Command string `yaml:"command,omitempty"` - ExitCode int `yaml:"exit-code"` - Stdout interface{} `yaml:"stdout,omitempty"` - Stderr interface{} `yaml:"stderr,omitempty"` - Config YAMLTestConfig `yaml:"config,omitempty"` -} - -//YAMLSuite represents a test suite which was configured in yaml -type YAMLSuite struct { - TestCases []runtime.TestCase - Config runtime.TestConfig -} - -// GetTests returns all tests of the test suite -func (s YAMLSuite) GetTests() []runtime.TestCase { - return s.TestCases -} - -//GetTestByTitle returns the first test it finds for the given title -func (s YAMLSuite) GetTestByTitle(title string) (runtime.TestCase, error) { - for _, t := range s.GetTests() { - if t.Title == title { - return t, nil - } - } - return runtime.TestCase{}, fmt.Errorf("Could not find test " + title) -} - -//GetGlobalConfig returns the global suite configuration -func (s YAMLSuite) GetGlobalConfig() runtime.TestConfig { - return s.Config + Title string `yaml:"-"` + Command string `yaml:"command,omitempty"` + ExitCode int `yaml:"exit-code"` + Stdout interface{} `yaml:"stdout,omitempty"` + Stderr interface{} `yaml:"stderr,omitempty"` + Config YAMLTestConfig `yaml:"config,omitempty"` + Nodes map[string]NodeConf `yaml:"nodes,omitempty"` } // ParseYAML parses the Suite from a yaml byte slice @@ -77,7 +67,7 @@ func ParseYAML(content []byte) Suite { panic(err.Error()) } - return YAMLSuite{ + return Suite{ TestCases: convertYAMLConfToTestCases(yamlConfig), Config: runtime.TestConfig{ InheritEnv: yamlConfig.Config.InheritEnv, @@ -86,11 +76,6 @@ func ParseYAML(content []byte) Suite { Timeout: yamlConfig.Config.Timeout, Retries: yamlConfig.Config.Retries, Interval: yamlConfig.Config.Interval, - SSH: runtime.SSHExecutor{ - User: yamlConfig.Config.SSH.User, - Password: yamlConfig.Config.SSH.Password, - Host: yamlConfig.Config.SSH.Host, - }, }, } } @@ -109,11 +94,6 @@ func convertYAMLConfToTestCases(conf YAMLConfig) []runtime.TestCase { Timeout: t.Config.Timeout, Retries: t.Config.Retries, Interval: t.Config.Interval, - SSH: runtime.SSHExecutor{ - User: t.Config.SSH.User, - Password: t.Config.SSH.Password, - Host: t.Config.SSH.Host, - }, }, Expected: runtime.Expected{ ExitCode: t.ExitCode, @@ -136,6 +116,7 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { var params struct { Tests map[string]YAMLTest `yaml:"tests"` Config YAMLTestConfig `yaml:"config"` + Nodes map[string]NodeConf `yaml:"nodes"` } err := unmarshal(¶ms) @@ -171,7 +152,6 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { Timeout: params.Config.Timeout, Retries: params.Config.Retries, Interval: params.Config.Interval, - SSH: params.Config.SSH, } return nil @@ -286,10 +266,6 @@ func (y *YAMLConfig) mergeConfigs(local YAMLTestConfig, global YAMLTestConfig) Y conf.InheritEnv = local.InheritEnv } - if local.SSH.Host != "" { - conf.SSH = local.SSH - } - return conf } diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go index e881afd8..af8fdcc4 100644 --- a/pkg/suite/yaml_suite_test.go +++ b/pkg/suite/yaml_suite_test.go @@ -323,30 +323,24 @@ func Test_convertExpectedOut_ReturnFullStruct(t *testing.T) { } func TestYAMLSuite_should_parse_ssh(t *testing.T) { - yaml := []byte(` -config: - ssh: - host: localhost - user: commander - password: 12345! - -tests: - echo hello: - exit-code: 0 - config: - ssh: - host: 192.168.0.1 - user: root - password: admin -`) - - got := ParseYAML(yaml) - assert.Equal(t, "192.168.0.1", got.GetTests()[0].Command.SSH.Host) - assert.Equal(t, "root", got.GetTests()[0].Command.SSH.User) - assert.Equal(t, "admin", got.GetTests()[0].Command.SSH.Password) - - assert.Equal(t, "localhost", got.GetGlobalConfig().SSH.Host) - assert.Equal(t, "commander", got.GetGlobalConfig().SSH.User) - assert.Equal(t, "12345!", got.GetGlobalConfig().SSH.Password) + // yaml := []byte(` + //nodes: + // ssh-host1: + // addr: localhost + // user: commander + // pass: 12345! + //tests: + // echo hello: + // exit-code: 0 + //`) + + //got := ParseYAML(yaml) + //assert.Equal(t, "192.168.0.1", got.GetTests()[0].Command.SSH.Host) + //assert.Equal(t, "root", got.GetTests()[0].Command.SSH.User) + //assert.Equal(t, "admin", got.GetTests()[0].Command.SSH.Password) + // + //assert.Equal(t, "localhost", got.GetGlobalConfig().SSH.Host) + //assert.Equal(t, "commander", got.GetGlobalConfig().SSH.User) + //assert.Equal(t, "12345!", got.GetGlobalConfig().SSH.Password) } From e68939cf3e13a91f79e652304220b9568b190c65 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Feb 2020 11:20:19 +0100 Subject: [PATCH 05/23] Add node parsing --- pkg/suite/suite.go | 13 +++++++++++++ pkg/suite/yaml_suite.go | 30 +++++++++++++++++++++++++++-- pkg/suite/yaml_suite_test.go | 37 ++++++++++++++++++------------------ 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/pkg/suite/suite.go b/pkg/suite/suite.go index 9e586574..4c1a781f 100644 --- a/pkg/suite/suite.go +++ b/pkg/suite/suite.go @@ -5,10 +5,19 @@ import ( "github.com/SimonBaeumer/commander/pkg/runtime" ) +type Node struct { + Name string + Type string + User string + Pass string + Addr string +} + // Suite represents the current tests and configs type Suite struct { TestCases []runtime.TestCase Config runtime.TestConfig + Nodes []Node } func NewSuite(config runtime.TestConfig, tests ...runtime.TestCase) *Suite { @@ -18,6 +27,10 @@ func NewSuite(config runtime.TestConfig, tests ...runtime.TestCase) *Suite { } } +func (s Suite) GetNodes() []Node { + return s.Nodes +} + func (s Suite) AddTest(t runtime.TestCase) { if _, err := s.GetTestByTitle(t.Title); err != nil { panic(fmt.Sprintf("Tests %s was already added to the suite", t.Title)) diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go index 282d51de..e3ba593b 100644 --- a/pkg/suite/yaml_suite.go +++ b/pkg/suite/yaml_suite.go @@ -31,8 +31,6 @@ type NodeConf struct { User string `yaml:"user"` Pass string `yaml:"pass"` Addr string `yaml:"addr,omitempty"` - //SSH []SSHConf `yaml:"ssh,omitempty"` - //Docker []DockerConf `yaml:"docker,omitempty"` } type DockerConf struct { @@ -77,9 +75,24 @@ func ParseYAML(content []byte) Suite { Retries: yamlConfig.Config.Retries, Interval: yamlConfig.Config.Interval, }, + Nodes: convertNodes(yamlConfig.Nodes), } } +func convertNodes(nodes map[string]NodeConf) []Node { + var n []Node + for _, v := range nodes { + n = append(n, Node{ + Pass: v.Pass, + Type: v.Type, + User: v.User, + Addr: v.Addr, + Name: v.Name, + }) + } + return n +} + //Convert YAMlConfig to runtime TestCases func convertYAMLConfToTestCases(conf YAMLConfig) []runtime.TestCase { var tests []runtime.TestCase @@ -144,6 +157,19 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { y.Tests[k] = test } + y.Nodes = make(map[string]NodeConf) + for k, v := range params.Nodes { + node := NodeConf{ + Name: k, + Addr: v.Addr, + User: v.User, + Type: v.Type, + Pass: v.Pass, + } + + y.Nodes[k] = node + } + //Parse global configuration y.Config = YAMLTestConfig{ InheritEnv: params.Config.InheritEnv, diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go index af8fdcc4..e0a5cb45 100644 --- a/pkg/suite/yaml_suite_test.go +++ b/pkg/suite/yaml_suite_test.go @@ -323,24 +323,23 @@ func Test_convertExpectedOut_ReturnFullStruct(t *testing.T) { } func TestYAMLSuite_should_parse_ssh(t *testing.T) { - // yaml := []byte(` - //nodes: - // ssh-host1: - // addr: localhost - // user: commander - // pass: 12345! - //tests: - // echo hello: - // exit-code: 0 - //`) - - //got := ParseYAML(yaml) - //assert.Equal(t, "192.168.0.1", got.GetTests()[0].Command.SSH.Host) - //assert.Equal(t, "root", got.GetTests()[0].Command.SSH.User) - //assert.Equal(t, "admin", got.GetTests()[0].Command.SSH.Password) - // - //assert.Equal(t, "localhost", got.GetGlobalConfig().SSH.Host) - //assert.Equal(t, "commander", got.GetGlobalConfig().SSH.User) - //assert.Equal(t, "12345!", got.GetGlobalConfig().SSH.Password) + yaml := []byte(` +nodes: + ssh-host1: + type: ssh + addr: localhost + user: root + pass: 12345! +tests: + echo hello: + exit-code: 0 +`) + got := ParseYAML(yaml) + assert.Len(t, got.GetNodes(), 1) + assert.Equal(t, "ssh-host1", got.GetNodes()[0].Name) + assert.Equal(t, "localhost", got.GetNodes()[0].Addr) + assert.Equal(t, "root", got.GetNodes()[0].User) + assert.Equal(t, "12345!", got.GetNodes()[0].Pass) + assert.Equal(t, "ssh", got.GetNodes()[0].Type) } From f1c25b017045c61990c93154088cb092d63f93a7 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Feb 2020 11:38:43 +0100 Subject: [PATCH 06/23] Parse docker host and add local nodes support --- pkg/runtime/runtime.go | 10 ++++++++++ pkg/suite/suite.go | 12 ++---------- pkg/suite/yaml_suite.go | 24 ++++++++++++++++-------- pkg/suite/yaml_suite_test.go | 7 +++++++ 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 4dec2be3..4d7072da 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -36,6 +36,16 @@ type TestCase struct { Command CommandUnderTest Expected Expected Result CommandResult + Nodes []Node +} + +type Node struct { + Name string + Type string + User string + Pass string + Addr string + Image string } //TestConfig represents the configuration for a test diff --git a/pkg/suite/suite.go b/pkg/suite/suite.go index 4c1a781f..870a0e27 100644 --- a/pkg/suite/suite.go +++ b/pkg/suite/suite.go @@ -5,19 +5,11 @@ import ( "github.com/SimonBaeumer/commander/pkg/runtime" ) -type Node struct { - Name string - Type string - User string - Pass string - Addr string -} - // Suite represents the current tests and configs type Suite struct { TestCases []runtime.TestCase Config runtime.TestConfig - Nodes []Node + Nodes []runtime.Node } func NewSuite(config runtime.TestConfig, tests ...runtime.TestCase) *Suite { @@ -27,7 +19,7 @@ func NewSuite(config runtime.TestConfig, tests ...runtime.TestCase) *Suite { } } -func (s Suite) GetNodes() []Node { +func (s Suite) GetNodes() []runtime.Node { return s.Nodes } diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go index e3ba593b..f3443244 100644 --- a/pkg/suite/yaml_suite.go +++ b/pkg/suite/yaml_suite.go @@ -26,11 +26,12 @@ type YAMLTestConfig struct { } type NodeConf struct { - Name string `yaml:"-"` - Type string `yaml:"type"` - User string `yaml:"user"` - Pass string `yaml:"pass"` - Addr string `yaml:"addr,omitempty"` + Name string `yaml:"-"` + Type string `yaml:"type"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + Addr string `yaml:"addr,omitempty"` + Image string `yaml:"image,omitempty"` } type DockerConf struct { @@ -79,15 +80,16 @@ func ParseYAML(content []byte) Suite { } } -func convertNodes(nodes map[string]NodeConf) []Node { - var n []Node +func convertNodes(nodes map[string]NodeConf) []runtime.Node { + var n []runtime.Node for _, v := range nodes { - n = append(n, Node{ + n = append(n, runtime.Node{ Pass: v.Pass, Type: v.Type, User: v.User, Addr: v.Addr, Name: v.Name, + Image: v.Image, }) } return n @@ -113,6 +115,7 @@ func convertYAMLConfToTestCases(conf YAMLConfig) []runtime.TestCase { Stdout: t.Stdout.(runtime.ExpectedOut), Stderr: t.Stderr.(runtime.ExpectedOut), }, + Nodes: convertNodes(t.Nodes), }) } @@ -147,6 +150,7 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { Stdout: y.convertToExpectedOut(v.Stdout), Stderr: y.convertToExpectedOut(v.Stderr), Config: y.mergeConfigs(v.Config, params.Config), + Nodes: y.mergeNodes(v.Nodes, params.Nodes), } // Set key as command, if command property was empty @@ -328,6 +332,10 @@ func (y YAMLConfig) MarshalYAML() (interface{}, error) { return y, nil } +func (y *YAMLConfig) mergeNodes(nodes map[string]NodeConf, globalNodes map[string]NodeConf) map[string]NodeConf { + return nodes +} + func convertExpectedOut(out runtime.ExpectedOut) interface{} { //If the property contains consists of only one element it will be set without the struct structure if isContainsASingleNonEmptyString(out) && propertiesAreEmpty(out) { diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go index e0a5cb45..8c4710fe 100644 --- a/pkg/suite/yaml_suite_test.go +++ b/pkg/suite/yaml_suite_test.go @@ -332,6 +332,10 @@ nodes: pass: 12345! tests: echo hello: + nodes: + docker-host: + type: docker + image: "ubuntu:18.04" exit-code: 0 `) @@ -342,4 +346,7 @@ tests: assert.Equal(t, "root", got.GetNodes()[0].User) assert.Equal(t, "12345!", got.GetNodes()[0].Pass) assert.Equal(t, "ssh", got.GetNodes()[0].Type) + + assert.Equal(t, "docker", got.GetTests()[0].Nodes[0].Type) + assert.Equal(t, "ubuntu:18.04", got.GetTests()[0].Nodes[0].Image) } From b479816d5498388e4a209404afbc2d1d1094226e Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Feb 2020 11:42:48 +0100 Subject: [PATCH 07/23] Add node list to tests --- pkg/runtime/runtime.go | 12 ++++++------ pkg/suite/yaml_suite.go | 30 +++++++++++++++--------------- pkg/suite/yaml_suite_test.go | 14 ++++++++------ 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 4d7072da..c41ce5a9 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -36,15 +36,15 @@ type TestCase struct { Command CommandUnderTest Expected Expected Result CommandResult - Nodes []Node + Nodes []string } type Node struct { - Name string - Type string - User string - Pass string - Addr string + Name string + Type string + User string + Pass string + Addr string Image string } diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go index f3443244..b1ad5976 100644 --- a/pkg/suite/yaml_suite.go +++ b/pkg/suite/yaml_suite.go @@ -48,13 +48,13 @@ type SSHConf struct { // YAMLTest represents a test in the yaml test suite type YAMLTest struct { - Title string `yaml:"-"` - Command string `yaml:"command,omitempty"` - ExitCode int `yaml:"exit-code"` - Stdout interface{} `yaml:"stdout,omitempty"` - Stderr interface{} `yaml:"stderr,omitempty"` - Config YAMLTestConfig `yaml:"config,omitempty"` - Nodes map[string]NodeConf `yaml:"nodes,omitempty"` + Title string `yaml:"-"` + Command string `yaml:"command,omitempty"` + ExitCode int `yaml:"exit-code"` + Stdout interface{} `yaml:"stdout,omitempty"` + Stderr interface{} `yaml:"stderr,omitempty"` + Config YAMLTestConfig `yaml:"config,omitempty"` + Nodes []string `yaml:"nodes,omitempty"` } // ParseYAML parses the Suite from a yaml byte slice @@ -84,11 +84,11 @@ func convertNodes(nodes map[string]NodeConf) []runtime.Node { var n []runtime.Node for _, v := range nodes { n = append(n, runtime.Node{ - Pass: v.Pass, - Type: v.Type, - User: v.User, - Addr: v.Addr, - Name: v.Name, + Pass: v.Pass, + Type: v.Type, + User: v.User, + Addr: v.Addr, + Name: v.Name, Image: v.Image, }) } @@ -115,7 +115,7 @@ func convertYAMLConfToTestCases(conf YAMLConfig) []runtime.TestCase { Stdout: t.Stdout.(runtime.ExpectedOut), Stderr: t.Stderr.(runtime.ExpectedOut), }, - Nodes: convertNodes(t.Nodes), + Nodes: t.Nodes, }) } @@ -150,7 +150,7 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { Stdout: y.convertToExpectedOut(v.Stdout), Stderr: y.convertToExpectedOut(v.Stderr), Config: y.mergeConfigs(v.Config, params.Config), - Nodes: y.mergeNodes(v.Nodes, params.Nodes), + Nodes: v.Nodes, } // Set key as command, if command property was empty @@ -333,7 +333,7 @@ func (y YAMLConfig) MarshalYAML() (interface{}, error) { } func (y *YAMLConfig) mergeNodes(nodes map[string]NodeConf, globalNodes map[string]NodeConf) map[string]NodeConf { - return nodes + return nodes } func convertExpectedOut(out runtime.ExpectedOut) interface{} { diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go index 8c4710fe..042760f2 100644 --- a/pkg/suite/yaml_suite_test.go +++ b/pkg/suite/yaml_suite_test.go @@ -330,23 +330,25 @@ nodes: addr: localhost user: root pass: 12345! + docker-host: + type: docker + image: ubuntu:18.04 tests: echo hello: nodes: - docker-host: - type: docker - image: "ubuntu:18.04" + - docker-host + - ssh-host1 exit-code: 0 `) got := ParseYAML(yaml) - assert.Len(t, got.GetNodes(), 1) + assert.Len(t, got.GetNodes(), 2) assert.Equal(t, "ssh-host1", got.GetNodes()[0].Name) assert.Equal(t, "localhost", got.GetNodes()[0].Addr) assert.Equal(t, "root", got.GetNodes()[0].User) assert.Equal(t, "12345!", got.GetNodes()[0].Pass) assert.Equal(t, "ssh", got.GetNodes()[0].Type) - assert.Equal(t, "docker", got.GetTests()[0].Nodes[0].Type) - assert.Equal(t, "ubuntu:18.04", got.GetTests()[0].Nodes[0].Image) + assert.Contains(t, got.GetTests()[0].Nodes, "docker-host") + assert.Contains(t, got.GetTests()[0].Nodes, "ssh-host1") } From e39c16032edd33704cb261f1f9cd5f1904008059 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Feb 2020 11:47:53 +0100 Subject: [PATCH 08/23] Add docker_executor.go --- pkg/runtime/docker_executor.go | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 pkg/runtime/docker_executor.go diff --git a/pkg/runtime/docker_executor.go b/pkg/runtime/docker_executor.go new file mode 100644 index 00000000..78534f5c --- /dev/null +++ b/pkg/runtime/docker_executor.go @@ -0,0 +1,9 @@ +package runtime + +type DockerExecutor struct { + Image string +} + +func (e DockerExecutor) Execute(test TestCase) TestResult { + return TestResult{} +} From c80d852789940e477bfa9cbf50688e4601c4461d Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Feb 2020 14:32:33 +0100 Subject: [PATCH 09/23] Refactor cli output and remove worker count --- pkg/output/cli.go | 12 ++++++--- pkg/output/cli_test.go | 4 +-- pkg/runtime/runtime.go | 50 ++++++++++++++++++++++++++---------- pkg/suite/suite.go | 11 +++++++- pkg/suite/yaml_suite_test.go | 16 +++++++----- 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/pkg/output/cli.go b/pkg/output/cli.go index 1cbf7592..8a25ad0c 100644 --- a/pkg/output/cli.go +++ b/pkg/output/cli.go @@ -42,13 +42,15 @@ func (w *OutputWriter) Start(results <-chan runtime.TestResult) bool { for r := range results { testResults = append(testResults, r) if r.ValidationResult.Success { - s := w.addTries("✓ "+r.TestCase.Title, r) + str := fmt.Sprintf("✓ %s", r.TestCase.Title) + s := w.addTries(str, r) w.fprintf(s) } if !r.ValidationResult.Success { failed++ - s := w.addTries("✗ "+r.TestCase.Title, r) + str := fmt.Sprintf("✗ %s", r.TestCase.Title) + s := w.addTries(str, r) w.fprintf(au.Red(s)) } } @@ -85,13 +87,15 @@ func (w *OutputWriter) printFailures(results []runtime.TestResult) { for _, r := range results { if r.TestCase.Result.Error != nil { - w.fprintf(au.Bold(au.Red("✗ '" + r.TestCase.Title + "' could not be executed with error message:"))) + str := fmt.Sprintf("✗ '%s' could not be executed with error message:", r.TestCase.Title) + w.fprintf(au.Bold(au.Red(str))) w.fprintf(r.TestCase.Result.Error.Error()) continue } if !r.ValidationResult.Success { - w.fprintf(au.Bold(au.Red("✗ '" + r.TestCase.Title + "', on property '" + r.FailedProperty + "'"))) + str := fmt.Sprintf("✗ '%s', on property '%s'", r.TestCase.Title, r.FailedProperty) + w.fprintf(au.Bold(au.Red(str))) w.fprintf(r.ValidationResult.Diff) } } diff --git a/pkg/output/cli_test.go b/pkg/output/cli_test.go index 3f0c74fd..7625162c 100644 --- a/pkg/output/cli_test.go +++ b/pkg/output/cli_test.go @@ -82,8 +82,8 @@ func Test_Start(t *testing.T) { output := buf.String() assert.True(t, strings.Contains(output, "✓ Successful test")) assert.True(t, strings.Contains(output, "✗ Failed test")) - assert.True(t, strings.Contains(output, "✗ 'Invalid command' could not be executed")) - assert.True(t, strings.Contains(output, "✗ 'Failed test on stderr', on property 'Stderr'")) + assert.Contains(t, output, "✗ 'Invalid command' could not be executed") + assert.Contains(t, output, "✗ 'Failed test on stderr', on property 'Stderr") assert.True(t, strings.Contains(output, "Some error message")) } diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index c41ce5a9..5b23e5f7 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/SimonBaeumer/cmd" "os" - "runtime" "strings" "sync" "time" @@ -109,6 +108,7 @@ type TestResult struct { ValidationResult ValidationResult FailedProperty string Tries int + Node string } // Start starts the given test suite and executes all tests @@ -124,33 +124,38 @@ func Start(tests []TestCase, maxConcurrent int) <-chan TestResult { } }(tests) - workerCount := maxConcurrent - if maxConcurrent == 0 { - workerCount = runtime.NumCPU() * WorkerCountMultiplicator - } - var wg sync.WaitGroup - for i := 0; i < workerCount; i++ { - wg.Add(1) - go func(tests chan TestCase) { - defer wg.Done() - for t := range tests { + wg.Add(1) + + go func(tests chan TestCase) { + defer wg.Done() + + for t := range tests { + // If no node was set use local mode + if len(t.Nodes) == 0 { + t.Nodes = []string{"local"} + } + + for _, n := range t.Nodes { result := TestResult{} for i := 1; i <= t.Command.GetRetries(); i++ { + e := LocalExecutor{} result = e.Execute(t) + result.Node = n result.Tries = i + if result.ValidationResult.Success { break } executeRetryInterval(t) } - out <- result } - }(in) - } + + } + }(in) go func(results chan TestResult) { wg.Wait() @@ -160,6 +165,23 @@ func Start(tests []TestCase, maxConcurrent int) <-chan TestResult { return out } +func execTest(t TestCase) TestResult { + result := TestResult{} + for i := 1; i <= t.Command.GetRetries(); i++ { + + e := LocalExecutor{} + result = e.Execute(t) + + result.Tries = i + if result.ValidationResult.Success { + break + } + + executeRetryInterval(t) + } + return result +} + func executeRetryInterval(t TestCase) { if t.Command.GetRetries() > 1 && t.Command.Interval != "" { interval, err := time.ParseDuration(t.Command.Interval) diff --git a/pkg/suite/suite.go b/pkg/suite/suite.go index 870a0e27..6d20b795 100644 --- a/pkg/suite/suite.go +++ b/pkg/suite/suite.go @@ -23,6 +23,15 @@ func (s Suite) GetNodes() []runtime.Node { return s.Nodes } +func (s Suite) GetNodeByName(name string) (runtime.Node, error) { + for _, n := range s.Nodes { + if n.Name == name { + return n, nil + } + } + return runtime.Node{}, fmt.Errorf("could not find node with name %s", name) +} + func (s Suite) AddTest(t runtime.TestCase) { if _, err := s.GetTestByTitle(t.Title); err != nil { panic(fmt.Sprintf("Tests %s was already added to the suite", t.Title)) @@ -41,7 +50,7 @@ func (s Suite) GetTestByTitle(title string) (runtime.TestCase, error) { return t, nil } } - return runtime.TestCase{}, fmt.Errorf("Could not find test " + title) + return runtime.TestCase{}, fmt.Errorf("could not find test %s", title) } func (s Suite) GetGlobalConfig() runtime.TestConfig { diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go index 042760f2..235df587 100644 --- a/pkg/suite/yaml_suite_test.go +++ b/pkg/suite/yaml_suite_test.go @@ -157,7 +157,7 @@ tests: `) _, err := ParseYAML(yaml).GetTestByTitle("does not exist") - assert.Equal(t, "Could not find test does not exist", err.Error()) + assert.Equal(t, "could not find test does not exist", err.Error()) } func TestYAMLSuite_ShouldParseGlobalConfig(t *testing.T) { @@ -342,12 +342,16 @@ tests: `) got := ParseYAML(yaml) + assert.Len(t, got.GetNodes(), 2) - assert.Equal(t, "ssh-host1", got.GetNodes()[0].Name) - assert.Equal(t, "localhost", got.GetNodes()[0].Addr) - assert.Equal(t, "root", got.GetNodes()[0].User) - assert.Equal(t, "12345!", got.GetNodes()[0].Pass) - assert.Equal(t, "ssh", got.GetNodes()[0].Type) + + node, err := got.GetNodeByName("ssh-host1") + assert.Nil(t, err) + assert.Equal(t, "ssh-host1", node.Name) + assert.Equal(t, "localhost", node.Addr) + assert.Equal(t, "root", node.User) + assert.Equal(t, "12345!", node.Pass) + assert.Equal(t, "ssh", node.Type) assert.Contains(t, got.GetTests()[0].Nodes, "docker-host") assert.Contains(t, got.GetTests()[0].Nodes, "ssh-host1") From 7e49e7f21f96996ccae71577425ac39e815b4c55 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Feb 2020 14:39:17 +0100 Subject: [PATCH 10/23] Add host to test logs --- commander_unix.yaml | 20 ++++++++++---------- commander_windows.yaml | 16 ++++++++-------- pkg/output/cli.go | 8 ++++---- pkg/output/cli_test.go | 17 +++++++++++------ 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/commander_unix.yaml b/commander_unix.yaml index d70c188a..9c980197 100644 --- a/commander_unix.yaml +++ b/commander_unix.yaml @@ -14,7 +14,7 @@ tests: command: ./commander test ./integration/unix/commander_test.yaml stdout: contains: - - ✓ it should exit with error code + - ✓ [local] it should exit with error code line-count: 16 exit-code: 0 @@ -22,8 +22,8 @@ tests: command: ./commander test ./integration/unix/failing_suite.yaml stdout: contains: - - ✗ 'it will fail', on property 'ExitCode' - - ✗ 'test timeout' could not be executed with error message + - ✗ [local] 'it will fail', on property 'ExitCode' + - ✗ [local] 'test timeout' could not be executed with error message - Command timed out after 10ms - "Count: 2, Failed: 2" exit-code: 1 @@ -32,7 +32,7 @@ tests: command: ./commander test ./integration/unix/test_big_output.yaml stdout: contains: - - ✓ cat ./integration/unix/_fixtures/big_out.txt + - ✓ [local] cat ./integration/unix/_fixtures/big_out.txt - "Count: 1, Failed: 0" exit-code: 0 @@ -43,9 +43,9 @@ tests: COMMANDER_FROM_SHELL: from_shell stdout: contains: - - ✓ should print global env value - - ✓ should print local env value - - ✓ should print env var from shell + - ✓ [local] should print global env value + - ✓ [local] should print local env value + - ✓ [local] should print env var from shell exit-code: 0 test add command: @@ -61,7 +61,7 @@ tests: command: ./commander test integration/unix/retries.yaml stdout: contains: - - ✗ echo hello, retries 3 - - ✓ it should retry failed commands, retries 2 - - ✗ it should retry failed commands with an interval, retries 2 + - ✗ [local] echo hello, retries 3 + - ✓ [local] it should retry failed commands, retries 2 + - ✗ [local] it should retry failed commands with an interval, retries 2 exit-code: 1 \ No newline at end of file diff --git a/commander_windows.yaml b/commander_windows.yaml index c0683190..7d274b2e 100644 --- a/commander_windows.yaml +++ b/commander_windows.yaml @@ -21,8 +21,8 @@ tests: command: commander.exe test ./integration/windows/failing_suite.yaml stdout: contains: - - ✗ 'it will fail', on property 'ExitCode' - - ✗ 'test timeout' could not be executed with error message + - ✗ [local] 'it will fail', on property 'ExitCode' + - ✗ [local] 'test timeout' could not be executed with error message - "Count: 2, Failed: 2" exit-code: 1 @@ -30,7 +30,7 @@ tests: command: commander.exe test ./integration/windows/test_big_output.yaml stdout: contains: - - ✓ type integration\windows\_fixtures\big_out.txt + - ✓ [local] type integration\windows\_fixtures\big_out.txt - "Count: 1, Failed: 0" exit-code: 0 @@ -41,10 +41,10 @@ tests: COMMANDER_FROM_SHELL: from_shell stdout: contains: - - ✓ should print global - - ✓ should print local - - ✓ should execute in given dir - - ✓ should work with timeout + - ✓ [local] should print global + - ✓ [local] should print local + - ✓ [local] should execute in given dir + - ✓ [local] should work with timeout exit-code: 0 test add command: @@ -60,5 +60,5 @@ tests: command: commander.exe test integration/windows/retries.yaml stdout: contains: - - ✗ echo hello, retries 3 + - ✗ [local] echo hello, retries 3 exit-code: 1 diff --git a/pkg/output/cli.go b/pkg/output/cli.go index 8a25ad0c..b3f42080 100644 --- a/pkg/output/cli.go +++ b/pkg/output/cli.go @@ -42,14 +42,14 @@ func (w *OutputWriter) Start(results <-chan runtime.TestResult) bool { for r := range results { testResults = append(testResults, r) if r.ValidationResult.Success { - str := fmt.Sprintf("✓ %s", r.TestCase.Title) + str := fmt.Sprintf("✓ [%s] %s", r.Node, r.TestCase.Title) s := w.addTries(str, r) w.fprintf(s) } if !r.ValidationResult.Success { failed++ - str := fmt.Sprintf("✗ %s", r.TestCase.Title) + str := fmt.Sprintf("✗ [%s] %s", r.Node, r.TestCase.Title) s := w.addTries(str, r) w.fprintf(au.Red(s)) } @@ -87,14 +87,14 @@ func (w *OutputWriter) printFailures(results []runtime.TestResult) { for _, r := range results { if r.TestCase.Result.Error != nil { - str := fmt.Sprintf("✗ '%s' could not be executed with error message:", r.TestCase.Title) + str := fmt.Sprintf("✗ [%s] '%s' could not be executed with error message:", r.Node, r.TestCase.Title) w.fprintf(au.Bold(au.Red(str))) w.fprintf(r.TestCase.Result.Error.Error()) continue } if !r.ValidationResult.Success { - str := fmt.Sprintf("✗ '%s', on property '%s'", r.TestCase.Title, r.FailedProperty) + str := fmt.Sprintf("✗ [%s] '%s', on property '%s'", r.Node, r.TestCase.Title, r.FailedProperty) w.fprintf(au.Bold(au.Red(str))) w.fprintf(r.ValidationResult.Diff) } diff --git a/pkg/output/cli_test.go b/pkg/output/cli_test.go index 7625162c..697d36f4 100644 --- a/pkg/output/cli_test.go +++ b/pkg/output/cli_test.go @@ -39,6 +39,7 @@ func Test_Start(t *testing.T) { Success: true, }, FailedProperty: "", + Node: "docker-host", } results <- runtime.TestResult{ @@ -50,6 +51,7 @@ func Test_Start(t *testing.T) { Success: false, }, FailedProperty: "Stdout", + Node: "192.168.0.1", } results <- runtime.TestResult{ @@ -61,6 +63,7 @@ func Test_Start(t *testing.T) { Success: false, }, FailedProperty: "Stderr", + Node: "ssh-host1", } results <- runtime.TestResult{ @@ -73,6 +76,7 @@ func Test_Start(t *testing.T) { Error: fmt.Errorf("Some error message"), }, }, + Node: "local", } close(results) @@ -80,11 +84,11 @@ func Test_Start(t *testing.T) { assert.True(t, true) output := buf.String() - assert.True(t, strings.Contains(output, "✓ Successful test")) - assert.True(t, strings.Contains(output, "✗ Failed test")) - assert.Contains(t, output, "✗ 'Invalid command' could not be executed") - assert.Contains(t, output, "✗ 'Failed test on stderr', on property 'Stderr") - assert.True(t, strings.Contains(output, "Some error message")) + assert.Contains(t, output, "✓ [docker-host] Successful test") + assert.Contains(t, output, "✗ [192.168.0.1] Failed test") + assert.Contains(t, output, "✗ [local] 'Invalid command' could not be executed with error message") + assert.Contains(t, output, "✗ [ssh-host1] 'Failed test on stderr', on property 'Stderr'") + assert.Contains(t, output, "Some error message") } func Test_SuccessSuite(t *testing.T) { @@ -111,13 +115,14 @@ func Test_SuccessSuite(t *testing.T) { Success: true, }, FailedProperty: "", + Node: "local", } close(results) wg.Wait() assert.True(t, true) - assert.True(t, strings.Contains(buf.String(), "✓ Successful test")) + assert.True(t, strings.Contains(buf.String(), "✓ [local] Successful test")) assert.True(t, strings.Contains(buf.String(), "Duration")) assert.True(t, strings.Contains(buf.String(), "Count: 1, Failed: 0")) } From 82cbb4847df9eb61d2db14dc7f1176a2c28b99c6 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 10 Feb 2020 21:46:24 +0100 Subject: [PATCH 11/23] Add SSH container --- hack/Dockerfile | 24 +++++++++++++++++ hack/Vagrantfile | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ hack/id_rsa | 27 +++++++++++++++++++ hack/id_rsa.pub | 1 + 4 files changed, 122 insertions(+) create mode 100644 hack/Dockerfile create mode 100644 hack/Vagrantfile create mode 100644 hack/id_rsa create mode 100644 hack/id_rsa.pub diff --git a/hack/Dockerfile b/hack/Dockerfile new file mode 100644 index 00000000..fd1a60ee --- /dev/null +++ b/hack/Dockerfile @@ -0,0 +1,24 @@ +FROM ubuntu:18.04 + +RUN apt-get update --fix-missing +RUN apt-get install -y \ + vim \ + openssh-server + +RUN mkdir /var/run/sshd +RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config + +WORKDIR /root +RUN mkdir .ssh +COPY id_rsa .ssh/id_rsa +COPY id_rsa.pub .ssh/id_rsa.pub +COPY id_rsa.pub .ssh/authorized_keys + +RUN chmod 700 ~/.ssh +RUN chmod 644 ~/.ssh/authorized_keys +RUN chmod 600 ~/.ssh/id_rsa +RUN chmod 644 ~/.ssh/id_rsa.pub + +EXPOSE 22 + +CMD ["/usr/sbin/sshd", "-D"] \ No newline at end of file diff --git a/hack/Vagrantfile b/hack/Vagrantfile new file mode 100644 index 00000000..1c4ee1e1 --- /dev/null +++ b/hack/Vagrantfile @@ -0,0 +1,70 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure("2") do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + # Every Vagrant development environment requires a box. You can search for + # boxes at https://vagrantcloud.com/search. + config.vm.box = "base" + + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + # NOTE: This will enable public access to the opened port + # config.vm.network "forwarded_port", guest: 80, host: 8080 + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine and only allow access + # via 127.0.0.1 to disable public access + # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1" + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "../data", "/vagrant_data" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + # vb.memory = "1024" + # end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + # config.vm.provision "shell", inline: <<-SHELL + # apt-get update + # apt-get install -y apache2 + # SHELL +end diff --git a/hack/id_rsa b/hack/id_rsa new file mode 100644 index 00000000..c69f7395 --- /dev/null +++ b/hack/id_rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA1GGb6BXRq0lo7XbDR2GL3yuxbmGAIZ2EfHGfKEVKqaNe8oGN +8soInjUND7+MMfq19knFfiYl6Eq6gXzBBv5cIytuAn06xxRT9NwBYulDFwwiTiOO +edSVY0iLJtIMg8AHyaBPlehiZz7MmbGcGh/azsWAn2gqI2rGxVGFf+wTtub/WVoN +tdm8XeIdOikURTBGdjglfSfxJ3Dwq2HcT1hZ/aNZZgwhysdYM2pvg4pq/J0xNw0w +Yk1g5fkEa6VDW2xSrhh5WUb0jHOUiXj05yhNxUUGQg9+TLyiKxGkZ0MJK+YliKfm +AZjuuCz6sRb2UZ8GC2bqYKltoPxe4CpYmQvi+wIDAQABAoIBAGe55HNNdMG7cMvJ +nUZXlgDBTj8iz3mbfQFqXNlGPljruYunSrSxh9P9BQwT+VzPnd9T2GjsKMdy9/QW +AhBwWn+yzl/Y2cQxYW/No7+zPVkcTATtyMAn225KF+w12nbeau9GpJgKj0Ttnu/R +lvQLaspiG5e/QBVcKQhl8FWx7078TyMBlRl11b3MIk5pEeGNVY6btAYxwAi3iVr6 +z4op9qddWX4e1Cn2PqifLQJzVXbzFBoXwU/52O/i0geVYCrf+khueF5JQASeTbfz +oWq48pjob3hNCHmJ/EjWsoMw0Oa2qsRm5to6XOcTx6H/h+20g9zj3WZxrqaYwP9i +SvGj1EECgYEA670FAlhjo1KSDP/McXRO+pM7SOxBL2eU8MXK/EBG8pxx3FekU4n1 +1EvFXWTubqrAe5mFp5T5IGwIrSi2wdOU21uGV/WxEu83+cF89T3kkQc1eHFJ7ZzG +bs/JeOd1nst69O3w2ZizWJUVak9enheJKAjTlYj06hUJKAmxnb36Ze0CgYEA5qKp +LrlRoiIDFv1sDca2f43hAerhddg9n2OKYNGhngLpsQwp2dP2XXWkg4zZ29yTOzyQ +FLy7YwOJ0rs58fLSXr/Zd1hwdteJInD0kMpC7Uqb/u7vqROhDijGUc9/9lA79TPh +H7lQo3+LISGdZFeL57lxqFLA8zJQL6uj1iU8T4cCgYBjhcVbnB1UkkN+oW+aZlQB +ja71tmg+ryg+DgMIQ8qwGkQnlgiWDCRwoByOtVHD/uqMQ+AdQ9N5UKLJF+swITcy +9IzfDabYK44iLwGmN58La1OzqYBghFNx7N9N38nUZGtghCOueb0dpLMJ0sBLVfxd +nnxVISDkwkS/VnhZ9B6GcQKBgQCcaMGtkzY+ayyHQdQuv41ykah/oWfHgJ/B4UKu +CFy/iSAs1p/nPu08UpUANdxlU4Bx0s+vkJLQQk0MobZZVCDA6wCzBEI5BlDVD598 +Nf8PZ+LFEAtuMYau0I42Wy9fvuf6kXViC04Zo1yaWm6f5xNVUhk3Wu/JvlxNB+zz +RItTLwKBgEnIn5IBmBUCX9meoT/EbASrdpy0YnGRdVCR3JAlzG2HZF3LGkSyAqv2 +iULLNWhSTTxkscjoEAyzaXaT04zO60d77XM1ir5OjSTn0SqUb2P2ixK613WIZz0x +m9Q7sf5OJjJjmeeLMoW635yQYa7RgkSJoP7phaiIFn+FAlIUqAbH +-----END RSA PRIVATE KEY----- diff --git a/hack/id_rsa.pub b/hack/id_rsa.pub new file mode 100644 index 00000000..f7025ad1 --- /dev/null +++ b/hack/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUYZvoFdGrSWjtdsNHYYvfK7FuYYAhnYR8cZ8oRUqpo17ygY3yygieNQ0Pv4wx+rX2ScV+JiXoSrqBfMEG/lwjK24CfTrHFFP03AFi6UMXDCJOI4551JVjSIsm0gyDwAfJoE+V6GJnPsyZsZwaH9rOxYCfaCojasbFUYV/7BO25v9ZWg212bxd4h06KRRFMEZ2OCV9J/EncPCrYdxPWFn9o1lmDCHKx1gzam+Dimr8nTE3DTBiTWDl+QRrpUNbbFKuGHlZRvSMc5SJePTnKE3FRQZCD35MvKIrEaRnQwkr5iWIp+YBmO64LPqxFvZRnwYLZupgqW2g/F7gKliZC+L7 root@72c02a6deeb5 \ No newline at end of file From 747cdb702cb001eab28b66436b8fcb6efa7190a1 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 10 Feb 2020 22:52:40 +0100 Subject: [PATCH 12/23] Add SSH executor to runtime --- .travis.yml | 3 + Makefile | 2 + README.md | 2 +- commander_unix.yaml | 6 +- .../containers/ssh}/Dockerfile | 3 + {hack => integration/containers/ssh}/id_rsa | 0 .../containers/ssh}/id_rsa.pub | 0 integration/setup_unix.sh | 6 ++ integration/teardown_unix.sh | 4 ++ integration/unix/_fixtures/id_rsa.pub | 1 + integration/unix/commander_test.yaml | 2 +- integration/unix/nodes.yaml | 18 ++++++ pkg/app/test_command.go | 4 +- pkg/runtime/runtime.go | 64 ++++++++++++++++--- pkg/runtime/runtime_test.go | 9 ++- pkg/runtime/ssh_executor.go | 37 +++++++++-- pkg/suite/yaml_suite.go | 37 ++++++----- pkg/suite/yaml_suite_test.go | 2 + 18 files changed, 161 insertions(+), 39 deletions(-) rename {hack => integration/containers/ssh}/Dockerfile (87%) rename {hack => integration/containers/ssh}/id_rsa (100%) rename {hack => integration/containers/ssh}/id_rsa.pub (100%) create mode 100755 integration/setup_unix.sh create mode 100755 integration/teardown_unix.sh create mode 100644 integration/unix/_fixtures/id_rsa.pub create mode 100644 integration/unix/nodes.yaml diff --git a/.travis.yml b/.travis.yml index ff9915ed..e2d9bc6b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,9 @@ go: sudo: required dist: trusty +services: + - docker + before_install: - go get -u golang.org/x/lint/golint - curl -L https://github.com/SimonBaeumer/commander/releases/download/v1.2.1/commander-linux-amd64 -o ~/bin/commander diff --git a/Makefile b/Makefile index ad63e7a3..4c624ee6 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,9 @@ test-coverage: integration: build $(info INFO: Starting build $@) + ./integration/setup_unix.sh commander test commander_unix.yaml + ./integration/teardown_unix.sh integration-windows: build $(info INFO: Starting build $@) diff --git a/README.md b/README.md index 2f40ea33..5164bd31 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ nodes: type: ssh addr: 192.168.0.1:22 user: root - public-key: /home/user/id_rsa.pub + identity-file: /home/user/id_rsa.pub docker-host1: type: docker image: alpine:2.4 diff --git a/commander_unix.yaml b/commander_unix.yaml index 9c980197..285d136c 100644 --- a/commander_unix.yaml +++ b/commander_unix.yaml @@ -64,4 +64,8 @@ tests: - ✗ [local] echo hello, retries 3 - ✓ [local] it should retry failed commands, retries 2 - ✗ [local] it should retry failed commands with an interval, retries 2 - exit-code: 1 \ No newline at end of file + exit-code: 1 + + test nodes: + command: ./commander test integration/unix/nodes.yaml + exit-code: 0 \ No newline at end of file diff --git a/hack/Dockerfile b/integration/containers/ssh/Dockerfile similarity index 87% rename from hack/Dockerfile rename to integration/containers/ssh/Dockerfile index fd1a60ee..70b6f121 100644 --- a/hack/Dockerfile +++ b/integration/containers/ssh/Dockerfile @@ -21,4 +21,7 @@ RUN chmod 644 ~/.ssh/id_rsa.pub EXPOSE 22 +RUN echo "test file ssh" >> /root/int-test +RUN chmod 777 /root/int-test + CMD ["/usr/sbin/sshd", "-D"] \ No newline at end of file diff --git a/hack/id_rsa b/integration/containers/ssh/id_rsa similarity index 100% rename from hack/id_rsa rename to integration/containers/ssh/id_rsa diff --git a/hack/id_rsa.pub b/integration/containers/ssh/id_rsa.pub similarity index 100% rename from hack/id_rsa.pub rename to integration/containers/ssh/id_rsa.pub diff --git a/integration/setup_unix.sh b/integration/setup_unix.sh new file mode 100755 index 00000000..33279c4a --- /dev/null +++ b/integration/setup_unix.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Execute it viá "make integration" from the project root + +cd integration/containers/ssh && docker build -t commander-int-ssh-server -f Dockerfile . +docker run -d -p 2222:22 --name=int-ssh-server commander-int-ssh-server \ No newline at end of file diff --git a/integration/teardown_unix.sh b/integration/teardown_unix.sh new file mode 100755 index 00000000..144c557b --- /dev/null +++ b/integration/teardown_unix.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +docker stop int-ssh-server +docker rm int-ssh-server \ No newline at end of file diff --git a/integration/unix/_fixtures/id_rsa.pub b/integration/unix/_fixtures/id_rsa.pub new file mode 100644 index 00000000..f7025ad1 --- /dev/null +++ b/integration/unix/_fixtures/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUYZvoFdGrSWjtdsNHYYvfK7FuYYAhnYR8cZ8oRUqpo17ygY3yygieNQ0Pv4wx+rX2ScV+JiXoSrqBfMEG/lwjK24CfTrHFFP03AFi6UMXDCJOI4551JVjSIsm0gyDwAfJoE+V6GJnPsyZsZwaH9rOxYCfaCojasbFUYV/7BO25v9ZWg212bxd4h06KRRFMEZ2OCV9J/EncPCrYdxPWFn9o1lmDCHKx1gzam+Dimr8nTE3DTBiTWDl+QRrpUNbbFKuGHlZRvSMc5SJePTnKE3FRQZCD35MvKIrEaRnQwkr5iWIp+YBmO64LPqxFvZRnwYLZupgqW2g/F7gKliZC+L7 root@72c02a6deeb5 \ No newline at end of file diff --git a/integration/unix/commander_test.yaml b/integration/unix/commander_test.yaml index d3170971..e47931da 100644 --- a/integration/unix/commander_test.yaml +++ b/integration/unix/commander_test.yaml @@ -64,4 +64,4 @@ tests: inherit-env: true command: echo $USER stdout: from_parent - exit-code: 0 \ No newline at end of file + exit-code: 0 diff --git a/integration/unix/nodes.yaml b/integration/unix/nodes.yaml new file mode 100644 index 00000000..5692409a --- /dev/null +++ b/integration/unix/nodes.yaml @@ -0,0 +1,18 @@ +nodes: + ssh-host: + type: ssh + user: root + identity-file: integration/containers/ssh/id_rsa + addr: localhost:2222 + +tests: + it should test ssh host: + nodes: + - ssh-host + command: cat /root/int-test && whoami && pwd + stdout: + contains: + - test file ssh + - root + - /root + exit-code: 0 \ No newline at end of file diff --git a/pkg/app/test_command.go b/pkg/app/test_command.go index 52e05a0f..855bc302 100644 --- a/pkg/app/test_command.go +++ b/pkg/app/test_command.go @@ -42,7 +42,9 @@ func TestCommand(file string, title string, ctx AddCommandContext) error { tests = []runtime.TestCase{test} } - results := runtime.Start(tests, ctx.Concurrent) + r := runtime.NewRuntime(s.Nodes...) + + results := r.Start(tests, ctx.Concurrent) out := output.NewCliOutput(!ctx.NoColor) if !out.Start(results) { return fmt.Errorf("Test suite failed, use --verbose for more detailed output") diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 5b23e5f7..4cabc2e7 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -3,6 +3,7 @@ package runtime import ( "fmt" "github.com/SimonBaeumer/cmd" + "log" "os" "strings" "sync" @@ -29,6 +30,23 @@ const ( Skipped ) +func NewRuntime(nodes ...Node) Runtime { + local := Node{ + Name: "local", + Type: "local", + Addr: "localhost", + } + + nodes = append(nodes, local) + return Runtime{ + Nodes: nodes, + } +} + +type Runtime struct { + Nodes []Node +} + // TestCase represents a test case which will be executed by the runtime type TestCase struct { Title string @@ -39,12 +57,13 @@ type TestCase struct { } type Node struct { - Name string - Type string - User string - Pass string - Addr string - Image string + Name string + Type string + User string + Pass string + Addr string + Image string + IdentityFile string } //TestConfig represents the configuration for a test @@ -113,7 +132,7 @@ type TestResult struct { // Start starts the given test suite and executes all tests // maxConcurrent configures the amount of go routines which will be started -func Start(tests []TestCase, maxConcurrent int) <-chan TestResult { +func (r *Runtime) Start(tests []TestCase, maxConcurrent int) <-chan TestResult { in := make(chan TestCase) out := make(chan TestResult) @@ -140,7 +159,7 @@ func Start(tests []TestCase, maxConcurrent int) <-chan TestResult { result := TestResult{} for i := 1; i <= t.Command.GetRetries(); i++ { - e := LocalExecutor{} + e := r.getExecutor(n) result = e.Execute(t) result.Node = n result.Tries = i @@ -165,6 +184,35 @@ func Start(tests []TestCase, maxConcurrent int) <-chan TestResult { return out } +func (r *Runtime) getExecutor(node string) Executor { + if len(r.Nodes) == 0 { + return LocalExecutor{} + } + + for _, n := range r.Nodes { + if n.Name == node { + switch n.Type { + case "ssh": + return SSHExecutor{ + Password: n.Pass, + IdentityFile: n.IdentityFile, + User: n.User, + Host: n.Addr, + } + case "local": + return LocalExecutor{} + case "": + return LocalExecutor{} + default: + log.Fatal(fmt.Sprintf("Node type %s not found for node %s", n.Type, n.Name)) + } + } + } + + log.Fatal(fmt.Sprintf("Node %s not found", node)) + return LocalExecutor{} +} + func execTest(t TestCase) TestResult { result := TestResult{} for i := 1; i <= t.Command.GetRetries(); i++ { diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 03a78545..57421dab 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -10,7 +10,8 @@ const SingleConcurrent = 1 func TestRuntime_Start(t *testing.T) { s := getExampleTestSuite() - got := Start(s, SingleConcurrent) + r := Runtime{} + got := r.Start(s, SingleConcurrent) assert.IsType(t, make(<-chan TestResult), got) @@ -28,7 +29,8 @@ func TestRuntime_WithRetries(t *testing.T) { s[0].Command.Retries = 3 s[0].Command.Cmd = "echo fail" - got := Start(s, 1) + r := Runtime{} + got := r.Start(s, 1) var counter = 0 for r := range got { @@ -47,7 +49,8 @@ func TestRuntime_WithRetriesAndInterval(t *testing.T) { s[0].Command.Interval = "50ms" start := time.Now() - got := Start(s, 0) + r := Runtime{} + got := r.Start(s, 0) var counter = 0 for r := range got { diff --git a/pkg/runtime/ssh_executor.go b/pkg/runtime/ssh_executor.go index 328b5835..307a92ff 100644 --- a/pkg/runtime/ssh_executor.go +++ b/pkg/runtime/ssh_executor.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "golang.org/x/crypto/ssh" + "io/ioutil" "log" "net" "strings" @@ -11,32 +12,45 @@ import ( // SSHExecutor type SSHExecutor struct { - Host string - User string - Password string + Host string + User string + Password string + IdentityFile string } // Execute executes a command on a remote host viá SSH func (e SSHExecutor) Execute(test TestCase) TestResult { if test.Command.InheritEnv { - log.Fatal("Inhereit env is not supported viá SSH") + log.Fatal("Inherit env is not supported viá SSH") } + // initialize auth methods with pass auth method as the default + authMethods := []ssh.AuthMethod{ + ssh.Password(e.Password), + } + + // add public key auth if identity file is given + if e.IdentityFile != "" { + signer := e.createSigner() + authMethods = append(authMethods, ssh.PublicKeys(signer)) + } + + // create ssh config sshConf := &ssh.ClientConfig{ User: e.User, - Auth: []ssh.AuthMethod{ - ssh.Password(e.Password), - }, + Auth: authMethods, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }, } + // create ssh connection conn, err := ssh.Dial("tcp", e.Host, sshConf) if err != nil { log.Fatal(err) } + // start session session, err := conn.NewSession() defer session.Close() if err != nil { @@ -91,3 +105,12 @@ func (e SSHExecutor) Execute(test TestCase) TestResult { return Validate(test) } + +func (e SSHExecutor) createSigner() ssh.Signer { + buffer, err := ioutil.ReadFile(e.IdentityFile) + if err != nil { + log.Fatal(err) + } + signer, err := ssh.ParsePrivateKey(buffer) + return signer +} diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go index b1ad5976..0cdfa9d7 100644 --- a/pkg/suite/yaml_suite.go +++ b/pkg/suite/yaml_suite.go @@ -26,12 +26,13 @@ type YAMLTestConfig struct { } type NodeConf struct { - Name string `yaml:"-"` - Type string `yaml:"type"` - User string `yaml:"user"` - Pass string `yaml:"pass"` - Addr string `yaml:"addr,omitempty"` - Image string `yaml:"image,omitempty"` + Name string `yaml:"-"` + Type string `yaml:"type"` + User string `yaml:"user"` + Pass string `yaml:"pass,omitempty"` + Addr string `yaml:"addr,omitempty"` + Image string `yaml:"image,omitempty"` + IdentityFile string `yaml:"identity-file,omitempty"` } type DockerConf struct { @@ -84,12 +85,13 @@ func convertNodes(nodes map[string]NodeConf) []runtime.Node { var n []runtime.Node for _, v := range nodes { n = append(n, runtime.Node{ - Pass: v.Pass, - Type: v.Type, - User: v.User, - Addr: v.Addr, - Name: v.Name, - Image: v.Image, + Pass: v.Pass, + Type: v.Type, + User: v.User, + Addr: v.Addr, + Name: v.Name, + Image: v.Image, + IdentityFile: v.IdentityFile, }) } return n @@ -164,11 +166,12 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { y.Nodes = make(map[string]NodeConf) for k, v := range params.Nodes { node := NodeConf{ - Name: k, - Addr: v.Addr, - User: v.User, - Type: v.Type, - Pass: v.Pass, + Name: k, + Addr: v.Addr, + User: v.User, + Type: v.Type, + Pass: v.Pass, + IdentityFile: v.IdentityFile, } y.Nodes[k] = node diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go index 235df587..58fcb6ea 100644 --- a/pkg/suite/yaml_suite_test.go +++ b/pkg/suite/yaml_suite_test.go @@ -330,6 +330,7 @@ nodes: addr: localhost user: root pass: 12345! + identity-file: ".ssh/id_rsa" docker-host: type: docker image: ubuntu:18.04 @@ -352,6 +353,7 @@ tests: assert.Equal(t, "root", node.User) assert.Equal(t, "12345!", node.Pass) assert.Equal(t, "ssh", node.Type) + assert.Equal(t, ".ssh/id_rsa", node.IdentityFile) assert.Contains(t, got.GetTests()[0].Nodes, "docker-host") assert.Contains(t, got.GetTests()[0].Nodes, "ssh-host1") From 369f8c4764c5b6047599fb72fc4736712de23791 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 10 Feb 2020 22:58:17 +0100 Subject: [PATCH 13/23] Add local executor to ssh test --- commander_unix.yaml | 6 +++++- integration/setup_unix.sh | 3 +++ integration/unix/nodes.yaml | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/commander_unix.yaml b/commander_unix.yaml index 285d136c..e35e12f2 100644 --- a/commander_unix.yaml +++ b/commander_unix.yaml @@ -68,4 +68,8 @@ tests: test nodes: command: ./commander test integration/unix/nodes.yaml - exit-code: 0 \ No newline at end of file + stdout: + contains: + - ✓ [ssh-host] it should test ssh host + - ✗ [local] it should test ssh host + exit-code: 1 \ No newline at end of file diff --git a/integration/setup_unix.sh b/integration/setup_unix.sh index 33279c4a..586401e6 100755 --- a/integration/setup_unix.sh +++ b/integration/setup_unix.sh @@ -2,5 +2,8 @@ # Execute it viá "make integration" from the project root +docker stop int-ssh-server || true +docker rm int-ssh-server || true + cd integration/containers/ssh && docker build -t commander-int-ssh-server -f Dockerfile . docker run -d -p 2222:22 --name=int-ssh-server commander-int-ssh-server \ No newline at end of file diff --git a/integration/unix/nodes.yaml b/integration/unix/nodes.yaml index 5692409a..6ceba42e 100644 --- a/integration/unix/nodes.yaml +++ b/integration/unix/nodes.yaml @@ -9,6 +9,7 @@ tests: it should test ssh host: nodes: - ssh-host + - local command: cat /root/int-test && whoami && pwd stdout: contains: From 9bb1d65da877642a5d7b90c137a0edfc27a50091 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 10:15:12 +0100 Subject: [PATCH 14/23] Add unit tests --- .travis.yml | 2 +- Makefile | 13 +++++++++++ README.md | 13 +++++++++++ ...{ssh_executor.go => ssh_executor_linux.go} | 0 ...tor_test.go => ssh_executor_linux_test.go} | 23 ++++++++++++++++--- 5 files changed, 47 insertions(+), 4 deletions(-) rename pkg/runtime/{ssh_executor.go => ssh_executor_linux.go} (100%) rename pkg/runtime/{ssh_executor_test.go => ssh_executor_linux_test.go} (72%) diff --git a/.travis.yml b/.travis.yml index e2d9bc6b..564314cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,7 +63,7 @@ jobs: - chmod +x test-reporter - ./test-reporter before-build script: - - make test-coverage + - make test-coverage-all after_script: - ./test-reporter after-build -t gocov --exit-code $TRAVIS_TEST_RESULT diff --git a/Makefile b/Makefile index 4c624ee6..26e4a76f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ exe = cmd/commander/* cmd = commander TRAVIS_TAG ?= "0.0.0" +CWD = $(shell pwd) .PHONY: deps lint test integration integration-windows git-hooks init @@ -30,6 +31,18 @@ test-coverage: $(info INFO: Starting build $@) go test -coverprofile c.out ./... + +test-coverage-all: export COMMANDER_SSH_TEST = 1 +test-coverage-all: export COMMANDER_TEST_SSH_HOST = localhost:2222 +#test-coverage-all: export COMMANDER_TEST_SSH_PASS = password +test-coverage-all: export COMMANDER_TEST_SSH_USER = root +test-coverage-all: export COMMANDER_TEST_SSH_IDENTITY_FILE = $(CWD)/integration/containers/ssh/id_rsa +test-coverage-all: + $(info INFO: Starting build $@) + ./integration/setup_unix.sh + go test -coverprofile c.out ./... + ./integration/teardown_unix.sh + integration: build $(info INFO: Starting build $@) ./integration/setup_unix.sh diff --git a/README.md b/README.md index 5164bd31..4eb66207 100644 --- a/README.md +++ b/README.md @@ -647,6 +647,19 @@ $ make integration $ make deps ``` +### Unit tests + +Enables ssh tests in unit test suite and sets the credentials for the target host. +`COMMANDER_SSH_TEST` must be set to `1` to enable ssh tests. + +``` +COMMANDER_TEST_SSH=1 +COMMANDER_TEST_SSH_HOST=localhost:2222 +COMMANDER_TEST_SSH_PASS=password +COMMANDER_TEST_SSH_USER=root +COMMANDER_TEST_SSH_IDENTITY_FILE=~/.ssh/id_rsa +``` + ## Misc Heavily inspired by [goss](https://github.com/aelsabbahy/goss). diff --git a/pkg/runtime/ssh_executor.go b/pkg/runtime/ssh_executor_linux.go similarity index 100% rename from pkg/runtime/ssh_executor.go rename to pkg/runtime/ssh_executor_linux.go diff --git a/pkg/runtime/ssh_executor_test.go b/pkg/runtime/ssh_executor_linux_test.go similarity index 72% rename from pkg/runtime/ssh_executor_test.go rename to pkg/runtime/ssh_executor_linux_test.go index e92d6f37..82b7d232 100644 --- a/pkg/runtime/ssh_executor_test.go +++ b/pkg/runtime/ssh_executor_linux_test.go @@ -7,6 +7,15 @@ import ( "testing" ) +type SSHExecutorTestEnv struct { + User string + Pass string + Host string + IdentityFile string +} + +var SSHTestEnv SSHExecutorTestEnv + func TestMain(m *testing.M) { v := os.Getenv("COMMANDER_SSH_TEST") if v != "1" { @@ -14,14 +23,22 @@ func TestMain(m *testing.M) { return } + SSHTestEnv = SSHExecutorTestEnv{ + Host: os.Getenv("COMMANDER_TEST_SSH_HOST"), + Pass: os.Getenv("COMMANDER_TEST_SSH_PASS"), + User: os.Getenv("COMMANDER_TEST_SSH_USER"), + IdentityFile: os.Getenv("COMMANDER_TEST_SSH_IDENTITY_FILE"), + } + m.Run() } func createExecutor() SSHExecutor { s := SSHExecutor{ - Host: "localhost:2222", - User: "vagrant", - Password: "vagrant", + Host: SSHTestEnv.Host, + User: SSHTestEnv.User, + Password: SSHTestEnv.Pass, + IdentityFile: SSHTestEnv.IdentityFile, } return s } From a7ee645e2eaddb0cc8aacc2ecb48a46146fa3575 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 11:03:59 +0100 Subject: [PATCH 15/23] Add unit tests for runtime --- CHANGELOG.md | 6 ++++ README.md | 10 +++--- cmd/commander/commander.go | 5 --- pkg/app/test_command.go | 2 +- pkg/runtime/docker_executor.go | 9 ----- pkg/runtime/runtime.go | 22 +------------ pkg/runtime/runtime_test.go | 32 +++++++++++++++--- ...{ssh_executor_linux.go => ssh_executor.go} | 0 pkg/runtime/ssh_executor_linux_test.go | 33 ++++++++++++------- 9 files changed, 63 insertions(+), 56 deletions(-) delete mode 100644 pkg/runtime/docker_executor.go rename pkg/runtime/{ssh_executor_linux.go => ssh_executor.go} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e57587a..56902239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v2.0.0 + + - Added `nodes` which allow remote execution of tests + - Added `SSHExecutor` and `LocalExecutor` + - Removed `concurrent` argument from `test` command + # v1.3.0 - Added `xml` assertion to `stdout` and `stderr` diff --git a/README.md b/README.md index 4eb66207..d3816dfc 100644 --- a/README.md +++ b/README.md @@ -653,11 +653,11 @@ Enables ssh tests in unit test suite and sets the credentials for the target hos `COMMANDER_SSH_TEST` must be set to `1` to enable ssh tests. ``` -COMMANDER_TEST_SSH=1 -COMMANDER_TEST_SSH_HOST=localhost:2222 -COMMANDER_TEST_SSH_PASS=password -COMMANDER_TEST_SSH_USER=root -COMMANDER_TEST_SSH_IDENTITY_FILE=~/.ssh/id_rsa +export COMMANDER_TEST_SSH=1 +export COMMANDER_TEST_SSH_HOST=localhost:2222 +export COMMANDER_TEST_SSH_PASS=pass +export COMMANDER_TEST_SSH_USER=root +export COMMANDER_TEST_SSH_IDENTITY_FILE=integration/containers/ssh/.ssh/id_rsa ``` ## Misc diff --git a/cmd/commander/commander.go b/cmd/commander/commander.go index c153fd8f..94c71a4b 100644 --- a/cmd/commander/commander.go +++ b/cmd/commander/commander.go @@ -48,11 +48,6 @@ func createTestCommand() cli.Command { Usage: "Execute the test suite, by default it will use the commander.yaml from your current directory", ArgsUsage: "[file] [title]", Flags: []cli.Flag{ - cli.IntFlag{ - Name: "concurrent", - EnvVar: "COMMANDER_CONCURRENT", - Usage: "Set the max amount of tests which should run concurrently", - }, cli.BoolFlag{ Name: "no-color", EnvVar: "COMMANDER_NO_COLOR", diff --git a/pkg/app/test_command.go b/pkg/app/test_command.go index 855bc302..18491e68 100644 --- a/pkg/app/test_command.go +++ b/pkg/app/test_command.go @@ -44,7 +44,7 @@ func TestCommand(file string, title string, ctx AddCommandContext) error { r := runtime.NewRuntime(s.Nodes...) - results := r.Start(tests, ctx.Concurrent) + results := r.Start(tests) out := output.NewCliOutput(!ctx.NoColor) if !out.Start(results) { return fmt.Errorf("Test suite failed, use --verbose for more detailed output") diff --git a/pkg/runtime/docker_executor.go b/pkg/runtime/docker_executor.go deleted file mode 100644 index 78534f5c..00000000 --- a/pkg/runtime/docker_executor.go +++ /dev/null @@ -1,9 +0,0 @@ -package runtime - -type DockerExecutor struct { - Image string -} - -func (e DockerExecutor) Execute(test TestCase) TestResult { - return TestResult{} -} diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 4cabc2e7..c391f778 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -18,8 +18,6 @@ const ( LineCount = "LineCount" ) -const WorkerCountMultiplicator = 5 - // Result status codes const ( //Success status @@ -131,8 +129,7 @@ type TestResult struct { } // Start starts the given test suite and executes all tests -// maxConcurrent configures the amount of go routines which will be started -func (r *Runtime) Start(tests []TestCase, maxConcurrent int) <-chan TestResult { +func (r *Runtime) Start(tests []TestCase) <-chan TestResult { in := make(chan TestCase) out := make(chan TestResult) @@ -213,23 +210,6 @@ func (r *Runtime) getExecutor(node string) Executor { return LocalExecutor{} } -func execTest(t TestCase) TestResult { - result := TestResult{} - for i := 1; i <= t.Command.GetRetries(); i++ { - - e := LocalExecutor{} - result = e.Execute(t) - - result.Tries = i - if result.ValidationResult.Success { - break - } - - executeRetryInterval(t) - } - return result -} - func executeRetryInterval(t TestCase) { if t.Command.GetRetries() > 1 && t.Command.Interval != "" { interval, err := time.ParseDuration(t.Command.Interval) diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 57421dab..a26704a4 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -6,12 +6,16 @@ import ( "time" ) -const SingleConcurrent = 1 +func Test_NewRuntime(t *testing.T) { + runtime := NewRuntime(Node{Name: "test"}, Node{Name: "test2"}) + + assert.Len(t, runtime.Nodes, 3) +} func TestRuntime_Start(t *testing.T) { s := getExampleTestSuite() r := Runtime{} - got := r.Start(s, SingleConcurrent) + got := r.Start(s) assert.IsType(t, make(<-chan TestResult), got) @@ -30,7 +34,7 @@ func TestRuntime_WithRetries(t *testing.T) { s[0].Command.Cmd = "echo fail" r := Runtime{} - got := r.Start(s, 1) + got := r.Start(s) var counter = 0 for r := range got { @@ -50,7 +54,7 @@ func TestRuntime_WithRetriesAndInterval(t *testing.T) { start := time.Now() r := Runtime{} - got := r.Start(s, 0) + got := r.Start(s) var counter = 0 for r := range got { @@ -64,6 +68,26 @@ func TestRuntime_WithRetriesAndInterval(t *testing.T) { assert.True(t, duration.Seconds() > 0.15, "Retry interval did not work") } +func Test_Runtime_getExecutor(t *testing.T) { + r := NewRuntime( + Node{Name: "ssh-host", Type: "ssh"}, + Node{Name: "localhost", Type: "local"}, + Node{Name: "default", Type: ""}, + ) + + // If empty string set as type use local executor + n := r.getExecutor("default") + assert.IsType(t, LocalExecutor{}, n) + + n = nil + n = r.getExecutor("localhost") + assert.IsType(t, LocalExecutor{}, n) + + n = nil + n = r.getExecutor("ssh-host") + assert.IsType(t, SSHExecutor{}, n) +} + func getExampleTestSuite() []TestCase { tests := []TestCase{ { diff --git a/pkg/runtime/ssh_executor_linux.go b/pkg/runtime/ssh_executor.go similarity index 100% rename from pkg/runtime/ssh_executor_linux.go rename to pkg/runtime/ssh_executor.go diff --git a/pkg/runtime/ssh_executor_linux_test.go b/pkg/runtime/ssh_executor_linux_test.go index 82b7d232..08dbb214 100644 --- a/pkg/runtime/ssh_executor_linux_test.go +++ b/pkg/runtime/ssh_executor_linux_test.go @@ -16,13 +16,7 @@ type SSHExecutorTestEnv struct { var SSHTestEnv SSHExecutorTestEnv -func TestMain(m *testing.M) { - v := os.Getenv("COMMANDER_SSH_TEST") - if v != "1" { - log.Println("Skip ssh_executor_test, set env COMMANDER_SSH_TEST to 1") - return - } - +func createExecutor() SSHExecutor { SSHTestEnv = SSHExecutorTestEnv{ Host: os.Getenv("COMMANDER_TEST_SSH_HOST"), Pass: os.Getenv("COMMANDER_TEST_SSH_PASS"), @@ -30,10 +24,6 @@ func TestMain(m *testing.M) { IdentityFile: os.Getenv("COMMANDER_TEST_SSH_IDENTITY_FILE"), } - m.Run() -} - -func createExecutor() SSHExecutor { s := SSHExecutor{ Host: SSHTestEnv.Host, User: SSHTestEnv.User, @@ -44,6 +34,10 @@ func createExecutor() SSHExecutor { } func Test_SSHExecutor(t *testing.T) { + if !isSSHTestsEnabled() { + return + } + s := createExecutor() test := TestCase{ @@ -62,6 +56,10 @@ func Test_SSHExecutor(t *testing.T) { } func Test_SSHExecutor_WithDir(t *testing.T) { + if !isSSHTestsEnabled() { + return + } + s := createExecutor() test := TestCase{ @@ -81,6 +79,10 @@ func Test_SSHExecutor_WithDir(t *testing.T) { } func Test_SSHExecutor_ExitCode(t *testing.T) { + if !isSSHTestsEnabled() { + return + } + s := createExecutor() test := TestCase{ @@ -94,3 +96,12 @@ func Test_SSHExecutor_ExitCode(t *testing.T) { assert.False(t, got.ValidationResult.Success) assert.Equal(t, 2, got.TestCase.Result.ExitCode) } + +func isSSHTestsEnabled() bool { + v := os.Getenv("COMMANDER_SSH_TEST") + if v != "1" { + log.Println("Skip ssh_executor_test, set env COMMANDER_SSH_TEST to 1") + return false + } + return true +} From cfe4a8443e801fdb5514717e773941dbba46b385 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 11:18:22 +0100 Subject: [PATCH 16/23] Add suite tests --- pkg/runtime/runtime.go | 1 - pkg/suite/suite.go | 9 +------- pkg/suite/suite_test.go | 47 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index c391f778..2930575f 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -72,7 +72,6 @@ type TestConfig struct { Retries int Interval string InheritEnv bool - SSH SSHExecutor } // ResultStatus represents the status code of a test result diff --git a/pkg/suite/suite.go b/pkg/suite/suite.go index 6d20b795..a3d752f6 100644 --- a/pkg/suite/suite.go +++ b/pkg/suite/suite.go @@ -12,13 +12,6 @@ type Suite struct { Nodes []runtime.Node } -func NewSuite(config runtime.TestConfig, tests ...runtime.TestCase) *Suite { - return &Suite{ - TestCases: tests, - Config: config, - } -} - func (s Suite) GetNodes() []runtime.Node { return s.Nodes } @@ -33,7 +26,7 @@ func (s Suite) GetNodeByName(name string) (runtime.Node, error) { } func (s Suite) AddTest(t runtime.TestCase) { - if _, err := s.GetTestByTitle(t.Title); err != nil { + if _, err := s.GetTestByTitle(t.Title); err == nil { panic(fmt.Sprintf("Tests %s was already added to the suite", t.Title)) } s.TestCases = append(s.TestCases, t) diff --git a/pkg/suite/suite_test.go b/pkg/suite/suite_test.go index 792b0335..0d0d05c7 100644 --- a/pkg/suite/suite_test.go +++ b/pkg/suite/suite_test.go @@ -1 +1,48 @@ package suite + +import ( + "github.com/SimonBaeumer/commander/pkg/runtime" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_GetNodes(t *testing.T) { + s := Suite{ + Nodes: []runtime.Node{runtime.Node{}, runtime.Node{}}, + } + + assert.Len(t, s.GetNodes(), 2) +} + +func Test_GetNodesByName(t *testing.T) { + s := Suite{ + Nodes: []runtime.Node{runtime.Node{}, runtime.Node{Name: "node1"}}, + } + + node, e := s.GetNodeByName("node1") + assert.Equal(t, node.Name, "node1") + assert.Nil(t, e) + + node, e = s.GetNodeByName("doesnt-exist") + assert.EqualError(t, e, "could not find node with name doesnt-exist") +} + +func Test_AddTest(t *testing.T) { + s := Suite{TestCases: []runtime.TestCase{{Title: "exists"}}} + s.AddTest(runtime.TestCase{Title: "test"}) + + assert.Len(t, s.GetTests(), 1) +} + +func Test_GetTestByTitle(t *testing.T) { + s := Suite{TestCases: []runtime.TestCase{{Title: "exists"}}} + test, err := s.GetTestByTitle("exists") + + assert.Nil(t, err) + assert.Equal(t, "exists", test.Title) +} + +func Test_GetGlobalConfig(t *testing.T) { + s := Suite{Config: runtime.TestConfig{Dir: "/tmp"}} + assert.Equal(t, "/tmp", s.GetGlobalConfig().Dir) +} From 2f34a676025e707cbf329d3efbee3ed7265ca044 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 11:52:35 +0100 Subject: [PATCH 17/23] Add better errors for ssh --- commander_unix.yaml | 3 +++ integration/unix/nodes.yaml | 20 +++++++++++++++++++- pkg/output/cli.go | 2 ++ pkg/runtime/runtime.go | 1 + pkg/runtime/ssh_executor.go | 9 +++++++-- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/commander_unix.yaml b/commander_unix.yaml index e35e12f2..a11e30e1 100644 --- a/commander_unix.yaml +++ b/commander_unix.yaml @@ -71,5 +71,8 @@ tests: stdout: contains: - ✓ [ssh-host] it should test ssh host + - ✓ [ssh-host] it should set env variable - ✗ [local] it should test ssh host + - ✗ [ssh-host] it should fail if env could not be set + - "Failed setting env variables, maybe ssh server is configured to only accept LC_ prefixed env variables. Error: ssh: setenv failed" exit-code: 1 \ No newline at end of file diff --git a/integration/unix/nodes.yaml b/integration/unix/nodes.yaml index 6ceba42e..17ad0948 100644 --- a/integration/unix/nodes.yaml +++ b/integration/unix/nodes.yaml @@ -16,4 +16,22 @@ tests: - test file ssh - root - /root - exit-code: 0 \ No newline at end of file + exit-code: 0 + + it should set env variable: + nodes: + - ssh-host + config: + env: + LC_TEST_ENV: env var + command: echo $LC_TEST_ENV + stdout: env var + + it should fail if env could not be set: + nodes: + - ssh-host + config: + env: + TEST_ENV: env var + command: echo $TEST_ENV + stdout: env var diff --git a/pkg/output/cli.go b/pkg/output/cli.go index b3f42080..3bd36f95 100644 --- a/pkg/output/cli.go +++ b/pkg/output/cli.go @@ -45,6 +45,7 @@ func (w *OutputWriter) Start(results <-chan runtime.TestResult) bool { str := fmt.Sprintf("✓ [%s] %s", r.Node, r.TestCase.Title) s := w.addTries(str, r) w.fprintf(s) + continue } if !r.ValidationResult.Success { @@ -52,6 +53,7 @@ func (w *OutputWriter) Start(results <-chan runtime.TestResult) bool { str := fmt.Sprintf("✗ [%s] %s", r.Node, r.TestCase.Title) s := w.addTries(str, r) w.fprintf(au.Red(s)) + continue } } diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 2930575f..8632aa7d 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -125,6 +125,7 @@ type TestResult struct { FailedProperty string Tries int Node string + Error error } // Start starts the given test suite and executes all tests diff --git a/pkg/runtime/ssh_executor.go b/pkg/runtime/ssh_executor.go index 307a92ff..7082353c 100644 --- a/pkg/runtime/ssh_executor.go +++ b/pkg/runtime/ssh_executor.go @@ -21,7 +21,7 @@ type SSHExecutor struct { // Execute executes a command on a remote host viá SSH func (e SSHExecutor) Execute(test TestCase) TestResult { if test.Command.InheritEnv { - log.Fatal("Inherit env is not supported viá SSH") + panic("Inherit env is not supported viá SSH") } // initialize auth methods with pass auth method as the default @@ -65,7 +65,12 @@ func (e SSHExecutor) Execute(test TestCase) TestResult { for k, v := range test.Command.Env { err := session.Setenv(k, v) if err != nil { - log.Fatal(fmt.Sprintf("Failed, maybe ssh server is configured to only accept LC_ prefixed env variables. Error: %s", err)) + test.Result = CommandResult{ + Error: fmt.Errorf("Failed setting env variables, maybe ssh server is configured to only accept LC_ prefixed env variables. Error: %s", err), + } + return TestResult{ + TestCase: test, + } } } From c030b61dffa23395885b3f7bd66b0d2909baceaf Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 12:05:09 +0100 Subject: [PATCH 18/23] Add node documentation --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/README.md b/README.md index d3816dfc..5ca27699 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Define language independent tests for your command line scripts and programs in simple `yaml` files. - It runs on `windows`, `osx` and `linux` + - It can validate local machines, ssh hosts and docker containers - It is a self-contained binary - no need to install a heavy lib or language - It is easy and fast to write @@ -47,6 +48,10 @@ For more information take a look at the [quick start](#quick-start), the [exampl - [interval](#interval) - [retries](#retries) - [timeout](#timeout) + - [nodes](#nodes) + + [Nodes](#nodes) + - [local](#local) + - [ssh](#ssh) + [Development](#development) * [Misc](#misc) @@ -625,6 +630,82 @@ If a tests exceeds the given `timeout` the test will fail. timeout: 600s ``` +### Nodes + +`Commander` has the option to execute tests against other hosts, i.e. via ssh. + +Available node types are currently: + + - `local`, execute tests locally + - `ssh`, execute tests viá ssh + +```yaml +nodes: # define nodes in the node section + ssh-host: + type: ssh # define the type of the connection + user: root # set the user which is used by the connection + pass: password # set password for authentication + addr: 192.168.0.100:2222 # target host address + identity-file: ~/.ssh/id_rsa # auth with private key +tests: + echo hello: + config: + nodes: # define on which host the test should be executed + - ssh-host + stdout: hello + exit-code: 0 +``` + +You can identify on which node a test failed by inspecting the test output. +The `[local]` and `[ssh-host]` represent the node name on which the test were executed. + +``` +✗ [local] it should test ssh host +✗ [ssh-host] it should fail if env could not be set +``` + +#### local + +The `local` node is the default execution and will be applied if nothing else was configured. +It is always pre-configured and available, i.e. if you want to execute tests on a node and locally. + +```yaml +nodes: + ssh-host: + addr: 192.168.1.100 + user: ... +tests: + echo hello: + config: + nodes: # will be executed on local and ssh-host + - ssh-host + - local + exit-code: 0 +``` + +#### ssh + +The `ssh` will execute tests against a configured node using ssh. + +**Limitations:** The `inhereit-env` config is disabled for ssh hosts, nevertheless it is possible to set env variables + +```yaml +nodes: # define nodes in the node section + ssh-host: + type: ssh # define the type of the connection + user: root # set the user which is used by the connection + pass: password # set password for authentication + addr: 192.168.0.100:2222 # target host address + identity-file: ~/.ssh/id_rsa # auth with private key +tests: + echo hello: + config: + nodes: # define on which host the test should be executed + - ssh-host + stdout: hello + exit-code: 0 +``` + ### Development ``` @@ -640,6 +721,9 @@ $ make test # Coverage $ make test-coverage +# Coverage with more complex tests like ssh execution +$ make test-coverage-all + # Integration tests $ make integration From 8c95c0524b8aae61964c73a9b6f2d636fd7ada1e Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 13:41:44 +0100 Subject: [PATCH 19/23] Move nodes config from root to config property and add inhereitance support --- README.md | 7 ++++++- commander_unix.yaml | 9 +++++---- integration/unix/nodes.yaml | 38 ++++++++++++++++++++++++------------ pkg/runtime/runtime.go | 2 +- pkg/suite/yaml_suite.go | 11 ++++++++--- pkg/suite/yaml_suite_test.go | 7 ++++--- 6 files changed, 50 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 5ca27699..78a828fc 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,8 @@ config: # Config for all executed tests KEY: global timeout: 50s # Define a timeout for a command under test retries: 2 # Define retries for each test + nodes: + - ssh-host1 # define default hosts tests: echo hello: # Define command as title @@ -164,7 +166,7 @@ tests: command: echo hello stdout: contains: - - hello #See test "it should fail" + - hello #See test "it should fail" exactly: hello line-count: 1 config: @@ -175,6 +177,9 @@ tests: ANOTHER: yeah # Add another env variable timeout: 1s # Overwrite timeout retries: 5 + nodes: # overwrite default nodes + - docker-host1 + - docker-host2 exit-code: 0 ``` diff --git a/commander_unix.yaml b/commander_unix.yaml index a11e30e1..8df9ed09 100644 --- a/commander_unix.yaml +++ b/commander_unix.yaml @@ -72,7 +72,8 @@ tests: contains: - ✓ [ssh-host] it should test ssh host - ✓ [ssh-host] it should set env variable - - ✗ [local] it should test ssh host - - ✗ [ssh-host] it should fail if env could not be set - - "Failed setting env variables, maybe ssh server is configured to only accept LC_ prefixed env variables. Error: ssh: setenv failed" - exit-code: 1 \ No newline at end of file + - ✓ [ssh-host-default] it should be executed on ssh-host-default + - ✓ [ssh-host] it should test multiple hosts + - ✓ [ssh-host-default] it should test multiple hosts + - ✓ [local] it should test multiple hosts + exit-code: 0 \ No newline at end of file diff --git a/integration/unix/nodes.yaml b/integration/unix/nodes.yaml index 17ad0948..eac02d3e 100644 --- a/integration/unix/nodes.yaml +++ b/integration/unix/nodes.yaml @@ -5,12 +5,22 @@ nodes: identity-file: integration/containers/ssh/id_rsa addr: localhost:2222 + ssh-host-default: + type: ssh + user: root + identity-file: integration/containers/ssh/id_rsa + addr: localhost:2222 + +config: + nodes: + - ssh-host-default + tests: it should test ssh host: - nodes: - - ssh-host - - local command: cat /root/int-test && whoami && pwd + config: + nodes: + - ssh-host stdout: contains: - test file ssh @@ -19,19 +29,23 @@ tests: exit-code: 0 it should set env variable: - nodes: - - ssh-host config: + nodes: + - ssh-host env: LC_TEST_ENV: env var command: echo $LC_TEST_ENV stdout: env var - it should fail if env could not be set: - nodes: - - ssh-host + it should be executed on ssh-host-default: + command: whoami + stdout: root + + it should test multiple hosts: + command: echo hello + stdout: hello config: - env: - TEST_ENV: env var - command: echo $TEST_ENV - stdout: env var + nodes: + - ssh-host-default + - ssh-host + - local diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 8632aa7d..b59df1e0 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -72,6 +72,7 @@ type TestConfig struct { Retries int Interval string InheritEnv bool + Nodes []string } // ResultStatus represents the status code of a test result @@ -115,7 +116,6 @@ type CommandUnderTest struct { Timeout string Retries int Interval string - Executors []Executor } // TestResult represents the TestCase and the ValidationResult diff --git a/pkg/suite/yaml_suite.go b/pkg/suite/yaml_suite.go index 0cdfa9d7..bea5e085 100644 --- a/pkg/suite/yaml_suite.go +++ b/pkg/suite/yaml_suite.go @@ -23,6 +23,7 @@ type YAMLTestConfig struct { Timeout string `yaml:"timeout,omitempty"` Retries int `yaml:"retries,omitempty"` Interval string `yaml:"interval,omitempty"` + Nodes []string `yaml:"nodes,omitempty"` } type NodeConf struct { @@ -55,7 +56,6 @@ type YAMLTest struct { Stdout interface{} `yaml:"stdout,omitempty"` Stderr interface{} `yaml:"stderr,omitempty"` Config YAMLTestConfig `yaml:"config,omitempty"` - Nodes []string `yaml:"nodes,omitempty"` } // ParseYAML parses the Suite from a yaml byte slice @@ -76,6 +76,7 @@ func ParseYAML(content []byte) Suite { Timeout: yamlConfig.Config.Timeout, Retries: yamlConfig.Config.Retries, Interval: yamlConfig.Config.Interval, + Nodes: yamlConfig.Config.Nodes, }, Nodes: convertNodes(yamlConfig.Nodes), } @@ -117,7 +118,7 @@ func convertYAMLConfToTestCases(conf YAMLConfig) []runtime.TestCase { Stdout: t.Stdout.(runtime.ExpectedOut), Stderr: t.Stderr.(runtime.ExpectedOut), }, - Nodes: t.Nodes, + Nodes: t.Config.Nodes, }) } @@ -152,7 +153,6 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { Stdout: y.convertToExpectedOut(v.Stdout), Stderr: y.convertToExpectedOut(v.Stderr), Config: y.mergeConfigs(v.Config, params.Config), - Nodes: v.Nodes, } // Set key as command, if command property was empty @@ -185,6 +185,7 @@ func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { Timeout: params.Config.Timeout, Retries: params.Config.Retries, Interval: params.Config.Interval, + Nodes: params.Config.Nodes, } return nil @@ -299,6 +300,10 @@ func (y *YAMLConfig) mergeConfigs(local YAMLTestConfig, global YAMLTestConfig) Y conf.InheritEnv = local.InheritEnv } + if len(local.Nodes) != 0 { + conf.Nodes = local.Nodes + } + return conf } diff --git a/pkg/suite/yaml_suite_test.go b/pkg/suite/yaml_suite_test.go index 58fcb6ea..fb29db0a 100644 --- a/pkg/suite/yaml_suite_test.go +++ b/pkg/suite/yaml_suite_test.go @@ -336,9 +336,10 @@ nodes: image: ubuntu:18.04 tests: echo hello: - nodes: - - docker-host - - ssh-host1 + config: + nodes: + - docker-host + - ssh-host1 exit-code: 0 `) From 5b92176cecbf2d2d099b0522709a5b16dbdf589b Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 13:48:29 +0100 Subject: [PATCH 20/23] Add unix and linux integration test targets to Makefile --- .travis.yml | 4 ++-- Makefile | 9 ++++++++- README.md | 10 ++++++++-- commander_linux.yaml | 12 ++++++++++++ commander_unix.yaml | 12 ------------ integration/{unix => linux}/nodes.yaml | 0 6 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 commander_linux.yaml rename integration/{unix => linux}/nodes.yaml (100%) diff --git a/.travis.yml b/.travis.yml index 564314cd..ec9448d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,7 @@ jobs: script: - curl -L https://github.com/SimonBaeumer/commander/releases/download/v0.3.0/commander-darwin-amd64 -o ~/bin/commander - chmod +x ~/bin/commander - - make integration + - make integration-unix - name: windows Unit os: windows @@ -68,7 +68,7 @@ jobs: - ./test-reporter after-build -t gocov --exit-code $TRAVIS_TEST_RESULT - name: Integration test - script: make integration + script: make integration-linux - stage: deploy name: "Deployment" diff --git a/Makefile b/Makefile index 26e4a76f..9d95707b 100644 --- a/Makefile +++ b/Makefile @@ -43,12 +43,19 @@ test-coverage-all: go test -coverprofile c.out ./... ./integration/teardown_unix.sh -integration: build +integration-unix: build $(info INFO: Starting build $@) ./integration/setup_unix.sh commander test commander_unix.yaml ./integration/teardown_unix.sh +integration-linux: build + $(info INFO: Starting build $@) + ./integration/setup_unix.sh + commander test commander_unix.yaml + commander test commander_linux.yaml + ./integration/teardown_unix.sh + integration-windows: build $(info INFO: Starting build $@) commander test commander_windows.yaml diff --git a/README.md b/README.md index 78a828fc..f35ebb76 100644 --- a/README.md +++ b/README.md @@ -729,8 +729,14 @@ $ make test-coverage # Coverage with more complex tests like ssh execution $ make test-coverage-all -# Integration tests -$ make integration +# Integration tests for linux and macos +$ make integration-unix + +# Integration on linux +$ make integration-linux + +# Integration windows +$ make integration-windows # Add depdencies to vendor $ make deps diff --git a/commander_linux.yaml b/commander_linux.yaml new file mode 100644 index 00000000..0a5d2e60 --- /dev/null +++ b/commander_linux.yaml @@ -0,0 +1,12 @@ +tests: + test nodes: + command: ./commander test integration/unix/nodes.yaml + stdout: + contains: + - ✓ [ssh-host] it should test ssh host + - ✓ [ssh-host] it should set env variable + - ✓ [ssh-host-default] it should be executed on ssh-host-default + - ✓ [ssh-host] it should test multiple hosts + - ✓ [ssh-host-default] it should test multiple hosts + - ✓ [local] it should test multiple hosts + exit-code: 0 \ No newline at end of file diff --git a/commander_unix.yaml b/commander_unix.yaml index 8df9ed09..c3f5cb93 100644 --- a/commander_unix.yaml +++ b/commander_unix.yaml @@ -65,15 +65,3 @@ tests: - ✓ [local] it should retry failed commands, retries 2 - ✗ [local] it should retry failed commands with an interval, retries 2 exit-code: 1 - - test nodes: - command: ./commander test integration/unix/nodes.yaml - stdout: - contains: - - ✓ [ssh-host] it should test ssh host - - ✓ [ssh-host] it should set env variable - - ✓ [ssh-host-default] it should be executed on ssh-host-default - - ✓ [ssh-host] it should test multiple hosts - - ✓ [ssh-host-default] it should test multiple hosts - - ✓ [local] it should test multiple hosts - exit-code: 0 \ No newline at end of file diff --git a/integration/unix/nodes.yaml b/integration/linux/nodes.yaml similarity index 100% rename from integration/unix/nodes.yaml rename to integration/linux/nodes.yaml From 2cf59b89f0c94bdfcf594472817be04fb9dfb361 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 14:09:41 +0100 Subject: [PATCH 21/23] Add src documentation --- pkg/runtime/executor.go | 1 + pkg/runtime/local_executor.go | 7 +++++++ pkg/runtime/runtime.go | 20 ++++++++++---------- pkg/runtime/ssh_executor.go | 28 ++++++++++++++++++++++++++++ pkg/suite/suite.go | 10 +++++++++- 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/pkg/runtime/executor.go b/pkg/runtime/executor.go index 3abe5932..c06b58c3 100644 --- a/pkg/runtime/executor.go +++ b/pkg/runtime/executor.go @@ -1,5 +1,6 @@ package runtime +// Executor interface which will be implemented by all available executors, like ssh or local type Executor interface { Execute(test TestCase) TestResult } diff --git a/pkg/runtime/local_executor.go b/pkg/runtime/local_executor.go index 373e55a6..aa35f1d7 100644 --- a/pkg/runtime/local_executor.go +++ b/pkg/runtime/local_executor.go @@ -6,9 +6,16 @@ import ( "strings" ) +// LocalExecutor will be used to execute tests on the local host type LocalExecutor struct { } +// NewLocalExecutor creates a new local executor +func NewLocalExecutor() Executor { + return LocalExecutor{} +} + +// Execute will execute the given test on the current node func (e LocalExecutor) Execute(test TestCase) TestResult { timeoutOpt, err := createTimeoutOption(test.Command.Timeout) if err != nil { diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index b59df1e0..be6cda04 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -28,6 +28,7 @@ const ( Skipped ) +// NewRuntime creates a new runtime and inits default nodes func NewRuntime(nodes ...Node) Runtime { local := Node{ Name: "local", @@ -41,6 +42,7 @@ func NewRuntime(nodes ...Node) Runtime { } } +// Runtime represents the current runtime, please use NewRuntime() instead of createing an instance directly type Runtime struct { Nodes []Node } @@ -54,6 +56,9 @@ type TestCase struct { Nodes []string } +// Node represents a configured node with everything needed to connect to the given host +// which is defined in the type property +// If the type is not available the test will fail and stop its execution type Node struct { Name string Type string @@ -147,7 +152,7 @@ func (r *Runtime) Start(tests []TestCase) <-chan TestResult { defer wg.Done() for t := range tests { - // If no node was set use local mode + // If no node was set use local mode as default if len(t.Nodes) == 0 { t.Nodes = []string{"local"} } @@ -183,23 +188,18 @@ func (r *Runtime) Start(tests []TestCase) <-chan TestResult { func (r *Runtime) getExecutor(node string) Executor { if len(r.Nodes) == 0 { - return LocalExecutor{} + return NewLocalExecutor() } for _, n := range r.Nodes { if n.Name == node { switch n.Type { case "ssh": - return SSHExecutor{ - Password: n.Pass, - IdentityFile: n.IdentityFile, - User: n.User, - Host: n.Addr, - } + return NewSSHExecutor(n.Addr, n.User, WithIdentityFile(n.IdentityFile), WithPassword(n.Pass)) case "local": - return LocalExecutor{} + return NewLocalExecutor() case "": - return LocalExecutor{} + return NewLocalExecutor() default: log.Fatal(fmt.Sprintf("Node type %s not found for node %s", n.Type, n.Name)) } diff --git a/pkg/runtime/ssh_executor.go b/pkg/runtime/ssh_executor.go index 7082353c..308f617b 100644 --- a/pkg/runtime/ssh_executor.go +++ b/pkg/runtime/ssh_executor.go @@ -18,6 +18,34 @@ type SSHExecutor struct { IdentityFile string } +// WithIdentityFile sets the identity file option for the ssh executor +func WithIdentityFile(identityFile string) func(e *SSHExecutor) { + return func(e *SSHExecutor) { + e.IdentityFile = identityFile + } +} + +// WithPassword sets the identity file option for the ssh executor +func WithPassword(pass string) func(e *SSHExecutor) { + return func(e *SSHExecutor) { + e.Password = pass + } +} + +// NewSSHExecutor creates a new executor +func NewSSHExecutor(host string, user string, opts ...func(e *SSHExecutor)) Executor { + e := SSHExecutor{ + Host: host, + User: user, + } + + for _, o := range opts { + o(&e) + } + + return e +} + // Execute executes a command on a remote host viá SSH func (e SSHExecutor) Execute(test TestCase) TestResult { if test.Command.InheritEnv { diff --git a/pkg/suite/suite.go b/pkg/suite/suite.go index a3d752f6..cbb7ee73 100644 --- a/pkg/suite/suite.go +++ b/pkg/suite/suite.go @@ -5,17 +5,21 @@ import ( "github.com/SimonBaeumer/commander/pkg/runtime" ) -// Suite represents the current tests and configs +// Suite represents the current tests, nodes and configs. +// It is used by the runtime to execute all tests and is an abstraction for the given source. +// In example it could be possible to add more formats like XML or a custom DSL implementation. type Suite struct { TestCases []runtime.TestCase Config runtime.TestConfig Nodes []runtime.Node } +// GetNodes returns all nodes defined in the suite func (s Suite) GetNodes() []runtime.Node { return s.Nodes } +// GetNodeByName returns a node by the given name func (s Suite) GetNodeByName(name string) (runtime.Node, error) { for _, n := range s.Nodes { if n.Name == name { @@ -25,6 +29,8 @@ func (s Suite) GetNodeByName(name string) (runtime.Node, error) { return runtime.Node{}, fmt.Errorf("could not find node with name %s", name) } +// AddTest pushes a new test to the suite +// if the test was already added it will panic func (s Suite) AddTest(t runtime.TestCase) { if _, err := s.GetTestByTitle(t.Title); err == nil { panic(fmt.Sprintf("Tests %s was already added to the suite", t.Title)) @@ -37,6 +43,7 @@ func (s Suite) GetTests() []runtime.TestCase { return s.TestCases } +// GetTestByTitle returns a test by title, if the test was not found an error is returned func (s Suite) GetTestByTitle(title string) (runtime.TestCase, error) { for _, t := range s.GetTests() { if t.Title == title { @@ -46,6 +53,7 @@ func (s Suite) GetTestByTitle(title string) (runtime.TestCase, error) { return runtime.TestCase{}, fmt.Errorf("could not find test %s", title) } +// GetGlobalConfig returns the global configuration which applies to the complete suite func (s Suite) GetGlobalConfig() runtime.TestConfig { return s.Config } From ef53add3c49f2ea562deb7785cd73a68a12696a2 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 14:09:52 +0100 Subject: [PATCH 22/23] Fix integration unix target --- Makefile | 2 -- commander_linux.yaml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9d95707b..e3057750 100644 --- a/Makefile +++ b/Makefile @@ -45,9 +45,7 @@ test-coverage-all: integration-unix: build $(info INFO: Starting build $@) - ./integration/setup_unix.sh commander test commander_unix.yaml - ./integration/teardown_unix.sh integration-linux: build $(info INFO: Starting build $@) diff --git a/commander_linux.yaml b/commander_linux.yaml index 0a5d2e60..9e28c7c3 100644 --- a/commander_linux.yaml +++ b/commander_linux.yaml @@ -1,6 +1,6 @@ tests: test nodes: - command: ./commander test integration/unix/nodes.yaml + command: ./commander test integration/lunix/nodes.yaml stdout: contains: - ✓ [ssh-host] it should test ssh host From 7935bac660f8e0239358645439c203d7e609d547 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2020 14:22:10 +0100 Subject: [PATCH 23/23] Fix path --- Makefile | 1 - commander_linux.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e3057750..8ca23ab8 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,6 @@ test-coverage: test-coverage-all: export COMMANDER_SSH_TEST = 1 test-coverage-all: export COMMANDER_TEST_SSH_HOST = localhost:2222 -#test-coverage-all: export COMMANDER_TEST_SSH_PASS = password test-coverage-all: export COMMANDER_TEST_SSH_USER = root test-coverage-all: export COMMANDER_TEST_SSH_IDENTITY_FILE = $(CWD)/integration/containers/ssh/id_rsa test-coverage-all: diff --git a/commander_linux.yaml b/commander_linux.yaml index 9e28c7c3..e1d0ff05 100644 --- a/commander_linux.yaml +++ b/commander_linux.yaml @@ -1,6 +1,6 @@ tests: test nodes: - command: ./commander test integration/lunix/nodes.yaml + command: ./commander test integration/linux/nodes.yaml stdout: contains: - ✓ [ssh-host] it should test ssh host