diff --git a/cloud/k8s/kubectl.go b/cloud/k8s/kubectl.go new file mode 100644 index 00000000..8c3bfff0 --- /dev/null +++ b/cloud/k8s/kubectl.go @@ -0,0 +1,89 @@ +package k8s + +import ( + "errors" + "fmt" + "kool-dev/kool/api" + "kool-dev/kool/cmd/builder" + "kool-dev/kool/cmd/shell" + "os" + "path/filepath" +) + +type K8S interface { + Authenticate(string, string) (string, error) + Kubectl(shell.PathChecker) (builder.Command, error) + Cleanup(shell.OutputWritter) +} + +type DefaultK8S struct { + apiExec api.ExecCall + resp *api.ExecResponse +} + +var authTempPath = "/tmp" + +// NewDefaultK8S returns a new pointer for DefaultK8S with dependencies +func NewDefaultK8S() *DefaultK8S { + return &DefaultK8S{ + apiExec: api.NewDefaultExecCall(), + } +} + +func (k *DefaultK8S) Authenticate(domain, service string) (cloudService string, err error) { + k.apiExec.Body().Set("domain", domain) + k.apiExec.Body().Set("service", service) + + if k.resp, err = k.apiExec.Call(); err != nil { + return + } + + if k.resp.Token == "" { + err = fmt.Errorf("failed to generate access credentials to cloud deploy") + return + } + + cloudService = k.resp.Path + + err = os.WriteFile(k.getTempCAPath(), []byte(k.resp.CA), os.ModePerm) + return +} + +func (k *DefaultK8S) Kubectl(looker shell.PathChecker) (kube builder.Command, err error) { + if k.resp == nil { + err = errors.New("calling kubectl but did not authenticate") + return + } + + kube = builder.NewCommand("kubectl") + + kube.AppendArgs("--server", k.resp.Server) + kube.AppendArgs("--token", k.resp.Token) + kube.AppendArgs("--namespace", k.resp.Namespace) + kube.AppendArgs("--certificate-authority", k.getTempCAPath()) + + if looker.LookPath(kube) != nil { + // we do not have 'kubectl' on current path... let's use a container! + kool := builder.NewCommand("kool") + kool.AppendArgs( + "docker", "--", + "-v", fmt.Sprintf("%s:%s", k.getTempCAPath(), k.getTempCAPath()), + "kooldev/toolkit:full", + kube.Cmd(), + ) + kool.AppendArgs(kube.Args()...) + kube = kool + } + + return +} + +func (k *DefaultK8S) Cleanup(out shell.OutputWritter) { + if err := os.Remove(k.getTempCAPath()); err != nil { + out.Warning("failed to clear up temporary file; error:", err.Error()) + } +} + +func (k *DefaultK8S) getTempCAPath() string { + return filepath.Join(authTempPath, ".kool-cluster-CA") +} diff --git a/cloud/k8s/kubectl_test.go b/cloud/k8s/kubectl_test.go new file mode 100644 index 00000000..83d6aec8 --- /dev/null +++ b/cloud/k8s/kubectl_test.go @@ -0,0 +1,156 @@ +package k8s + +import ( + "errors" + "kool-dev/kool/api" + "kool-dev/kool/cmd/shell" + "os" + "strings" + "testing" +) + +// fake api.ExecCall +type fakeExecCall struct { + api.DefaultEndpoint + + err error + resp *api.ExecResponse +} + +func (d *fakeExecCall) Call() (*api.ExecResponse, error) { + return d.resp, d.err +} + +func newFakeExecCall() *fakeExecCall { + return &fakeExecCall{ + DefaultEndpoint: *api.NewDefaultEndpoint(""), + } +} + +// fake shell.OutputWritter +type fakeOutputWritter struct { + warned []interface{} +} + +func (*fakeOutputWritter) Println(args ...interface{}) { +} + +func (*fakeOutputWritter) Printf(s string, args ...interface{}) { +} + +func (f *fakeOutputWritter) Warning(args ...interface{}) { + f.warned = append(f.warned, args...) +} + +func (*fakeOutputWritter) Success(args ...interface{}) { +} + +func TestNewDefaultK8S(t *testing.T) { + k := NewDefaultK8S() + if _, ok := k.apiExec.(*api.DefaultExecCall); !ok { + t.Error("invalid type on apiExec") + } +} + +func TestAuthenticate(t *testing.T) { + k := &DefaultK8S{ + apiExec: newFakeExecCall(), + } + + expectedErr := errors.New("call error") + k.apiExec.(*fakeExecCall).err = expectedErr + + if _, err := k.Authenticate("foo", "bar"); !errors.Is(err, expectedErr) { + t.Error("unexpected error return from Authenticate") + } + + k.apiExec.(*fakeExecCall).err = nil + k.apiExec.(*fakeExecCall).resp = &api.ExecResponse{ + Server: "server", + Namespace: "ns", + Path: "path", + Token: "", + CA: "ca", + } + + if _, err := k.Authenticate("foo", "bar"); !strings.Contains(err.Error(), "failed to generate access credentials") { + t.Errorf("unexpected error from DeployExec call: %v", err) + } + + k.apiExec.(*fakeExecCall).resp.Token = "token" + authTempPath = t.TempDir() + + if cloudService, err := k.Authenticate("foo", "bar"); err != nil { + t.Errorf("unexpected error from Authenticate call: %v", err) + } else if cloudService != "path" { + t.Errorf("unexpected cloudService return: %s", cloudService) + } +} + +func TestTempCAPath(t *testing.T) { + k := NewDefaultK8S() + + authTempPath = "fake-path" + + if !strings.Contains(k.getTempCAPath(), authTempPath) { + t.Error("missing authTempPath from temp CA path") + } +} + +func TestCleanup(t *testing.T) { + k := NewDefaultK8S() + + authTempPath = t.TempDir() + if err := os.WriteFile(k.getTempCAPath(), []byte("ca"), os.ModePerm); err != nil { + t.Fatal(err) + } + + fakeOut := &fakeOutputWritter{} + + k.Cleanup(fakeOut) + + if len(fakeOut.warned) != 0 { + t.Error("should not have warned on removing the file") + } + + authTempPath = t.TempDir() + "test" + k.Cleanup(fakeOut) + + if len(fakeOut.warned) != 2 { + t.Error("should have warned on removing the file once") + } +} + +func TestKubectl(t *testing.T) { + authTempPath = t.TempDir() + + k := &DefaultK8S{ + apiExec: newFakeExecCall(), + } + + k.apiExec.(*fakeExecCall).resp = &api.ExecResponse{ + Server: "server", + Namespace: "ns", + Path: "path", + Token: "token", + CA: "ca", + } + + fakeShell := &shell.FakeShell{} + + if _, err := k.Kubectl(fakeShell); !strings.Contains(err.Error(), "but did not auth") { + t.Error("should get error before authenticating") + } + + _, _ = k.Authenticate("foo", "bar") + + if cmd, _ := k.Kubectl(fakeShell); cmd.Cmd() != "kubectl" { + t.Error("should use kubectl") + } + + fakeShell.MockLookPath = errors.New("err") + + if cmd, _ := k.Kubectl(fakeShell); cmd.Cmd() != "kool" { + t.Error("should use kool") + } +} diff --git a/cmd/deploy.go b/cmd/deploy.go index b93599b0..0c04c5d1 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -57,6 +57,7 @@ func AddKoolDeploy(root *cobra.Command) { root.AddCommand(deployCmd) deployCmd.AddCommand(NewDeployExecCommand(NewKoolDeployExec())) deployCmd.AddCommand(NewDeployDestroyCommand(NewKoolDeployDestroy())) + deployCmd.AddCommand(NewDeployLogsCommand(NewKoolDeployLogs())) } // Execute runs the deploy logic. diff --git a/cmd/deploy_exec.go b/cmd/deploy_exec.go index 4e94b72a..34fe9cda 100644 --- a/cmd/deploy_exec.go +++ b/cmd/deploy_exec.go @@ -3,30 +3,30 @@ package cmd import ( "fmt" "kool-dev/kool/api" + "kool-dev/kool/cloud/k8s" "kool-dev/kool/cmd/builder" "kool-dev/kool/environment" - "os" - "path/filepath" "github.com/spf13/cobra" ) -var authTempPath = "/tmp" - // KoolDeployExec holds handlers and functions for using Deploy API type KoolDeployExec struct { DefaultKoolService + Flags *KoolDeployExecFlags + env environment.EnvStorage + cloud k8s.K8S +} - kubectl, kool builder.Command - - env environment.EnvStorage - apiExec api.ExecCall +// KoolDeployExecFlags holds flags to kool deploy exec command +type KoolDeployExecFlags struct { + Container string } -// NewDeployExecCommand initializes new kool deploy Cobra command -func NewDeployExecCommand(deployExec *KoolDeployExec) *cobra.Command { - return &cobra.Command{ - Use: "exec SERVICE COMMAND [--] [ARG...]", +// NewDeployExecCommand inits Cobra command for kool deploy exec +func NewDeployExecCommand(deployExec *KoolDeployExec) (cmd *cobra.Command) { + cmd = &cobra.Command{ + Use: "exec SERVICE [COMMAND] [--] [ARG...]", Short: "Execute a command inside a running service container deployed to Kool Cloud", Long: `After deploying an application to Kool Cloud using 'kool deploy', execute a COMMAND inside the specified SERVICE container (similar to an SSH session). @@ -36,25 +36,28 @@ Must use a KOOL_API_TOKEN environment variable for authentication.`, DisableFlagsInUseLine: true, } + + cmd.Flags().SetInterspersed(false) + cmd.Flags().StringVarP(&deployExec.Flags.Container, "container", "c", "default", "Container target.") + return } // NewKoolDeployExec creates a new pointer with default KoolDeployExec service dependencies func NewKoolDeployExec() *KoolDeployExec { return &KoolDeployExec{ *newDefaultKoolService(), - builder.NewCommand("kubectl"), - builder.NewCommand("kool"), + &KoolDeployExecFlags{"default"}, environment.NewEnvStorage(), - api.NewDefaultExecCall(), + k8s.NewDefaultK8S(), } } // Execute runs the deploy exec logic - integrating with Deploy API func (e *KoolDeployExec) Execute(args []string) (err error) { var ( - domain string - service string - resp *api.ExecResponse + domain, service, cloudService string + + kubectl builder.Command ) if len(args) == 0 { @@ -74,57 +77,28 @@ func (e *KoolDeployExec) Execute(args []string) (err error) { return } - e.apiExec.Body().Set("domain", domain) - e.apiExec.Body().Set("service", service) - - if resp, err = e.apiExec.Call(); err != nil { + if cloudService, err = e.cloud.Authenticate(domain, service); err != nil { return } - if resp.Token == "" { - err = fmt.Errorf("failed to generate access credentials to cloud deploy") - return - } + defer e.cloud.Cleanup(e) - CAPath := filepath.Join(authTempPath, ".kool-cluster-CA") - defer func() { - if err := os.Remove(CAPath); err != nil { - e.Warning("failed to clear up temporary file; error:", err.Error()) - } - }() - if err = os.WriteFile(CAPath, []byte(resp.CA), os.ModePerm); err != nil { + if kubectl, err = e.cloud.Kubectl(e); err != nil { return } - e.kubectl.AppendArgs("--server", resp.Server) - e.kubectl.AppendArgs("--token", resp.Token) - e.kubectl.AppendArgs("--namespace", resp.Namespace) - e.kubectl.AppendArgs("--certificate-authority", CAPath) - e.kubectl.AppendArgs("exec", "-i") + // finish building exec command + kubectl.AppendArgs("exec", "-i") if e.IsTerminal() { - e.kubectl.AppendArgs("-t") + kubectl.AppendArgs("-t") } - e.kubectl.AppendArgs(resp.Path, "--") + kubectl.AppendArgs(cloudService, "-c", e.Flags.Container) + kubectl.AppendArgs("--") if len(args) == 0 { args = []string{"bash"} } - e.kubectl.AppendArgs(args...) - - if e.LookPath(e.kubectl) == nil { - // the command is available on current PATH, so let's use it - err = e.Interactive(e.kubectl) - return - } - - // we do not have 'kubectl' on current path... let's use a container! - e.kool.AppendArgs( - "docker", "--", - "-v", fmt.Sprintf("%s:%s", CAPath, CAPath), - "kooldev/toolkit:full", - e.kubectl.Cmd(), - ) - e.kool.AppendArgs(e.kubectl.Args()...) + kubectl.AppendArgs(args...) - err = e.Interactive(e.kool) + err = e.Interactive(kubectl) return } diff --git a/cmd/deploy_exec_test.go b/cmd/deploy_exec_test.go index e29729b3..fa0d2c5c 100644 --- a/cmd/deploy_exec_test.go +++ b/cmd/deploy_exec_test.go @@ -2,49 +2,30 @@ package cmd import ( "errors" - "kool-dev/kool/api" + "kool-dev/kool/cloud/k8s" "kool-dev/kool/cmd/builder" + "kool-dev/kool/cmd/shell" "kool-dev/kool/environment" "strings" "testing" ) -type fakeExecCall struct { - api.DefaultEndpoint - - err error - resp *api.ExecResponse -} - -func (d *fakeExecCall) Call() (*api.ExecResponse, error) { - return d.resp, d.err -} - func newFakeKoolDeployExec() *KoolDeployExec { return &KoolDeployExec{ *newFakeKoolService(), - &builder.FakeCommand{}, // kubectl - &builder.FakeCommand{}, // kool + &KoolDeployExecFlags{}, environment.NewFakeEnvStorage(), - &fakeExecCall{ - DefaultEndpoint: *api.NewDefaultEndpoint(""), - }, + &fakeK8S{}, } } func TestNewKoolDeployExec(t *testing.T) { e := NewKoolDeployExec() - if _, ok := e.kubectl.(*builder.DefaultCommand); !ok { - t.Errorf("unexpected type for kubectl command") - } - if _, ok := e.kool.(*builder.DefaultCommand); !ok { - t.Errorf("unexpected type for kool command") - } if _, ok := e.env.(*environment.DefaultEnvStorage); !ok { t.Errorf("unexpected type for env storage") } - if _, ok := e.apiExec.(*api.DefaultExecCall); !ok { + if _, ok := e.cloud.(*k8s.DefaultK8S); !ok { t.Errorf("unexpected type for apiExec endpoint") } } @@ -57,65 +38,53 @@ func TestKoolDeployExec(t *testing.T) { t.Errorf("expected: missing required parameter; got something else") } - var service string = "my-service" - err = e.Execute([]string{service}) + var args = []string{"my-service"} - if err == nil || !strings.Contains(err.Error(), "missing deploy domain") { + if err = e.Execute(args); err == nil || !strings.Contains(err.Error(), "missing deploy domain") { t.Errorf("expected: missing deploy domain; got something else") } var domain string = "example.com" e.env.Set("KOOL_DEPLOY_DOMAIN", domain) - e.apiExec.(*fakeExecCall).err = errors.New("call error") + mock := e.cloud.(*fakeK8S) + mock.MockAuthenticateErr = errors.New("auth error") - if err = e.Execute([]string{service}); !errors.Is(err, e.apiExec.(*fakeExecCall).err) { - t.Errorf("unexpected error from DeployExec call: %v", err) + if err = e.Execute(args); !errors.Is(err, mock.MockAuthenticateErr) { + t.Error("should return auth error") } - e.apiExec.(*fakeExecCall).err = nil - e.apiExec.(*fakeExecCall).resp = &api.ExecResponse{ - Server: "server", - Namespace: "ns", - Path: "path", - Token: "", - CA: "ca", - } + mock.MockAuthenticateErr = nil + mock.MockAuthenticateCloudService = "cloud-service" + mock.MockKubectlErr = errors.New("kube error") - if err = e.Execute([]string{service}); !strings.Contains(err.Error(), "failed to generate access credentials") { - t.Errorf("unexpected error from DeployExec call: %v", err) + if err = e.Execute(args); !errors.Is(err, mock.MockKubectlErr) { + t.Error("should return kube error") } - e.apiExec.(*fakeExecCall).resp.Token = "token" - authTempPath = t.TempDir() - - if err = e.Execute([]string{service, "foo", "bar"}); err != nil { - t.Errorf("unexpected error from DeployExec call: %v", err) - } + fakeKubectl := &builder.FakeCommand{} + mock.MockKubectlErr = nil + mock.MockKubectlKube = fakeKubectl - args := e.kubectl.(*builder.FakeCommand).ArgsAppend - if args[1] != "server" || args[3] != "token" || args[5] != "ns" || !strings.Contains(args[7], ".kool-cluster-CA") { - t.Errorf("unexpected arguments to kubectl: %v", args) - } + fakeKubectl.MockInteractiveError = errors.New("interactive error") - if len(e.kool.(*builder.FakeCommand).ArgsAppend) > 0 { - t.Errorf("should not have used kool") + if err = e.Execute(args); !errors.Is(err, fakeKubectl.MockInteractiveError) { + t.Error("should return interactive error") } - e.kubectl.(*builder.FakeCommand).MockLookPathError = errors.New("not found") - e.kubectl.(*builder.FakeCommand).MockCmd = "kub-foo" + fakeKubectl = &builder.FakeCommand{} + mock.MockKubectlKube = fakeKubectl + fakeKubectl.MockInteractiveError = nil + e.term.(*shell.FakeTerminalChecker).MockIsTerminal = true + e.Flags.Container = "foo" - if err = e.Execute([]string{service, "foo", "bar"}); err != nil { - t.Errorf("unexpected error from DeployExec call: %v", err) + if err = e.Execute(args); err != nil { + t.Error("unexpected error") } - args = e.kool.(*builder.FakeCommand).ArgsAppend - - if len(args) == 0 { - t.Errorf("should have used kool") - } + str := strings.Join(fakeKubectl.ArgsAppend, " ") - if args[5] != "kub-foo" { - t.Errorf("unexpected kubectl Cmd on kool: %v", args) + if !strings.Contains(str, "exec -i -t cloud-service -c foo -- bash") { + t.Error("bad kubectl command args") } } diff --git a/cmd/deploy_logs.go b/cmd/deploy_logs.go new file mode 100644 index 00000000..cb316691 --- /dev/null +++ b/cmd/deploy_logs.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "fmt" + "kool-dev/kool/api" + "kool-dev/kool/cloud/k8s" + "kool-dev/kool/cmd/builder" + "kool-dev/kool/environment" + + "github.com/spf13/cobra" +) + +// KoolDeployLogs holds handlers and functions for using Deploy API +type KoolDeployLogs struct { + DefaultKoolService + Flags *KoolDeployLogsFlags + env environment.EnvStorage + cloud k8s.K8S +} + +// KoolDeployLogsFlags holds flags to kool deploy logs command +type KoolDeployLogsFlags struct { + KoolLogsFlags + Container string +} + +// NewDeployLogsCommand inits Cobra command for kool deploy logs +func NewDeployLogsCommand(deployLogs *KoolDeployLogs) (cmd *cobra.Command) { + cmd = &cobra.Command{ + Use: "logs [OPTIONS] SERVICE", + Short: "See the logs of running service container deployed to Kool Cloud", + Long: `After deploying an application to Kool Cloud using 'kool deploy', +you can see the logs from the specified SERVICE container. +Must use a KOOL_API_TOKEN environment variable for authentication.`, + Args: cobra.ExactArgs(1), + Run: DefaultCommandRunFunction(deployLogs), + + DisableFlagsInUseLine: true, + } + + cmd.Flags().IntVarP(&deployLogs.Flags.Tail, "tail", "t", 25, "Number of lines to show from the end of the logs for each container. A value equal to 0 will show all lines.") + cmd.Flags().BoolVarP(&deployLogs.Flags.Follow, "follow", "f", false, "Follow log output.") + cmd.Flags().StringVarP(&deployLogs.Flags.Container, "container", "c", "default", "Container target.") + return +} + +// NewKoolDeployLogs creates a new pointer with default KoolDeployLogs service dependencies +func NewKoolDeployLogs() *KoolDeployLogs { + return &KoolDeployLogs{ + *newDefaultKoolService(), + &KoolDeployLogsFlags{KoolLogsFlags{25, false}, "default"}, + environment.NewEnvStorage(), + k8s.NewDefaultK8S(), + } +} + +// Execute runs the deploy logs logic - integrating with API and K8S +func (e *KoolDeployLogs) Execute(args []string) (err error) { + var ( + domain, service, cloudService string + + kubectl builder.Command + ) + + service = args[0] + + if url := e.env.Get("KOOL_API_URL"); url != "" { + api.SetBaseURL(url) + } + + if domain = e.env.Get("KOOL_DEPLOY_DOMAIN"); domain == "" { + err = fmt.Errorf("missing deploy domain (env KOOL_DEPLOY_DOMAIN)") + return + } + + if cloudService, err = e.cloud.Authenticate(domain, service); err != nil { + return + } + + defer e.cloud.Cleanup(e) + + if kubectl, err = e.cloud.Kubectl(e); err != nil { + return + } + + kubectl.AppendArgs("logs") + if e.Flags.Follow { + kubectl.AppendArgs("-f") + } + if e.Flags.Tail > 0 { + kubectl.AppendArgs("--tail", fmt.Sprintf("%d", e.Flags.Tail)) + } + kubectl.AppendArgs(cloudService, "-c", e.Flags.Container) + + err = e.Interactive(kubectl) + return +} diff --git a/cmd/deploy_logs_test.go b/cmd/deploy_logs_test.go new file mode 100644 index 00000000..06a3f25a --- /dev/null +++ b/cmd/deploy_logs_test.go @@ -0,0 +1,145 @@ +package cmd + +import ( + "errors" + "kool-dev/kool/cloud/k8s" + "kool-dev/kool/cmd/builder" + "kool-dev/kool/cmd/shell" + "kool-dev/kool/environment" + "strings" + "testing" +) + +type fakeK8S struct { + // Authenticate + CalledAuthenticate bool + CalledAuthenticateParamDomain string + CalledAuthenticateParamService string + MockAuthenticateCloudService string + MockAuthenticateErr error + + // Kubectl + CalledKubectl bool + CalledKubectlParamLooker shell.PathChecker + MockKubectlKube builder.Command + MockKubectlErr error + + // Cleanup + CalledCleanup bool + CalledCleanupParamOut shell.OutputWritter +} + +func (f *fakeK8S) Authenticate(domain, service string) (cloudService string, err error) { + f.CalledAuthenticate = true + f.CalledAuthenticateParamDomain = domain + f.CalledAuthenticateParamService = service + + cloudService = f.MockAuthenticateCloudService + err = f.MockAuthenticateErr + return +} + +func (f *fakeK8S) Kubectl(looker shell.PathChecker) (kube builder.Command, err error) { + f.CalledKubectl = true + f.CalledKubectlParamLooker = looker + kube = f.MockKubectlKube + err = f.MockKubectlErr + return +} + +func (f *fakeK8S) Cleanup(out shell.OutputWritter) { + f.CalledCleanup = true + f.CalledCleanupParamOut = out +} + +func fakeKoolDeployLogs() *KoolDeployLogs { + return &KoolDeployLogs{ + *newFakeKoolService(), + &KoolDeployLogsFlags{}, + environment.NewFakeEnvStorage(), + &fakeK8S{}, + } +} + +func TestNewKoolDeployLogs(t *testing.T) { + l := NewKoolDeployLogs() + + if l.Flags.Follow { + t.Error("unexpected default Follow behaviour") + } + if l.Flags.Tail != 25 { + t.Error("unexpected default Tail behaviour") + } + if _, ok := l.env.(*environment.DefaultEnvStorage); !ok { + t.Error("bad default type for env storage") + } + if _, ok := l.env.(*environment.DefaultEnvStorage); !ok { + t.Error("bad default type for env storage") + } + if _, ok := l.cloud.(*k8s.DefaultK8S); !ok { + t.Error("bad default type for k8s cloud") + } +} + +func TestNewDeployLogsCommand(t *testing.T) { + cmd := NewDeployLogsCommand(fakeKoolDeployLogs()) + + if cmd.Flags().Lookup("tail") == nil { + t.Error("missing flag: tailt") + } + + if cmd.Flags().Lookup("follow") == nil { + t.Error("missing flag: tailt") + } +} + +func TestKoolDeployLogsExecute(t *testing.T) { + l := fakeKoolDeployLogs() + args := []string{"foo"} + + l.env.Set("KOOL_API_URL", "api-url") + + if err := l.Execute(args); !strings.Contains(err.Error(), "missing deploy domain") { + t.Error("should get error on missing domain") + } + + l.env.Set("KOOL_DEPLOY_DOMAIN", "deploy.domain") + + l.cloud.(*fakeK8S).MockAuthenticateErr = errors.New("authenticate error") + + if err := l.Execute(args); !errors.Is(err, l.cloud.(*fakeK8S).MockAuthenticateErr) { + t.Error("should get error on authenticate") + } + + l.cloud.(*fakeK8S).MockAuthenticateErr = nil + l.cloud.(*fakeK8S).MockAuthenticateCloudService = "app" + l.cloud.(*fakeK8S).MockKubectlErr = errors.New("kubectl error") + + if err := l.Execute(args); !errors.Is(err, l.cloud.(*fakeK8S).MockKubectlErr) { + t.Error("should get error on kubectl") + } + + l.cloud.(*fakeK8S).MockKubectlErr = nil + l.cloud.(*fakeK8S).MockKubectlKube = &builder.FakeCommand{ + MockInteractiveError: errors.New("interactive error"), + } + + if err := l.Execute(args); !errors.Is(err, l.cloud.(*fakeK8S).MockKubectlKube.(*builder.FakeCommand).MockInteractiveError) { + t.Error("should get error on kubectl - interactive") + } + + fakeKubectl := &builder.FakeCommand{} + l.cloud.(*fakeK8S).MockKubectlKube = fakeKubectl + l.Flags.Follow = true + l.Flags.Tail = 25 + + if err := l.Execute(args); err != nil { + t.Error("unexpected error") + } + + str := strings.Join(fakeKubectl.ArgsAppend, " ") + + if !strings.Contains(str, "logs -f --tail 25") { + t.Error("bad kubectl command - missing logs -f : " + str) + } +} diff --git a/cmd/shell/fake_shell.go b/cmd/shell/fake_shell.go index 6af0d1dd..5240089a 100644 --- a/cmd/shell/fake_shell.go +++ b/cmd/shell/fake_shell.go @@ -31,6 +31,7 @@ type FakeShell struct { MockOutStream io.Writer MockErrStream io.Writer MockInStream io.Reader + MockLookPath error } // InStream is a mocked testing function @@ -111,8 +112,11 @@ func (f *FakeShell) LookPath(command builder.Command) (err error) { if _, ok := command.(*builder.FakeCommand); ok { err = command.(*builder.FakeCommand).MockLookPathError + return } + err = f.MockLookPath + return } diff --git a/cmd/shell/fake_shell_test.go b/cmd/shell/fake_shell_test.go index 477d2a3a..23951da2 100644 --- a/cmd/shell/fake_shell_test.go +++ b/cmd/shell/fake_shell_test.go @@ -79,6 +79,15 @@ func TestFakeShell(t *testing.T) { t.Error("failed to use mocked LookPath function on FakeShell") } + if val, ok := f.CalledLookPath["cmd"]; !val || !ok || lookPathError != command.MockLookPathError { + t.Error("failed to use mocked LookPath function on FakeShell") + } + + f.MockLookPath = errors.New("mock look path err") + if err := f.LookPath(builder.NewCommand("")); !errors.Is(err, f.MockLookPath) { + t.Error("failed returning MockLookPath") + } + f.Println() if !f.CalledPrintln { diff --git a/cmd/shell/shell.go b/cmd/shell/shell.go index b8849254..ca969481 100644 --- a/cmd/shell/shell.go +++ b/cmd/shell/shell.go @@ -37,8 +37,22 @@ type DefaultShell struct { env environment.EnvStorage } +// OutputWritter implements basic output for CLIss +type OutputWritter interface { + Println(...interface{}) + Printf(string, ...interface{}) + Warning(...interface{}) + Success(...interface{}) +} + +type PathChecker interface { + LookPath(builder.Command) error +} + // Shell implements functions for handling a shell type Shell interface { + OutputWritter + PathChecker InStream() io.Reader SetInStream(io.Reader) OutStream() io.Writer @@ -47,12 +61,7 @@ type Shell interface { SetErrStream(io.Writer) Exec(builder.Command, ...string) (string, error) Interactive(builder.Command, ...string) error - LookPath(builder.Command) error - Println(...interface{}) - Printf(string, ...interface{}) Error(error) - Warning(...interface{}) - Success(...interface{}) } // NewShell creates a new shell