From 3094e6191fcc72004b3f2352b337770a6678e3cf Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 25 Apr 2020 01:37:31 -0700 Subject: [PATCH 01/33] Add `opsani app console` --- command/app.go | 42 ++++++++++++++++++++++++++++++++++++++++++ command/app_test.go | 19 ++++++++++++++++++- opsani/config.go | 8 ++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/command/app.go b/command/app.go index 8b96d63..1af4c1c 100644 --- a/command/app.go +++ b/command/app.go @@ -15,6 +15,12 @@ package command import ( + "fmt" + "log" + "os/exec" + "runtime" + + "github.com/opsani/cli/opsani" "github.com/spf13/cobra" ) @@ -44,5 +50,41 @@ func NewAppCommand() *cobra.Command { // Config appCmd.AddCommand(appConfigCmd) + appCmd.AddCommand(NewAppConsoleCommand()) + return appCmd } + +// NewAppConsoleCommand returns a command that opens the Opsani Console +// in the default browser +func NewAppConsoleCommand() *cobra.Command { + return &cobra.Command{ + Use: "console", + Short: "Open the Opsani console in the default web browser", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + org, appID := opsani.GetAppComponents() + url := fmt.Sprintf("https://console.opsani.com/accounts/%s/applications/%s", org, appID) + openURLInDefaultBrowser(url) + return nil + }, + } +} + +func openURLInDefaultBrowser(url string) { + var err error + + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + if err != nil { + log.Fatal(err) + } +} diff --git a/command/app_test.go b/command/app_test.go index 5fed1b9..bd1fece 100644 --- a/command/app_test.go +++ b/command/app_test.go @@ -36,8 +36,25 @@ func (s *AppTestSuite) SetupTest() { s.SetCommand(command.NewRootCommand()) } -func (s *AppTestSuite) TestRunningAppStartHelp() { +func (s *AppTestSuite) TestRunningApp() { + output, err := s.Execute("app") + s.Require().NoError(err) + s.Require().Contains(output, "Manage apps") + s.Require().Contains(output, "Usage:") +} + +func (s *AppTestSuite) TestRunningAppHelp() { output, err := s.Execute("app", "--help") s.Require().NoError(err) s.Require().Contains(output, "Manage apps") } + +func (s *AppTestSuite) TestRunningAppConsoleHelp() { + output, err := s.Execute("app", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Open the Opsani console") +} + +func TestRunningAppConsle(t *testing.T) { + t.Skip("Pending test for launching browser") +} diff --git a/opsani/config.go b/opsani/config.go index 83f0312..3be4c34 100644 --- a/opsani/config.go +++ b/opsani/config.go @@ -90,6 +90,14 @@ func SetApp(app string) { viper.Set(KeyApp, app) } +// GetAppComponents returns the organization name and app ID as separate path components +func GetAppComponents() (orgSlug string, appSlug string) { + app := GetApp() + org := filepath.Dir(app) + appID := filepath.Base(app) + return org, appID +} + // GetAllSettings returns all configuration settings func GetAllSettings() map[string]interface{} { return viper.AllSettings() From 8d88389d209b459a9b69fba44a3e327dde416b9b Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 25 Apr 2020 04:08:53 -0700 Subject: [PATCH 02/33] Tricky for help with embedded structs --- command/command.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/command/command.go b/command/command.go index 057d043..66172ed 100644 --- a/command/command.go +++ b/command/command.go @@ -171,17 +171,19 @@ func NewCommandWithCobraCommand(cobraCommand *cobra.Command, configFunc func(*Co } return nil } - cobraCommand.Run = func(cmd *cobra.Command, args []string) { - if opsaniCommand.Run != nil { - opsaniCommand.Run(opsaniCommand, args) - } - } - cobraCommand.RunE = func(cmd *cobra.Command, args []string) error { + // NOTE: We expect errors + cobraCommand.Run = nil + defer func() { if opsaniCommand.RunE != nil { - return opsaniCommand.RunE(opsaniCommand, args) + cobraCommand.RunE = func(cmd *cobra.Command, args []string) error { + if opsaniCommand.RunE != nil { + return opsaniCommand.RunE(opsaniCommand, args) + } + return cobraCommand.Usage() + } } - return nil - } + }() + cobraCommand.PostRun = func(cmd *cobra.Command, args []string) { if opsaniCommand.PostRun != nil { opsaniCommand.PostRun(opsaniCommand, args) From fcc494abab749b85bea28ebe5eb2b5c649e2b54a Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 25 Apr 2020 04:09:32 -0700 Subject: [PATCH 03/33] Servo SSH beginnings --- command/servo.go | 190 ++++++++++++++++++++++++++++++++++++++++++ command/servo_test.go | 62 ++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 command/servo.go create mode 100644 command/servo_test.go diff --git a/command/servo.go b/command/servo.go new file mode 100644 index 0000000..639a588 --- /dev/null +++ b/command/servo.go @@ -0,0 +1,190 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "fmt" + "net" + "os" + + "github.com/prometheus/common/log" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + "golang.org/x/crypto/ssh/terminal" +) + +// NewServoCommand returns a new instance of the servo command +func NewServoCommand() *Command { + servoCmd := NewCommandWithCobraCommand(&cobra.Command{ + Use: "servo", + Short: "Manage Servos", + Args: cobra.NoArgs, + }, func(cmd *Command) { + cmd.PersistentPreRunE = ReduceRunEFuncsO(InitConfigRunE, RequireConfigFileFlagToExistRunE, RequireInitRunE) + }) + + servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + Use: "ssh", + Short: "SSH into a Servo", + // Args: cobra.ExactArgs(1), + }, func(cmd *Command) { + cmd.RunE = RunServoSSH + }).Command) + + servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + Use: "logs", + Short: "View logs on a Servo", + // Args: cobra.ExactArgs(1), + }, func(cmd *Command) { + cmd.RunE = RunServoLogs + }).Command) + + return servoCmd +} + +const username = "root" +const hostname = "3.93.217.12" +const port = "22" + +const sshKey = ` +-----BEGIN OPENSSH PRIVATE KEY----- +FAKE KEY +-----END OPENSSH PRIVATE KEY----- +` + +func SSHAgent() ssh.AuthMethod { + if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { + return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) + } + return nil +} + +func runInSSHSession(ctx context.Context, runIt func(context.Context, *ssh.Session) error) error { + // TODO: Recover from passphrase error + // // signer, err := ssh.ParsePrivateKey([]byte(sshKey)) + // signer, err := ssh.ParsePrivateKey([]byte(sshKey)) + // signer, err := ssh.ParsePrivateKeyWithPassphrase([]byte(sshKey), []byte("THIS_IS_NOT_A_PASSPHRASE")) + // if err != nil { + // log.Base().Fatal(err) + // } + // fmt.Printf("Got signer %+v\n\n", signer) + // key, err := x509.ParsePKCS1PrivateKey(der) + // if err != nil { + // log.Fatal(err) + // } + // signer := ssh.NewSignerFromKey(key) + + // SSH client config + config := &ssh.ClientConfig{ + User: username, + // Auth: []ssh.AuthMethod{ + // // ssh.Password(password), + // ssh.PublicKeys(signer), + // }, + Auth: []ssh.AuthMethod{ + SSHAgent(), + }, + // Non-production only + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + // // Connect to host + client, err := ssh.Dial("tcp", hostname+":"+port, config) + if err != nil { + log.Fatal(err) + } + defer client.Close() + + // Create sesssion + session, err := client.NewSession() + if err != nil { + log.Fatal("Failed to create session: ", err) + } + defer session.Close() + + go func() { + <-ctx.Done() + client.Close() + }() + + return runIt(ctx, session) +} + +func RunServoLogs(cmd *Command, args []string) error { + ctx := context.Background() + return runInSSHSession(ctx, RunLogsSSHSession) +} + +// RunConfig displays Opsani CLI config info +func RunServoSSH(cmd *Command, args []string) error { + ctx := context.Background() + return runInSSHSession(ctx, RunShellOnSSHSession) +} + +func RunLogsSSHSession(ctx context.Context, session *ssh.Session) error { + session.Stdout = os.Stdout + session.Stderr = os.Stderr + return session.Run("cd /root/dev.opsani.com/blake/oco && docker-compose logs -f --tail=100") +} + +func RunShellOnSSHSession(ctx context.Context, session *ssh.Session) error { + fd := int(os.Stdin.Fd()) + state, err := terminal.MakeRaw(fd) + if err != nil { + return fmt.Errorf("terminal make raw: %s", err) + } + defer terminal.Restore(fd, state) + + w, h, err := terminal.GetSize(fd) + if err != nil { + return fmt.Errorf("terminal get size: %s", err) + } + + modes := ssh.TerminalModes{ + ssh.ECHO: 1, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, + } + + term := os.Getenv("TERM") + if term == "" { + term = "xterm-256color" + } + if err := session.RequestPty(term, h, w, modes); err != nil { + return fmt.Errorf("session xterm: %s", err) + } + + session.Stdout = os.Stdout + session.Stderr = os.Stderr + session.Stdin = os.Stdin + + if err := session.Shell(); err != nil { + return fmt.Errorf("session shell: %s", err) + } + + if err := session.Wait(); err != nil { + if e, ok := err.(*ssh.ExitError); ok { + switch e.ExitStatus() { + case 130: + return nil + } + } + return fmt.Errorf("ssh: %s", err) + } + + return err +} diff --git a/command/servo_test.go b/command/servo_test.go new file mode 100644 index 0000000..d4ac0a7 --- /dev/null +++ b/command/servo_test.go @@ -0,0 +1,62 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command_test + +import ( + "testing" + + "github.com/opsani/cli/command" + "github.com/opsani/cli/test" + "github.com/spf13/viper" + "github.com/stretchr/testify/suite" +) + +type ServoTestSuite struct { + test.Suite +} + +func TestServoTestSuite(t *testing.T) { + suite.Run(t, new(ServoTestSuite)) +} + +func (s *ServoTestSuite) SetupTest() { + viper.Reset() + s.SetCommand(command.NewRootCommand()) +} + +func (s *ServoTestSuite) TestRunningServo() { + output, err := s.Execute("servo") + s.Require().NoError(err) + s.Require().Contains(output, "Manage Servos") + s.Require().Contains(output, "Usage:") +} + +func (s *ServoTestSuite) TestRunningServoHelp() { + output, err := s.Execute("servo", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Manage Servos") +} + +func (s *ServoTestSuite) TestRunningServoInvalidPositionalArg() { + output, err := s.Execute("servo", "--help", "sadasdsdas") + s.Require().NoError(err) + s.Require().Contains(output, "Manage Servos") +} + +func (s *ServoTestSuite) TestRunningServoSSHHelp() { + output, err := s.Execute("servo", "ssh", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "SSH into a Servo") +} From 2c8c4d9d9fac01c9123495958ee9441d0dd3068e Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 25 Apr 2020 05:49:25 -0700 Subject: [PATCH 04/33] Roughed in servo ssh and servo logs support --- command/root.go | 1 + command/servo.go | 112 ++++++++++++++++++++++++++++++++---------- command/servo_test.go | 28 +++++++++++ 3 files changed, 114 insertions(+), 27 deletions(-) diff --git a/command/root.go b/command/root.go index 53a330a..84a716c 100644 --- a/command/root.go +++ b/command/root.go @@ -78,6 +78,7 @@ We'd love to hear your feedback at `, rootCmd.AddCommand(NewConfigCommand().Command) rootCmd.AddCommand(NewCompletionCommand().Command) + rootCmd.AddCommand(NewServoCommand().Command) // See Execute() rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { diff --git a/command/servo.go b/command/servo.go index 639a588..92f171b 100644 --- a/command/servo.go +++ b/command/servo.go @@ -19,16 +19,28 @@ import ( "fmt" "net" "os" + "strings" + "github.com/mitchellh/go-homedir" "github.com/prometheus/common/log" "github.com/spf13/cobra" + "github.com/spf13/viper" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" + "golang.org/x/crypto/ssh/knownhosts" "golang.org/x/crypto/ssh/terminal" ) // NewServoCommand returns a new instance of the servo command func NewServoCommand() *Command { + logsCmd := NewCommandWithCobraCommand(&cobra.Command{ + Use: "logs", + Short: "View logs on a Servo", + Args: cobra.ExactArgs(1), + }, func(cmd *Command) { + cmd.RunE = RunServoLogs + }) + servoCmd := NewCommandWithCobraCommand(&cobra.Command{ Use: "servo", Short: "Manage Servos", @@ -40,31 +52,42 @@ func NewServoCommand() *Command { servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ Use: "ssh", Short: "SSH into a Servo", - // Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(1), }, func(cmd *Command) { cmd.RunE = RunServoSSH }).Command) - servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ - Use: "logs", - Short: "View logs on a Servo", - // Args: cobra.ExactArgs(1), - }, func(cmd *Command) { - cmd.RunE = RunServoLogs - }).Command) + servoCmd.AddCommand(logsCmd.Command) + + logsCmd.Flags().BoolP("follow", "f", false, "Follow log output") + viper.BindPFlag("follow", logsCmd.Flags().Lookup("follow")) + logsCmd.Flags().BoolP("timestamps", "t", false, "Show timestamps") + viper.BindPFlag("timestamps", logsCmd.Flags().Lookup("timestamps")) + logsCmd.Flags().StringP("lines", "l", "25", `Number of lines to show from the end of the logs (or "all").`) + viper.BindPFlag("lines", logsCmd.Flags().Lookup("lines")) return servoCmd } -const username = "root" -const hostname = "3.93.217.12" -const port = "22" - -const sshKey = ` ------BEGIN OPENSSH PRIVATE KEY----- -FAKE KEY ------END OPENSSH PRIVATE KEY----- -` +// const username = "root" +// const hostname = "3.93.217.12" +// const port = "22" + +// const sshKey = ` +// -----BEGIN OPENSSH PRIVATE KEY----- +// FAKE KEY +// -----END OPENSSH PRIVATE KEY----- +// ` + +var servos = []map[string]string{ + { + "name": "opsani-dev", + "host": "3.93.217.12", + "port": "22", + "user": "root", + "path": "/root/dev.opsani.com/blake/oco", + }, +} func SSHAgent() ssh.AuthMethod { if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { @@ -73,7 +96,8 @@ func SSHAgent() ssh.AuthMethod { return nil } -func runInSSHSession(ctx context.Context, runIt func(context.Context, *ssh.Session) error) error { +func runInSSHSession(ctx context.Context, name string, runIt func(context.Context, map[string]string, *ssh.Session) error) error { + // TODO: Recover from passphrase error // // signer, err := ssh.ParsePrivateKey([]byte(sshKey)) // signer, err := ssh.ParsePrivateKey([]byte(sshKey)) @@ -88,9 +112,28 @@ func runInSSHSession(ctx context.Context, runIt func(context.Context, *ssh.Sessi // } // signer := ssh.NewSignerFromKey(key) + var servo map[string]string + for _, s := range servos { + if s["name"] == name { + servo = s + break + } + } + if len(servo) == 0 { + return fmt.Errorf("no such Servo %q", name) + } + // SSH client config + knownHosts, err := homedir.Expand("~/.ssh/known_hosts") + if err != nil { + return err + } + hostKeyCallback, err := knownhosts.New(knownHosts) + if err != nil { + log.Fatal("could not create hostkeycallback function: ", err) + } config := &ssh.ClientConfig{ - User: username, + User: servo["user"], // Auth: []ssh.AuthMethod{ // // ssh.Password(password), // ssh.PublicKeys(signer), @@ -99,11 +142,12 @@ func runInSSHSession(ctx context.Context, runIt func(context.Context, *ssh.Sessi SSHAgent(), }, // Non-production only - HostKeyCallback: ssh.InsecureIgnoreHostKey(), + HostKeyCallback: hostKeyCallback, } // // Connect to host - client, err := ssh.Dial("tcp", hostname+":"+port, config) + fmt.Printf("Servos: %+v\nServo=%+v\n\n", servos, servo) + client, err := ssh.Dial("tcp", servo["host"]+":"+servo["port"], config) if err != nil { log.Fatal(err) } @@ -121,27 +165,41 @@ func runInSSHSession(ctx context.Context, runIt func(context.Context, *ssh.Sessi client.Close() }() - return runIt(ctx, session) + return runIt(ctx, servo, session) } func RunServoLogs(cmd *Command, args []string) error { ctx := context.Background() - return runInSSHSession(ctx, RunLogsSSHSession) + return runInSSHSession(ctx, args[0], RunLogsSSHSession) } // RunConfig displays Opsani CLI config info func RunServoSSH(cmd *Command, args []string) error { ctx := context.Background() - return runInSSHSession(ctx, RunShellOnSSHSession) + return runInSSHSession(ctx, args[0], RunShellOnSSHSession) } -func RunLogsSSHSession(ctx context.Context, session *ssh.Session) error { +func RunLogsSSHSession(ctx context.Context, servo map[string]string, session *ssh.Session) error { session.Stdout = os.Stdout session.Stderr = os.Stderr - return session.Run("cd /root/dev.opsani.com/blake/oco && docker-compose logs -f --tail=100") + + args := []string{} + if path := servo["path"]; path != "" { + args = append(args, "cd", path+"&&") + } + args = append(args, "docker-compose logs") + args = append(args, "--tail "+viper.GetString("lines")) + if viper.GetBool("follow") { + args = append(args, "--follow") + } + if viper.GetBool("timestamps") { + args = append(args, "--timestamps") + } + fmt.Printf("args: %v\n", args) + return session.Run(strings.Join(args, " ")) } -func RunShellOnSSHSession(ctx context.Context, session *ssh.Session) error { +func RunShellOnSSHSession(ctx context.Context, servo map[string]string, session *ssh.Session) error { fd := int(os.Stdin.Fd()) state, err := terminal.MakeRaw(fd) if err != nil { diff --git a/command/servo_test.go b/command/servo_test.go index d4ac0a7..ba32845 100644 --- a/command/servo_test.go +++ b/command/servo_test.go @@ -60,3 +60,31 @@ func (s *ServoTestSuite) TestRunningServoSSHHelp() { s.Require().NoError(err) s.Require().Contains(output, "SSH into a Servo") } + +func (s *ServoTestSuite) TestRunningServoSSHInvalidServo() { + _, err := s.Execute("servo", "ssh", "fake-name") + s.Require().EqualError(err, `no such Servo "fake-name"`) +} + +func (s *ServoTestSuite) TestRunningServoLogsHelp() { + output, err := s.Execute("servo", "logs", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "View logs on a Servo") +} + +func (s *ServoTestSuite) TestRunningServoLogsInvalidServo() { + _, err := s.Execute("servo", "logs", "fake-name") + s.Require().EqualError(err, `no such Servo "fake-name"`) +} + +func (s *ServoTestSuite) TestRunningServoFollowHelp() { + output, err := s.Execute("servo", "logs", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Follow log output") +} + +func (s *ServoTestSuite) TestRunningLogsTimestampsHelp() { + output, err := s.Execute("servo", "logs", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Show timestamps") +} From 00e9ebfad3a8b38fc72e7d6aa062c278e60ea043 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 25 Apr 2020 06:21:57 -0700 Subject: [PATCH 05/33] Add servo start, stop, restart, status --- command/servo.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/command/servo.go b/command/servo.go index 92f171b..d51ccfb 100644 --- a/command/servo.go +++ b/command/servo.go @@ -57,6 +57,35 @@ func NewServoCommand() *Command { cmd.RunE = RunServoSSH }).Command) + servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + Use: "status", + Short: "Check Servo status", + Args: cobra.ExactArgs(1), + }, func(cmd *Command) { + cmd.RunE = RunServoStatus + }).Command) + servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + Use: "start", + Short: "Start the Servo", + Args: cobra.ExactArgs(1), + }, func(cmd *Command) { + cmd.RunE = RunServoStart + }).Command) + servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + Use: "stop", + Short: "Stop the Servo", + Args: cobra.ExactArgs(1), + }, func(cmd *Command) { + cmd.RunE = RunServoStop + }).Command) + servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + Use: "restart", + Short: "Restart Servo", + Args: cobra.ExactArgs(1), + }, func(cmd *Command) { + cmd.RunE = RunServoRestart + }).Command) + servoCmd.AddCommand(logsCmd.Command) logsCmd.Flags().BoolP("follow", "f", false, "Follow log output") @@ -168,6 +197,34 @@ func runInSSHSession(ctx context.Context, name string, runIt func(context.Contex return runIt(ctx, servo, session) } +func RunServoStatus(cmd *Command, args []string) error { + ctx := context.Background() + return runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return runDockerComposeOverSSH("ps", nil, servo, session) + }) +} + +func RunServoStart(cmd *Command, args []string) error { + ctx := context.Background() + return runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return runDockerComposeOverSSH("start", nil, servo, session) + }) +} + +func RunServoStop(cmd *Command, args []string) error { + ctx := context.Background() + return runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return runDockerComposeOverSSH("stop", nil, servo, session) + }) +} + +func RunServoRestart(cmd *Command, args []string) error { + ctx := context.Background() + return runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return runDockerComposeOverSSH("stop && docker-compse start", nil, servo, session) + }) +} + func RunServoLogs(cmd *Command, args []string) error { ctx := context.Background() return runInSSHSession(ctx, args[0], RunLogsSSHSession) @@ -179,6 +236,17 @@ func RunServoSSH(cmd *Command, args []string) error { return runInSSHSession(ctx, args[0], RunShellOnSSHSession) } +func runDockerComposeOverSSH(cmd string, args []string, servo map[string]string, session *ssh.Session) error { + session.Stdout = os.Stdout + session.Stderr = os.Stderr + + if path := servo["path"]; path != "" { + args = append(args, "cd", path+"&&") + } + args = append(args, "docker-compose", cmd) + return session.Run(strings.Join(args, " ")) +} + func RunLogsSSHSession(ctx context.Context, servo map[string]string, session *ssh.Session) error { session.Stdout = os.Stdout session.Stderr = os.Stderr @@ -195,7 +263,6 @@ func RunLogsSSHSession(ctx context.Context, servo map[string]string, session *ss if viper.GetBool("timestamps") { args = append(args, "--timestamps") } - fmt.Printf("args: %v\n", args) return session.Run(strings.Join(args, " ")) } From 4ae3853decd83cdef3287b782bd942ee46577bf1 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 25 Apr 2020 07:03:00 -0700 Subject: [PATCH 06/33] Add servo ls --- command/servo.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 4 ++++ 3 files changed, 49 insertions(+) diff --git a/command/servo.go b/command/servo.go index d51ccfb..832cf6c 100644 --- a/command/servo.go +++ b/command/servo.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/mitchellh/go-homedir" + "github.com/olekukonko/tablewriter" "github.com/prometheus/common/log" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -85,6 +86,14 @@ func NewServoCommand() *Command { }, func(cmd *Command) { cmd.RunE = RunServoRestart }).Command) + servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List Servos", + Args: cobra.NoArgs, + }, func(cmd *Command) { + cmd.RunE = RunServoList + }).Command) servoCmd.AddCommand(logsCmd.Command) @@ -197,6 +206,41 @@ func runInSSHSession(ctx context.Context, name string, runIt func(context.Contex return runIt(ctx, servo, session) } +func RunServoList(cmd *Command, args []string) error { + data := [][]string{} + + for _, servo := range servos { + name := servo["name"] + user := servo["user"] + host := servo["host"] + if port := servo["port"]; port != "" && port != "22" { + host = host + ":" + port + } + path := "~/" + if p := servo["path"]; p != "" { + path = p + } + data = append(data, []string{name, user, host, path}) + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"NAME", "USER", "HOST", "PATH"}) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") // pad with tabs + table.SetNoWhiteSpace(true) + table.AppendBulk(data) // Add Bulk Data + table.Render() + return nil +} + func RunServoStatus(cmd *Command, args []string) error { ctx := context.Background() return runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { diff --git a/go.mod b/go.mod index 3007d6c..ecab50b 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.2.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/olekukonko/tablewriter v0.0.4 github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/runc v0.1.1 // indirect github.com/pelletier/go-toml v1.7.0 // indirect diff --git a/go.sum b/go.sum index 8e2d0ca..c24fd5a 100644 --- a/go.sum +++ b/go.sum @@ -208,6 +208,8 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -229,6 +231,8 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= From 89c29575b6f9f18fd270b7d0af241f799bb0fa4a Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sat, 25 Apr 2020 22:23:02 -0700 Subject: [PATCH 07/33] Refactor command and config architecture to stop fighting against Golang idioms --- command/app.go | 21 ++-- command/app_config.go | 26 ++--- command/app_lifecycle.go | 16 +-- command/command.go | 235 +++++++++++++++++++++------------------ command/completion.go | 29 +++-- command/config.go | 29 ++--- command/discover.go | 8 +- command/init.go | 79 +++++++------ command/login.go | 44 ++++---- command/root.go | 201 +++++++++++++++++++++++---------- command/servo.go | 230 +++++++++++++++++++++++++++----------- command/servo_test.go | 18 ++- opsani/config.go | 119 -------------------- opsani/keys.go | 27 ----- test/suite.go | 10 +- 15 files changed, 587 insertions(+), 505 deletions(-) delete mode 100644 opsani/config.go delete mode 100644 opsani/keys.go diff --git a/command/app.go b/command/app.go index 1af4c1c..843f375 100644 --- a/command/app.go +++ b/command/app.go @@ -20,26 +20,25 @@ import ( "os/exec" "runtime" - "github.com/opsani/cli/opsani" "github.com/spf13/cobra" ) // NewAppCommand returns a new `opsani app` command instance -func NewAppCommand() *cobra.Command { +func NewAppCommand(baseCmd *BaseCommand) *cobra.Command { appCmd := &cobra.Command{ Use: "app", Short: "Manage apps", // All commands require an initialized client - PersistentPreRunE: InitConfigRunE, + PersistentPreRunE: baseCmd.InitConfigRunE, } // Initialize our subcommands - appStartCmd := NewAppStartCommand() - appStopCmd := NewAppStopCommand() - appRestartCmd := NewAppRestartCommand() - appStatusCmd := NewAppStatusCommand() - appConfigCmd := NewAppConfigCommand() + appStartCmd := NewAppStartCommand(baseCmd) + appStopCmd := NewAppStopCommand(baseCmd) + appRestartCmd := NewAppRestartCommand(baseCmd) + appStatusCmd := NewAppStatusCommand(baseCmd) + appConfigCmd := NewAppConfigCommand(baseCmd) // Lifecycle appCmd.AddCommand(appStartCmd) @@ -50,20 +49,20 @@ func NewAppCommand() *cobra.Command { // Config appCmd.AddCommand(appConfigCmd) - appCmd.AddCommand(NewAppConsoleCommand()) + appCmd.AddCommand(NewAppConsoleCommand(baseCmd)) return appCmd } // NewAppConsoleCommand returns a command that opens the Opsani Console // in the default browser -func NewAppConsoleCommand() *cobra.Command { +func NewAppConsoleCommand(baseCmd *BaseCommand) *cobra.Command { return &cobra.Command{ Use: "console", Short: "Open the Opsani console in the default web browser", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - org, appID := opsani.GetAppComponents() + org, appID := baseCmd.GetAppComponents() url := fmt.Sprintf("https://console.opsani.com/accounts/%s/applications/%s", org, appID) openURLInDefaultBrowser(url) return nil diff --git a/command/app_config.go b/command/app_config.go index 28805a5..83506f3 100644 --- a/command/app_config.go +++ b/command/app_config.go @@ -50,7 +50,7 @@ func openFileInEditor(filename string, editor string) error { } // NewAppConfigEditCommand returns a new Opsani CLI app config edit action -func NewAppConfigEditCommand() *cobra.Command { +func NewAppConfigEditCommand(baseCmd *BaseCommand) *cobra.Command { return &cobra.Command{ Use: "edit [PATH=VALUE ...]", Short: "Edit app config", @@ -64,7 +64,7 @@ func NewAppConfigEditCommand() *cobra.Command { filename := tempFile.Name() // Download config to temp - client := NewAPIClientFromConfig() + client := baseCmd.NewAPIClient() resp, err := client.GetConfig() if err != nil { return err @@ -120,13 +120,13 @@ func NewAppConfigEditCommand() *cobra.Command { } // NewAppConfigGetCommand returns a new Opsani CLI `app config get` action -func NewAppConfigGetCommand() *cobra.Command { +func NewAppConfigGetCommand(baseCmd *BaseCommand) *cobra.Command { return &cobra.Command{ Use: "get [PATH ...]", Short: "Get app config", Args: cobra.ArbitraryArgs, RunE: func(cmd *cobra.Command, args []string) error { - client := NewAPIClientFromConfig() + client := baseCmd.NewAPIClient() resp, err := client.GetConfig() if err != nil { return err @@ -193,13 +193,13 @@ func bodyForConfigUpdateWithArgs(args []string) (interface{}, error) { } // NewAppConfigSetCommand returns a new Opsani CLI `app config set` action -func NewAppConfigSetCommand() *cobra.Command { +func NewAppConfigSetCommand(baseCmd *BaseCommand) *cobra.Command { return &cobra.Command{ Use: "set [CONFIG]", Short: "Set app config", Args: RangeOfValidJSONArgs(0, 1), RunE: func(cmd *cobra.Command, args []string) error { - client := NewAPIClientFromConfig() + client := baseCmd.NewAPIClient() body, err := bodyForConfigUpdateWithArgs(args) if err != nil { return err @@ -215,14 +215,14 @@ func NewAppConfigSetCommand() *cobra.Command { } // NewAppConfigPatchCommand returns a new Opsani CLI `app config patch` action -func NewAppConfigPatchCommand() *cobra.Command { +func NewAppConfigPatchCommand(baseCmd *BaseCommand) *cobra.Command { return &cobra.Command{ Use: "patch [CONFIG]", Short: "Patch app config", Long: "Patch merges the incoming change into the existing configuration.", Args: RangeOfValidJSONArgs(0, 1), RunE: func(cmd *cobra.Command, args []string) error { - client := NewAPIClientFromConfig() + client := baseCmd.NewAPIClient() body, err := bodyForConfigUpdateWithArgs(args) if err != nil { return err @@ -246,16 +246,16 @@ var appConfig = struct { }{} // NewAppConfigCommand returns a new Opsani CLI `app config` action -func NewAppConfigCommand() *cobra.Command { +func NewAppConfigCommand(baseCmd *BaseCommand) *cobra.Command { appConfigCmd := &cobra.Command{ Use: "config", Short: "Manage app config", } - appConfigGetCmd := NewAppConfigGetCommand() - appConfigSetCmd := NewAppConfigSetCommand() - appConfigPatchCmd := NewAppConfigPatchCommand() - appConfigEditCmd := NewAppConfigEditCommand() + appConfigGetCmd := NewAppConfigGetCommand(baseCmd) + appConfigSetCmd := NewAppConfigSetCommand(baseCmd) + appConfigPatchCmd := NewAppConfigPatchCommand(baseCmd) + appConfigEditCmd := NewAppConfigEditCommand(baseCmd) appConfigCmd.AddCommand(appConfigGetCmd) appConfigCmd.AddCommand(appConfigSetCmd) diff --git a/command/app_lifecycle.go b/command/app_lifecycle.go index 79661a6..ce25cc4 100644 --- a/command/app_lifecycle.go +++ b/command/app_lifecycle.go @@ -17,13 +17,13 @@ package command import "github.com/spf13/cobra" // NewAppStartCommand returns an Opsani CLI command for starting the app -func NewAppStartCommand() *cobra.Command { +func NewAppStartCommand(baseCmd *BaseCommand) *cobra.Command { return &cobra.Command{ Use: "start", Short: "Start the app", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - client := NewAPIClientFromConfig() + client := baseCmd.NewAPIClient() if resp, err := client.StartApp(); err == nil { return PrettyPrintJSONResponse(resp) } else { @@ -34,13 +34,13 @@ func NewAppStartCommand() *cobra.Command { } // NewAppStopCommand returns an Opsani CLI command for stopping the app -func NewAppStopCommand() *cobra.Command { +func NewAppStopCommand(baseCmd *BaseCommand) *cobra.Command { return &cobra.Command{ Use: "stop", Short: "Stop the app", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - client := NewAPIClientFromConfig() + client := baseCmd.NewAPIClient() resp, err := client.StopApp() if err != nil { return err @@ -51,13 +51,13 @@ func NewAppStopCommand() *cobra.Command { } // NewAppRestartCommand returns an Opsani CLI command for restarting the app -func NewAppRestartCommand() *cobra.Command { +func NewAppRestartCommand(baseCmd *BaseCommand) *cobra.Command { return &cobra.Command{ Use: "restart", Short: "Restart the app", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - client := NewAPIClientFromConfig() + client := baseCmd.NewAPIClient() resp, err := client.RestartApp() if err != nil { return err @@ -68,13 +68,13 @@ func NewAppRestartCommand() *cobra.Command { } // NewAppStatusCommand returns an Opsani CLI command for retrieving status on the app -func NewAppStatusCommand() *cobra.Command { +func NewAppStatusCommand(baseCmd *BaseCommand) *cobra.Command { return &cobra.Command{ Use: "status", Short: "Check app status", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - client := NewAPIClientFromConfig() + client := baseCmd.NewAPIClient() resp, err := client.GetAppStatus() if err != nil { return err diff --git a/command/command.go b/command/command.go index 66172ed..cf60800 100644 --- a/command/command.go +++ b/command/command.go @@ -17,41 +17,19 @@ package command import ( "encoding/json" "fmt" + "io" + "net/url" "os" + "path/filepath" "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" "github.com/go-resty/resty/v2" "github.com/hokaccha/go-prettyjson" "github.com/spf13/cobra" + "github.com/spf13/viper" ) -// Command is a wrapper around cobra.Command that adds Opsani functionality -type Command struct { - *cobra.Command - - // Shadow all Cobra functions with Opsani equivalents - PersistentPreRun func(cmd *Command, args []string) - // PersistentPreRunE: PersistentPreRun but returns an error. - PersistentPreRunE func(cmd *Command, args []string) error - // PreRun: children of this command will not inherit. - PreRun func(cmd *Command, args []string) - // PreRunE: PreRun but returns an error. - PreRunE func(cmd *Command, args []string) error - // Run: Typically the actual work function. Most commands will only implement this. - Run func(cmd *Command, args []string) - // RunE: Run but returns an error. - RunE func(cmd *Command, args []string) error - // PostRun: run after the Run command. - PostRun func(cmd *Command, args []string) - // PostRunE: PostRun but returns an error. - PostRunE func(cmd *Command, args []string) error - // PersistentPostRun: children of this command will inherit and execute after PostRun. - PersistentPostRun func(cmd *Command, args []string) - // PersistentPostRunE: PersistentPostRun but returns an error. - PersistentPostRunE func(cmd *Command, args []string) error -} - // Survey method wrappers // Survey needs access to a file descriptor for configuring the terminal but Cobra wants to model // stdio as streams. @@ -63,8 +41,20 @@ func SetStdio(stdio terminal.Stdio) { globalStdio = stdio } +// BaseCommand is the foundational command structure for the Opsani CLI +// It contains the root command for Cobra and is designed for embedding +// into other command structures to add subcommand functionality +type BaseCommand struct { + rootCmd *cobra.Command + // viper *viper + + ConfigFile string + requestTracingEnabled bool + debugModeEnabled bool +} + // stdio is a test helper for returning terminal file descriptors usable by Survey -func (cmd *Command) stdio() terminal.Stdio { +func (cmd *BaseCommand) stdio() terminal.Stdio { if globalStdio != (terminal.Stdio{}) { return globalStdio } else { @@ -76,20 +66,69 @@ func (cmd *Command) stdio() terminal.Stdio { } } +// CobraCommand returns the Cobra instance underlying the Opsani CLI command +func (cmd *BaseCommand) CobraCommand() *cobra.Command { + return cmd.rootCmd +} + +// Viper returns the Viper configuration object underlying the Opsani CLI command +// func (cmd *BaseCommand) Viper() *viper { +// return viper +// } + +// Proxy the Cobra I/O methods for convenience + +// OutOrStdout returns output to stdout. +func (cmd *BaseCommand) OutOrStdout() io.Writer { + return cmd.rootCmd.OutOrStdout() +} + +// Print is a convenience method to Print to the defined output, fallback to Stderr if not set. +func (cmd *BaseCommand) Print(i ...interface{}) { + cmd.rootCmd.Print(i...) +} + +// Println is a convenience method to Println to the defined output, fallback to Stderr if not set. +func (cmd *BaseCommand) Println(i ...interface{}) { + cmd.rootCmd.Println(i...) +} + +// Printf is a convenience method to Printf to the defined output, fallback to Stderr if not set. +func (cmd *BaseCommand) Printf(format string, i ...interface{}) { + cmd.rootCmd.Printf(format, i...) +} + +// PrintErr is a convenience method to Print to the defined Err output, fallback to Stderr if not set. +func (cmd *BaseCommand) PrintErr(i ...interface{}) { + cmd.rootCmd.PrintErr(i...) +} + +// PrintErrln is a convenience method to Println to the defined Err output, fallback to Stderr if not set. +func (cmd *BaseCommand) PrintErrln(i ...interface{}) { + cmd.rootCmd.PrintErrln(i...) +} + +// PrintErrf is a convenience method to Printf to the defined Err output, fallback to Stderr if not set. +func (cmd *BaseCommand) PrintErrf(format string, i ...interface{}) { + cmd.rootCmd.PrintErrf(format, i...) +} + +// Proxy the Survey library to follow our output directives + // Ask is a wrapper for survey.AskOne that executes with the command's stdio -func (cmd *Command) Ask(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error { +func (cmd *BaseCommand) Ask(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error { stdio := cmd.stdio() return survey.Ask(qs, response, append(opts, survey.WithStdio(stdio.In, stdio.Out, stdio.Err))...) } // AskOne is a wrapper for survey.AskOne that executes with the command's stdio -func (cmd *Command) AskOne(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error { +func (cmd *BaseCommand) AskOne(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error { stdio := cmd.stdio() return survey.AskOne(p, response, append(opts, survey.WithStdio(stdio.In, stdio.Out, stdio.Err))...) } // PrettyPrintJSONObject prints the given object as pretty printed JSON -func (cmd *Command) PrettyPrintJSONObject(obj interface{}) error { +func (cmd *BaseCommand) PrettyPrintJSONObject(obj interface{}) error { s, err := prettyjson.Marshal(obj) if err != nil { return err @@ -99,7 +138,7 @@ func (cmd *Command) PrettyPrintJSONObject(obj interface{}) error { } // PrettyPrintJSONBytes prints the given byte array as pretty printed JSON -func (cmd *Command) PrettyPrintJSONBytes(bytes []byte) error { +func (cmd *BaseCommand) PrettyPrintJSONBytes(bytes []byte) error { s, err := prettyjson.Format(bytes) if err != nil { return err @@ -109,12 +148,12 @@ func (cmd *Command) PrettyPrintJSONBytes(bytes []byte) error { } // PrettyPrintJSONString prints the given string as pretty printed JSON -func (cmd *Command) PrettyPrintJSONString(str string) error { +func (cmd *BaseCommand) PrettyPrintJSONString(str string) error { return PrettyPrintJSONBytes([]byte(str)) } // PrettyPrintJSONResponse prints the given API response as pretty printed JSON -func (cmd *Command) PrettyPrintJSONResponse(resp *resty.Response) error { +func (cmd *BaseCommand) PrettyPrintJSONResponse(resp *resty.Response) error { if resp.IsSuccess() { if r := resp.Result(); r != nil { return PrettyPrintJSONObject(r) @@ -132,84 +171,68 @@ func (cmd *Command) PrettyPrintJSONResponse(resp *resty.Response) error { return PrettyPrintJSONObject(result) } -// ReduceRunEFuncsO reduces a list of Cobra run functions that return an error into a single aggregate run function -func ReduceRunEFuncsO(runFuncs ...RunEFunc) func(cmd *Command, args []string) error { - return func(cmd *Command, args []string) error { - for _, runFunc := range runFuncs { - if err := runFunc(cmd.Command, args); err != nil { - return err - } - } - return nil - } +// BaseURL returns the Opsani API base URL +func (cmd *BaseCommand) BaseURL() string { + return viper.GetString(KeyBaseURL) } -// NewCommandWithCobraCommand returns a new Opsani CLI command with a given Cobra command -func NewCommandWithCobraCommand(cobraCommand *cobra.Command, configFunc func(*Command)) *Command { - opsaniCommand := &Command{ - Command: cobraCommand, - } - cobraCommand.PersistentPreRun = func(cmd *cobra.Command, args []string) { - if opsaniCommand.PersistentPreRun != nil { - opsaniCommand.PersistentPreRun(opsaniCommand, args) - } - } - cobraCommand.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { - if opsaniCommand.PersistentPreRunE != nil { - return opsaniCommand.PersistentPreRunE(opsaniCommand, args) - } - return nil - } - cobraCommand.PreRun = func(cmd *cobra.Command, args []string) { - if opsaniCommand.PreRun != nil { - opsaniCommand.PreRun(opsaniCommand, args) - } +// BaseURLHostnameAndPort returns the hostname and port portion of Opsani base URL for summary display +func (cmd *BaseCommand) BaseURLHostnameAndPort() string { + u, err := url.Parse(cmd.BaseURL()) + if err != nil { + return cmd.GetBaseURL() } - cobraCommand.PreRunE = func(cmd *cobra.Command, args []string) error { - if opsaniCommand.PreRunE != nil { - return opsaniCommand.PreRunE(opsaniCommand, args) - } - return nil + baseURLDescription := u.Hostname() + if port := u.Port(); port != "" && port != "80" && port != "443" { + baseURLDescription = baseURLDescription + ":" + port } - // NOTE: We expect errors - cobraCommand.Run = nil - defer func() { - if opsaniCommand.RunE != nil { - cobraCommand.RunE = func(cmd *cobra.Command, args []string) error { - if opsaniCommand.RunE != nil { - return opsaniCommand.RunE(opsaniCommand, args) - } - return cobraCommand.Usage() - } - } - }() + return baseURLDescription +} - cobraCommand.PostRun = func(cmd *cobra.Command, args []string) { - if opsaniCommand.PostRun != nil { - opsaniCommand.PostRun(opsaniCommand, args) - } - } - cobraCommand.PostRunE = func(cmd *cobra.Command, args []string) error { - if opsaniCommand.PostRunE != nil { - return opsaniCommand.PostRunE(opsaniCommand, args) - } - return nil - } - cobraCommand.PersistentPostRun = func(cmd *cobra.Command, args []string) { - if opsaniCommand.PersistentPostRun != nil { - opsaniCommand.PersistentPostRun(opsaniCommand, args) - } - } - cobraCommand.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { - if opsaniCommand.PersistentPostRunE != nil { - return opsaniCommand.PersistentPostRunE(opsaniCommand, args) - } - return nil - } +// SetBaseURL sets the Opsani API base URL +func (cmd *BaseCommand) SetBaseURL(baseURL string) { + viper.Set(KeyBaseURL, baseURL) +} - if configFunc != nil { - configFunc(opsaniCommand) - } +// AccessToken returns the Opsani API access token +func (cmd *BaseCommand) AccessToken() string { + return viper.GetString(KeyToken) +} + +// SetAccessToken sets the Opsani API access token +func (cmd *BaseCommand) SetAccessToken(accessToken string) { + viper.Set(KeyToken, accessToken) +} + +// App returns the target Opsani app +func (cmd *BaseCommand) App() string { + return viper.GetString(KeyApp) +} + +// SetApp sets the target Opsani app +func (cmd *BaseCommand) SetApp(app string) { + viper.Set(KeyApp, app) +} + +// AppComponents returns the organization name and app ID as separate path components +func (cmd *BaseCommand) AppComponents() (orgSlug string, appSlug string) { + app := cmd.App() + org := filepath.Dir(app) + appID := filepath.Base(app) + return org, appID +} + +// AllSettings returns all configuration settings +func (cmd *BaseCommand) AllSettings() map[string]interface{} { + return viper.AllSettings() +} + +// DebugModeEnabled returns a boolean value indicating if debugging is enabled +func (cmd *BaseCommand) DebugModeEnabled() bool { + return cmd.debugModeEnabled +} - return opsaniCommand +// RequestTracingEnabled returns a boolean value indicating if request tracing is enabled +func (cmd *BaseCommand) RequestTracingEnabled() bool { + return cmd.requestTracingEnabled } diff --git a/command/completion.go b/command/completion.go index 536d766..3c925ca 100644 --- a/command/completion.go +++ b/command/completion.go @@ -25,26 +25,25 @@ import ( ) // NewCompletionCommand returns a new Opsani CLI cmpletion command instance -func NewCompletionCommand() *Command { - completionCmd := NewCommandWithCobraCommand(&cobra.Command{ +func NewCompletionCommand(baseCmd *BaseCommand) *cobra.Command { + completionCmd := &cobra.Command{ Use: "completion", Short: "Generate shell completion scripts", Long: `Generate shell completion scripts for Opsani CLI commands. - The output of this command will be computer code and is meant to be saved to a - file or immediately evaluated by an interactive shell. + The output of this command will be computer code and is meant to be saved to a + file or immediately evaluated by an interactive shell. - For example, for bash you could add this to your '~/.bash_profile': + For example, for bash you could add this to your '~/.bash_profile': - eval "$(gh completion -s bash)" + eval "$(gh completion -s bash)" - When installing Opsani CLI through a package manager, however, it's possible that - no additional shell configuration is necessary to gain completion support. For - Homebrew, see - `, - }, func(command *Command) { - command.PersistentPreRunE = nil - command.RunE = func(cmd *Command, args []string) error { + When installing Opsani CLI through a package manager, however, it's possible that + no additional shell configuration is necessary to gain completion support. For + Homebrew, see + `, + PersistentPreRunE: nil, + RunE: func(cmd *cobra.Command, args []string) error { shellType, err := cmd.Flags().GetString("shell") if err != nil { return err @@ -75,8 +74,8 @@ func NewCompletionCommand() *Command { default: return fmt.Errorf("unsupported shell type %q", shellType) } - } - }) + }, + } completionCmd.Flags().StringP("shell", "s", "", "Shell type: {bash|zsh|fish|powershell}") diff --git a/command/config.go b/command/config.go index 12f907d..7ac266b 100644 --- a/command/config.go +++ b/command/config.go @@ -15,24 +15,27 @@ package command import ( - "github.com/opsani/cli/opsani" "github.com/spf13/cobra" ) +type configCommand struct { + *BaseCommand +} + // NewConfigCommand returns a new instance of the root command for Opsani CLI -func NewConfigCommand() *Command { - return NewCommandWithCobraCommand(&cobra.Command{ - Use: "config", - Short: "Manages client configuration", - Args: cobra.NoArgs, - }, func(cmd *Command) { - cmd.RunE = RunConfig - cmd.PersistentPreRunE = ReduceRunEFuncsO(InitConfigRunE, RequireConfigFileFlagToExistRunE, RequireInitRunE) - }) +func NewConfigCommand(baseCmd *BaseCommand) *cobra.Command { + cfgCmd := configCommand{BaseCommand: baseCmd} + return &cobra.Command{ + Use: "config", + Short: "Manages client configuration", + Args: cobra.NoArgs, + RunE: cfgCmd.Run, + PersistentPreRunE: ReduceRunEFuncs(baseCmd.InitConfigRunE, baseCmd.RequireConfigFileFlagToExistRunE, baseCmd.RequireInitRunE), + } } // RunConfig displays Opsani CLI config info -func RunConfig(cmd *Command, args []string) error { - cmd.Println("Using config from:", opsani.ConfigFile) - return cmd.PrettyPrintJSONObject(opsani.GetAllSettings()) +func (configCmd *configCommand) Run(_ *cobra.Command, args []string) error { + configCmd.Println("Using config from:", configCmd.ConfigFile) + return configCmd.PrettyPrintJSONObject(configCmd.GetAllSettings()) } diff --git a/command/discover.go b/command/discover.go index 969267f..55a4bca 100644 --- a/command/discover.go +++ b/command/discover.go @@ -240,7 +240,7 @@ func runDiscoveryCommand(cmd *cobra.Command, args []string) error { return nil } -func newPullCommand() *cobra.Command { +func newPullCommand(baseCmd *BaseCommand) *cobra.Command { pullCmd := &cobra.Command{ Use: "pull", Short: "Pull a Docker image", @@ -265,7 +265,7 @@ func newPullCommand() *cobra.Command { return pullCmd } -func newDiscoverCommand() *cobra.Command { +func newDiscoverCommand(baseCmd *BaseCommand) *cobra.Command { discoverCmd := &cobra.Command{ Use: "discover", Short: "Build Servo assets through Kubernetes discovery", @@ -275,7 +275,7 @@ func newDiscoverCommand() *cobra.Command { Upon completion of discovery, manifests will be generated that can be used to build a Servo assembly image and deploy it to Kubernetes.`, Args: cobra.NoArgs, - PersistentPreRunE: InitConfigRunE, + PersistentPreRunE: baseCmd.InitConfigRunE, RunE: runDiscoveryCommand, } @@ -285,7 +285,7 @@ func newDiscoverCommand() *cobra.Command { return discoverCmd } -func newIMBCommand() *cobra.Command { +func newIMBCommand(baseCmd *BaseCommand) *cobra.Command { imbCmd := &cobra.Command{ Use: "imb", Short: "Run the intelligent manifest builder under Docker", diff --git a/command/init.go b/command/init.go index 74edb32..3156c14 100644 --- a/command/init.go +++ b/command/init.go @@ -22,30 +22,34 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" "github.com/mgutz/ansi" - "github.com/opsani/cli/opsani" "github.com/spf13/cobra" "github.com/spf13/viper" ) const confirmedArg = "confirmed" -// RunInitCommand initializes Opsani CLI config -func RunInitCommand(cmd *Command, args []string) error { - confirmed, err := cmd.Flags().GetBool(confirmedArg) - if err != nil { - return err - } +type initCommand struct { + *BaseCommand + + confirmed bool +} +/// +/// TODO: Swap out config accessors +/// + +// RunInitCommand initializes Opsani CLI config +func (initCmd *initCommand) RunInitCommand(_ *cobra.Command, args []string) error { // Handle reinitialization case overwrite := false - if _, err := os.Stat(opsani.ConfigFile); !os.IsNotExist(err) && !confirmed { - cmd.Println("Using config from:", opsani.ConfigFile) - cmd.PrettyPrintJSONObject(opsani.GetAllSettings()) + if _, err := os.Stat(initCmd.ConfigFile); !os.IsNotExist(err) && !initCmd.confirmed { + initCmd.Println("Using config from:", initCmd.ConfigFile) + initCmd.PrettyPrintJSONObject(initCmd.GetAllSettings()) prompt := &survey.Confirm{ - Message: fmt.Sprintf("Existing config found. Overwrite %s?", opsani.ConfigFile), + Message: fmt.Sprintf("Existing config found. Overwrite %s?", initCmd.ConfigFile), } - err := cmd.AskOne(prompt, &overwrite) + err := initCmd.AskOne(prompt, &overwrite) if err != nil { return err } @@ -53,65 +57,66 @@ func RunInitCommand(cmd *Command, args []string) error { return terminal.InterruptErr } } - app := opsani.GetApp() - token := opsani.GetAccessToken() + app := initCmd.App() + token := initCmd.AccessToken() whiteBold := ansi.ColorCode("white+b") if overwrite || app == "" { - err := cmd.AskOne(&survey.Input{ + err := initCmd.AskOne(&survey.Input{ Message: "Opsani app (i.e. domain.com/app):", - Default: opsani.GetApp(), + Default: app, }, &app, survey.WithValidator(survey.Required)) if err != nil { return err } } else { - cmd.Printf("%si %sApp: %s%s%s%s\n", ansi.Blue, whiteBold, ansi.Reset, ansi.LightCyan, app, ansi.Reset) + initCmd.Printf("%si %sApp: %s%s%s%s\n", ansi.Blue, whiteBold, ansi.Reset, ansi.LightCyan, app, ansi.Reset) } if overwrite || token == "" { - err := cmd.AskOne(&survey.Input{ + err := initCmd.AskOne(&survey.Input{ Message: "API Token:", - Default: opsani.GetAccessToken(), + Default: token, }, &token, survey.WithValidator(survey.Required)) if err != nil { return err } } else { - cmd.Printf("%si %sAPI Token: %s%s%s%s\n", ansi.Blue, whiteBold, ansi.Reset, ansi.LightCyan, token, ansi.Reset) + initCmd.Printf("%si %sAPI Token: %s%s%s%s\n", ansi.Blue, whiteBold, ansi.Reset, ansi.LightCyan, token, ansi.Reset) } // Confirm that the user wants to write this config - opsani.SetApp(app) - opsani.SetAccessToken(token) + initCmd.SetApp(app) + initCmd.SetAccessToken(token) - cmd.Printf("\nOpsani config initialized:\n") - cmd.PrettyPrintJSONObject(opsani.GetAllSettings()) - if !confirmed { + initCmd.Printf("\nOpsani config initialized:\n") + initCmd.PrettyPrintJSONObject(initCmd.GetAllSettings()) + if !initCmd.confirmed { prompt := &survey.Confirm{ - Message: fmt.Sprintf("Write to %s?", opsani.ConfigFile), + Message: fmt.Sprintf("Write to %s?", initCmd.ConfigFile), } - cmd.AskOne(prompt, &confirmed) + initCmd.AskOne(prompt, &initCmd.confirmed) } - if confirmed { - configDir := filepath.Dir(opsani.ConfigFile) + if initCmd.confirmed { + configDir := filepath.Dir(initCmd.ConfigFile) if _, err := os.Stat(configDir); os.IsNotExist(err) { err = os.Mkdir(configDir, 0755) if err != nil { return err } } - if err := viper.WriteConfigAs(opsani.ConfigFile); err != nil { + if err := viper.WriteConfigAs(initCmd.ConfigFile); err != nil { return err } - cmd.Println("\nOpsani CLI initialized") + initCmd.Println("\nOpsani CLI initialized") } return nil } // NewInitCommand returns a new `opsani init` command instance -func NewInitCommand() *Command { - return NewCommandWithCobraCommand(&cobra.Command{ +func NewInitCommand(baseCommand *BaseCommand) *cobra.Command { + initCmd := &initCommand{BaseCommand: baseCommand} + cmd := &cobra.Command{ Use: "init", Short: "Initialize Opsani config", Long: `Initializes an Opsani config file and acquires the required settings: @@ -120,8 +125,8 @@ func NewInitCommand() *Command { * 'token': API token to authenticate with (OPSANI_TOKEN). `, Args: cobra.NoArgs, - }, func(cmd *Command) { - cmd.RunE = RunInitCommand - cmd.Flags().Bool(confirmedArg, false, "Write config without asking for confirmation") - }) + RunE: initCmd.RunInitCommand, + } + cmd.Flags().BoolVar(&initCmd.confirmed, confirmedArg, false, "Write config without asking for confirmation") + return cmd } diff --git a/command/login.go b/command/login.go index 1582011..8fbb184 100644 --- a/command/login.go +++ b/command/login.go @@ -19,40 +19,35 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/mgutz/ansi" - "github.com/opsani/cli/opsani" "github.com/spf13/cobra" ) -const usernameArg = "username" -const passwordArg = "password" +type loginCommand struct { + *BaseCommand -func runLoginCommand(cmd *cobra.Command, args []string) error { - username, err := cmd.Flags().GetString(usernameArg) - if err != nil { - return err - } - password, err := cmd.Flags().GetString(passwordArg) - if err != nil { - return err - } - fmt.Println("Logging into", opsani.GetBaseURLHostnameAndPort()) + username string + password string +} + +func (loginCmd *loginCommand) runLoginCommand(_ *cobra.Command, args []string) error { + fmt.Println("Logging into", loginCmd.GetBaseURLHostnameAndPort()) whiteBold := ansi.ColorCode("white+b") - if username == "" { + if loginCmd.username == "" { err := survey.AskOne(&survey.Input{ Message: "Username:", - }, &username, survey.WithValidator(survey.Required)) + }, &loginCmd.username, survey.WithValidator(survey.Required)) if err != nil { return err } } else { - fmt.Printf("%si %sUsername: %s%s%s%s\n", ansi.Blue, whiteBold, ansi.Reset, ansi.LightCyan, username, ansi.Reset) + fmt.Printf("%si %sUsername: %s%s%s%s\n", ansi.Blue, whiteBold, ansi.Reset, ansi.LightCyan, loginCmd.username, ansi.Reset) } - if password == "" { + if loginCmd.password == "" { err := survey.AskOne(&survey.Password{ Message: "Password:", - }, &password, survey.WithValidator(survey.Required)) + }, &loginCmd.password, survey.WithValidator(survey.Required)) if err != nil { return err } @@ -61,17 +56,18 @@ func runLoginCommand(cmd *cobra.Command, args []string) error { } // NewLoginCommand returns a new `opani login` command instance -func NewLoginCommand() *cobra.Command { - loginCmd := &cobra.Command{ +func NewLoginCommand(baseCmd *BaseCommand) *cobra.Command { + loginCmd := loginCommand{BaseCommand: baseCmd} + c := &cobra.Command{ Use: "login", Short: "Login to the Opsani API", Long: `Login to the Opsani API and persist access credentials.`, Args: cobra.NoArgs, - RunE: runLoginCommand, + RunE: loginCmd.runLoginCommand, } - loginCmd.Flags().StringP(usernameArg, "u", "", "Opsani Username") - loginCmd.Flags().StringP(passwordArg, "p", "", "Password") + c.Flags().StringVarP(&loginCmd.username, "username", "u", "", "Opsani Username") + c.Flags().StringVarP(&loginCmd.password, "password", "p", "", "Password") - return loginCmd + return c } diff --git a/command/root.go b/command/root.go index 84a716c..f008511 100644 --- a/command/root.go +++ b/command/root.go @@ -17,10 +17,13 @@ package command import ( "errors" "fmt" + "net/url" "os" + "path/filepath" "strings" "github.com/AlecAivazis/survey/v2/terminal" + "github.com/mitchellh/go-homedir" "github.com/opsani/cli/opsani" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -28,9 +31,26 @@ import ( "github.com/spf13/viper" ) +// Configuration keys (Cobra and Viper) +const ( + KeyBaseURL = "base-url" + KeyApp = "app" + KeyToken = "token" + KeyDebugMode = "debug" + KeyRequestTracing = "trace-requests" + KeyEnvPrefix = "OPSANI" + + DefaultBaseURL = "https://api.opsani.com/" +) + // NewRootCommand returns a new instance of the root command for Opsani CLI -func NewRootCommand() *cobra.Command { - rootCmd := &cobra.Command{ +func NewRootCommand() *BaseCommand { + // Create our base command to bind configuration + // viper := viper.New() + // baseCommand := &BaseCommand{viper: viper} + rootCmd := &BaseCommand{} + + cobraCmd := &cobra.Command{ Use: "opsani", Short: "The official CLI for Opsani", Long: `Work with Opsani from the command line. @@ -45,43 +65,50 @@ We'd love to hear your feedback at `, Version: "0.0.1", } - rootCmd.PersistentFlags().String(opsani.KeyBaseURL, opsani.DefaultBaseURL, "Base URL for accessing the Opsani API") - rootCmd.PersistentFlags().MarkHidden(opsani.KeyBaseURL) - viper.BindPFlag(opsani.KeyBaseURL, rootCmd.PersistentFlags().Lookup(opsani.KeyBaseURL)) - rootCmd.PersistentFlags().String(opsani.KeyApp, "", "App to control (overrides config file and OPSANI_APP)") - viper.BindPFlag(opsani.KeyApp, rootCmd.PersistentFlags().Lookup(opsani.KeyApp)) - rootCmd.PersistentFlags().String(opsani.KeyToken, "", "API token to authenticate with (overrides config file and OPSANI_TOKEN)") - viper.BindPFlag(opsani.KeyToken, rootCmd.PersistentFlags().Lookup(opsani.KeyToken)) - rootCmd.PersistentFlags().BoolP(opsani.KeyDebugMode, "D", false, "Enable debug mode") - viper.BindPFlag(opsani.KeyDebugMode, rootCmd.PersistentFlags().Lookup(opsani.KeyDebugMode)) - rootCmd.PersistentFlags().Bool(opsani.KeyRequestTracing, false, "Enable request tracing") - viper.BindPFlag(opsani.KeyRequestTracing, rootCmd.PersistentFlags().Lookup(opsani.KeyRequestTracing)) - - rootCmd.PersistentFlags().StringVar(&opsani.ConfigFile, "config", "", fmt.Sprintf("Location of config file (default \"%s\")", opsani.DefaultConfigFile())) - rootCmd.MarkPersistentFlagFilename("config", "*.yaml", "*.yml") - rootCmd.SetVersionTemplate("Opsani CLI version {{.Version}}\n") - rootCmd.Flags().Bool("version", false, "Display version and exit") - rootCmd.PersistentFlags().Bool("help", false, "Display help and exit") - rootCmd.PersistentFlags().MarkHidden("help") - rootCmd.SetHelpCommand(&cobra.Command{ + // Link our root command to Cobra + rootCmd.rootCmd = cobraCmd + + // Bind our global configuration parameters + cobraCmd.PersistentFlags().String(KeyBaseURL, DefaultBaseURL, "Base URL for accessing the Opsani API") + cobraCmd.PersistentFlags().MarkHidden(KeyBaseURL) + viper.BindPFlag(KeyBaseURL, cobraCmd.PersistentFlags().Lookup(KeyBaseURL)) + + cobraCmd.PersistentFlags().String(KeyApp, "", "App to control (overrides config file and OPSANI_APP)") + viper.BindPFlag(KeyApp, cobraCmd.PersistentFlags().Lookup(KeyApp)) + + cobraCmd.PersistentFlags().String(KeyToken, "", "API token to authenticate with (overrides config file and OPSANI_TOKEN)") + viper.BindPFlag(KeyToken, cobraCmd.PersistentFlags().Lookup(KeyToken)) + + // Not stored in Viper + cobraCmd.PersistentFlags().BoolVarP(&rootCmd.debugModeEnabled, KeyDebugMode, "D", false, "Enable debug mode") + cobraCmd.PersistentFlags().BoolVar(&rootCmd.requestTracingEnabled, KeyRequestTracing, false, "Enable request tracing") + + configFileUsage := fmt.Sprintf("Location of config file (default \"%s\")", rootCmd.DefaultConfigFile()) + cobraCmd.PersistentFlags().StringVar(&rootCmd.ConfigFile, "config", "", configFileUsage) + cobraCmd.MarkPersistentFlagFilename("config", "*.yaml", "*.yml") + cobraCmd.SetVersionTemplate("Opsani CLI version {{.Version}}\n") + cobraCmd.Flags().Bool("version", false, "Display version and exit") + cobraCmd.PersistentFlags().Bool("help", false, "Display help and exit") + cobraCmd.PersistentFlags().MarkHidden("help") + cobraCmd.SetHelpCommand(&cobra.Command{ Hidden: true, }) // Add all sub-commands - rootCmd.AddCommand(NewInitCommand().Command) - rootCmd.AddCommand(NewAppCommand()) - rootCmd.AddCommand(NewLoginCommand()) + cobraCmd.AddCommand(NewInitCommand(rootCmd)) + cobraCmd.AddCommand(NewAppCommand(rootCmd)) + cobraCmd.AddCommand(NewLoginCommand(rootCmd)) - rootCmd.AddCommand(newDiscoverCommand()) - rootCmd.AddCommand(newIMBCommand()) - rootCmd.AddCommand(newPullCommand()) + cobraCmd.AddCommand(newDiscoverCommand(rootCmd)) + cobraCmd.AddCommand(newIMBCommand(rootCmd)) + cobraCmd.AddCommand(newPullCommand(rootCmd)) - rootCmd.AddCommand(NewConfigCommand().Command) - rootCmd.AddCommand(NewCompletionCommand().Command) - rootCmd.AddCommand(NewServoCommand().Command) + cobraCmd.AddCommand(NewConfigCommand(rootCmd)) + cobraCmd.AddCommand(NewCompletionCommand(rootCmd)) + cobraCmd.AddCommand(NewServoCommand(rootCmd)) // See Execute() - rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { + cobraCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { if err == pflag.ErrHelp { return err } @@ -89,7 +116,7 @@ We'd love to hear your feedback at `, }) // Load configuration before execution of every action - rootCmd.PersistentPreRunE = ReduceRunEFuncs(InitConfigRunE, RequireConfigFileFlagToExistRunE) + cobraCmd.PersistentPreRunE = ReduceRunEFuncs(rootCmd.InitConfigRunE, rootCmd.RequireConfigFileFlagToExistRunE) return rootCmd } @@ -126,13 +153,14 @@ func subCommandPath(rootCmd *cobra.Command, cmd *cobra.Command) string { // All commands with RunE will bubble errors back here func Execute() (cmd *cobra.Command, err error) { rootCmd := NewRootCommand() + cobraCmd := rootCmd.rootCmd - if err := initConfig(); err != nil { + if err := rootCmd.initConfig(); err != nil { rootCmd.PrintErr(err) - return rootCmd, err + return cobraCmd, err } - executedCmd, err := rootCmd.ExecuteC() + executedCmd, err := rootCmd.rootCmd.ExecuteC() if err != nil { // Exit silently if the user bailed with control-c if errors.Is(err, terminal.InterruptErr) { @@ -150,7 +178,7 @@ func Execute() (cmd *cobra.Command, err error) { executedCmd.PrintErrln(executedCmd.UsageString()) } } - return executedCmd, err + return cobraCmd, err } // RunFunc is a Cobra Run function @@ -180,16 +208,18 @@ func ReduceRunEFuncs(runFuncs ...RunEFunc) RunEFunc { } } +// TODO: Move all of these onto BaseCommand as methods + // InitConfigRunE initializes client configuration and aborts execution if an error is encountered -func InitConfigRunE(cmd *cobra.Command, args []string) error { - return initConfig() +func (baseCmd *BaseCommand) InitConfigRunE(cmd *cobra.Command, args []string) error { + return baseCmd.initConfig() } // RequireConfigFileFlagToExistRunE aborts command execution with an error if the config file specified via a flag does not exist -func RequireConfigFileFlagToExistRunE(cmd *cobra.Command, args []string) error { +func (baseCmd *BaseCommand) RequireConfigFileFlagToExistRunE(cmd *cobra.Command, args []string) error { if configFilePath, err := cmd.Root().PersistentFlags().GetString("config"); err == nil { if configFilePath != "" { - if _, err := os.Stat(opsani.ConfigFile); os.IsNotExist(err) { + if _, err := os.Stat(baseCmd.ConfigFile); os.IsNotExist(err) { return err } } @@ -200,34 +230,33 @@ func RequireConfigFileFlagToExistRunE(cmd *cobra.Command, args []string) error { } // RequireInitRunE aborts command execution with an error if the client is not initialized -func RequireInitRunE(cmd *cobra.Command, args []string) error { - if !opsani.IsInitialized() { +func (baseCmd *BaseCommand) RequireInitRunE(cmd *cobra.Command, args []string) error { + if !baseCmd.IsInitialized() { return fmt.Errorf("command failed because client is not initialized. Run %q and try again", "opsani init") } return nil } -func initConfig() error { - if opsani.ConfigFile != "" { - // Use config file from the flag. (TODO: Should we check if the file exists unless we are running init?) - viper.SetConfigFile(opsani.ConfigFile) +func (baseCmd *BaseCommand) initConfig() error { + if baseCmd.ConfigFile != "" { + viper.SetConfigFile(baseCmd.ConfigFile) } else { // Find Opsani config in home directory - viper.AddConfigPath(opsani.DefaultConfigPath()) + viper.AddConfigPath(baseCmd.DefaultConfigPath()) viper.SetConfigName("config") - viper.SetConfigType(opsani.DefaultConfigType()) - opsani.ConfigFile = opsani.DefaultConfigFile() + viper.SetConfigType(baseCmd.DefaultConfigType()) + baseCmd.ConfigFile = baseCmd.DefaultConfigFile() } // Set up environment variables - viper.SetEnvPrefix(opsani.KeyEnvPrefix) + viper.SetEnvPrefix(KeyEnvPrefix) viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) viper.AutomaticEnv() // Load the configuration if err := viper.ReadInConfig(); err == nil { - opsani.ConfigFile = viper.ConfigFileUsed() + baseCmd.ConfigFile = viper.ConfigFileUsed() } else { switch err.(type) { @@ -241,14 +270,14 @@ func initConfig() error { return nil } -// NewAPIClientFromConfig returns an Opsani API client configured using the active configuration -func NewAPIClientFromConfig() *opsani.Client { +// NewAPIClient returns an Opsani API client configured using the active configuration +func (baseCmd *BaseCommand) NewAPIClient() *opsani.Client { c := opsani.NewClient(). - SetBaseURL(opsani.GetBaseURL()). - SetApp(opsani.GetApp()). - SetAuthToken(opsani.GetAccessToken()). - SetDebug(opsani.GetDebugModeEnabled()) - if opsani.GetRequestTracingEnabled() { + SetBaseURL(baseCmd.BaseURL()). + SetApp(baseCmd.App()). + SetAuthToken(baseCmd.AccessToken()). + SetDebug(baseCmd.DebugModeEnabled()) + if baseCmd.RequestTracingEnabled() { c.EnableTrace() } @@ -258,3 +287,59 @@ func NewAPIClientFromConfig() *opsani.Client { } return c } + +// GetBaseURLHostnameAndPort returns the hostname and port portion of Opsani base URL for summary display +func (baseCmd *BaseCommand) GetBaseURLHostnameAndPort() string { + u, err := url.Parse(baseCmd.GetBaseURL()) + if err != nil { + return baseCmd.GetBaseURL() + } + baseURLDescription := u.Hostname() + if port := u.Port(); port != "" && port != "80" && port != "443" { + baseURLDescription = baseURLDescription + ":" + port + } + return baseURLDescription +} + +// DefaultConfigFile returns the full path to the default Opsani configuration file +func (baseCmd *BaseCommand) DefaultConfigFile() string { + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + return filepath.Join(home, ".opsani", "config.yaml") +} + +// DefaultConfigPath returns the path to the directory storing the Opsani configuration file +func (baseCmd *BaseCommand) DefaultConfigPath() string { + return filepath.Dir(baseCmd.DefaultConfigFile()) +} + +// DefaultConfigType returns the +func (baseCmd *BaseCommand) DefaultConfigType() string { + return "yaml" +} + +// GetBaseURL returns the Opsani API base URL +func (baseCmd *BaseCommand) GetBaseURL() string { + return viper.GetString(KeyBaseURL) +} + +// GetAppComponents returns the organization name and app ID as separate path components +func (baseCmd *BaseCommand) GetAppComponents() (orgSlug string, appSlug string) { + app := baseCmd.App() + org := filepath.Dir(app) + appID := filepath.Base(app) + return org, appID +} + +// GetAllSettings returns all configuration settings +func (baseCmd *BaseCommand) GetAllSettings() map[string]interface{} { + return viper.AllSettings() +} + +// IsInitialized returns a boolean value that indicates if the client has been initialized +func (baseCmd *BaseCommand) IsInitialized() bool { + return baseCmd.App() != "" && baseCmd.AccessToken() != "" +} diff --git a/command/servo.go b/command/servo.go index 832cf6c..20a7612 100644 --- a/command/servo.go +++ b/command/servo.go @@ -21,6 +21,7 @@ import ( "os" "strings" + "github.com/AlecAivazis/survey/v2" "github.com/mitchellh/go-homedir" "github.com/olekukonko/tablewriter" "github.com/prometheus/common/log" @@ -32,70 +33,80 @@ import ( "golang.org/x/crypto/ssh/terminal" ) +type servoCommand struct { + *BaseCommand +} + // NewServoCommand returns a new instance of the servo command -func NewServoCommand() *Command { - logsCmd := NewCommandWithCobraCommand(&cobra.Command{ - Use: "logs", - Short: "View logs on a Servo", - Args: cobra.ExactArgs(1), - }, func(cmd *Command) { - cmd.RunE = RunServoLogs - }) +func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { + servoCommand := servoCommand{BaseCommand: baseCmd} - servoCmd := NewCommandWithCobraCommand(&cobra.Command{ + servoCmd := &cobra.Command{ Use: "servo", Short: "Manage Servos", Args: cobra.NoArgs, - }, func(cmd *Command) { - cmd.PersistentPreRunE = ReduceRunEFuncsO(InitConfigRunE, RequireConfigFileFlagToExistRunE, RequireInitRunE) - }) + PersistentPreRunE: ReduceRunEFuncs( + baseCmd.InitConfigRunE, + baseCmd.RequireConfigFileFlagToExistRunE, + baseCmd.RequireInitRunE, + ), + } - servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ - Use: "ssh", - Short: "SSH into a Servo", - Args: cobra.ExactArgs(1), - }, func(cmd *Command) { - cmd.RunE = RunServoSSH - }).Command) + // Servo registry + servoCmd.AddCommand(&cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List Servos", + Args: cobra.NoArgs, + RunE: servoCommand.RunServoList, + }) + servoCmd.AddCommand(&cobra.Command{ + Use: "Add", + Short: "Add a Servo", + Args: cobra.MaximumNArgs(1), + RunE: servoCommand.RunAddServo, + }) + servoCmd.AddCommand(&cobra.Command{ + Use: "Remove", + Aliases: []string{"rm"}, + Short: "Remove a Servo", + Args: cobra.ExactArgs(1), + RunE: servoCommand.RunRemoveServo, + }) - servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + // Servo Lifecycle + servoCmd.AddCommand(&cobra.Command{ Use: "status", Short: "Check Servo status", Args: cobra.ExactArgs(1), - }, func(cmd *Command) { - cmd.RunE = RunServoStatus - }).Command) - servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + RunE: servoCommand.RunServoStatus, + }) + servoCmd.AddCommand(&cobra.Command{ Use: "start", Short: "Start the Servo", Args: cobra.ExactArgs(1), - }, func(cmd *Command) { - cmd.RunE = RunServoStart - }).Command) - servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + RunE: servoCommand.RunServoStart, + }) + servoCmd.AddCommand(&cobra.Command{ Use: "stop", Short: "Stop the Servo", Args: cobra.ExactArgs(1), - }, func(cmd *Command) { - cmd.RunE = RunServoStop - }).Command) - servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ + RunE: servoCommand.RunServoStop, + }) + servoCmd.AddCommand(&cobra.Command{ Use: "restart", Short: "Restart Servo", Args: cobra.ExactArgs(1), - }, func(cmd *Command) { - cmd.RunE = RunServoRestart - }).Command) - servoCmd.AddCommand(NewCommandWithCobraCommand(&cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List Servos", - Args: cobra.NoArgs, - }, func(cmd *Command) { - cmd.RunE = RunServoList - }).Command) + RunE: servoCommand.RunServoRestart, + }) - servoCmd.AddCommand(logsCmd.Command) + // Servo Access + logsCmd := &cobra.Command{ + Use: "logs", + Short: "View logs on a Servo", + Args: cobra.ExactArgs(1), + RunE: servoCommand.RunServoLogs, + } logsCmd.Flags().BoolP("follow", "f", false, "Follow log output") viper.BindPFlag("follow", logsCmd.Flags().Lookup("follow")) @@ -104,6 +115,14 @@ func NewServoCommand() *Command { logsCmd.Flags().StringP("lines", "l", "25", `Number of lines to show from the end of the logs (or "all").`) viper.BindPFlag("lines", logsCmd.Flags().Lookup("lines")) + servoCmd.AddCommand(logsCmd) + servoCmd.AddCommand(&cobra.Command{ + Use: "ssh", + Short: "SSH into a Servo", + Args: cobra.ExactArgs(1), + RunE: servoCommand.RunServoSSH, + }) + return servoCmd } @@ -127,14 +146,86 @@ var servos = []map[string]string{ }, } -func SSHAgent() ssh.AuthMethod { +func (servoCmd *servoCommand) SSHAgent() ssh.AuthMethod { if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) } return nil } -func runInSSHSession(ctx context.Context, name string, runIt func(context.Context, map[string]string, *ssh.Session) error) error { +func (servoCmd *servoCommand) RunAddServo(_ *cobra.Command, args []string) error { + // Name, [user@]host[:port]/path + return nil + + servos := []map[string]string{} + var servo struct { + Name string + Host string + Port string + Path string + } + // var newServo servo + + if servo.Name == "" { + servo.Name = args[0] + } + + confirmed := false + if servo.Name == "" { + err := survey.AskOne(&survey.Input{ + Message: "Servo name?", + }, &servo.Name, survey.WithValidator(survey.Required)) + if err != nil { + return err + } + // prompt := &survey.Confirm{ + // Message: fmt.Sprintf("Write to %s?", opsani.ConfigFile), + // } + // cmd.AskOne(prompt, &servo.Name) + // servos = append(servos, servo) + } + + if confirmed { + viper.Set("servos", servos) + if err := viper.WriteConfig(); err != nil { + return err + } + } + + return nil +} + +func (servoCmd *servoCommand) RunRemoveServo(_ *cobra.Command, args []string) error { + name := args[0] + var servo map[string]string + for _, s := range servos { + if s["name"] == name { + servo = s + break + } + } + if len(servo) == 0 { + return fmt.Errorf("no such Servo %q", name) + } + + confirmed := false + if !confirmed { + // prompt := &survey.Confirm{ + // Message: fmt.Sprintf("Remove Servo %q? from %q", servo["name"], opsani.ConfigFile), + // } + // servoCommand.AskOne(prompt, &confirmed) + } + + if confirmed { + if err := viper.WriteConfig(); err != nil { + return err + } + } + + return nil +} + +func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, runIt func(context.Context, map[string]string, *ssh.Session) error) error { // TODO: Recover from passphrase error // // signer, err := ssh.ParsePrivateKey([]byte(sshKey)) @@ -177,7 +268,7 @@ func runInSSHSession(ctx context.Context, name string, runIt func(context.Contex // ssh.PublicKeys(signer), // }, Auth: []ssh.AuthMethod{ - SSHAgent(), + servoCmd.SSHAgent(), }, // Non-production only HostKeyCallback: hostKeyCallback, @@ -206,9 +297,14 @@ func runInSSHSession(ctx context.Context, name string, runIt func(context.Contex return runIt(ctx, servo, session) } -func RunServoList(cmd *Command, args []string) error { +func (servoCmd *servoCommand) RunServoList(_ *cobra.Command, args []string) error { data := [][]string{} + viper.Set("servos", servos) + if err := viper.WriteConfigAs(servoCmd.ConfigFile); err != nil { + return err + } + for _, servo := range servos { name := servo["name"] user := servo["user"] @@ -241,46 +337,46 @@ func RunServoList(cmd *Command, args []string) error { return nil } -func RunServoStatus(cmd *Command, args []string) error { +func (servoCmd *servoCommand) RunServoStatus(_ *cobra.Command, args []string) error { ctx := context.Background() - return runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { - return runDockerComposeOverSSH("ps", nil, servo, session) + return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return servoCmd.runDockerComposeOverSSH("ps", nil, servo, session) }) } -func RunServoStart(cmd *Command, args []string) error { +func (servoCmd *servoCommand) RunServoStart(_ *cobra.Command, args []string) error { ctx := context.Background() - return runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { - return runDockerComposeOverSSH("start", nil, servo, session) + return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return servoCmd.runDockerComposeOverSSH("start", nil, servo, session) }) } -func RunServoStop(cmd *Command, args []string) error { +func (servoCmd *servoCommand) RunServoStop(_ *cobra.Command, args []string) error { ctx := context.Background() - return runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { - return runDockerComposeOverSSH("stop", nil, servo, session) + return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return servoCmd.runDockerComposeOverSSH("stop", nil, servo, session) }) } -func RunServoRestart(cmd *Command, args []string) error { +func (servoCmd *servoCommand) RunServoRestart(_ *cobra.Command, args []string) error { ctx := context.Background() - return runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { - return runDockerComposeOverSSH("stop && docker-compse start", nil, servo, session) + return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return servoCmd.runDockerComposeOverSSH("stop && docker-compse start", nil, servo, session) }) } -func RunServoLogs(cmd *Command, args []string) error { +func (servoCmd *servoCommand) RunServoLogs(_ *cobra.Command, args []string) error { ctx := context.Background() - return runInSSHSession(ctx, args[0], RunLogsSSHSession) + return servoCmd.runInSSHSession(ctx, args[0], servoCmd.RunLogsSSHSession) } // RunConfig displays Opsani CLI config info -func RunServoSSH(cmd *Command, args []string) error { +func (servoCmd *servoCommand) RunServoSSH(_ *cobra.Command, args []string) error { ctx := context.Background() - return runInSSHSession(ctx, args[0], RunShellOnSSHSession) + return servoCmd.runInSSHSession(ctx, args[0], servoCmd.RunShellOnSSHSession) } -func runDockerComposeOverSSH(cmd string, args []string, servo map[string]string, session *ssh.Session) error { +func (servoCmd *servoCommand) runDockerComposeOverSSH(cmd string, args []string, servo map[string]string, session *ssh.Session) error { session.Stdout = os.Stdout session.Stderr = os.Stderr @@ -291,7 +387,7 @@ func runDockerComposeOverSSH(cmd string, args []string, servo map[string]string, return session.Run(strings.Join(args, " ")) } -func RunLogsSSHSession(ctx context.Context, servo map[string]string, session *ssh.Session) error { +func (servoCmd *servoCommand) RunLogsSSHSession(ctx context.Context, servo map[string]string, session *ssh.Session) error { session.Stdout = os.Stdout session.Stderr = os.Stderr @@ -310,7 +406,7 @@ func RunLogsSSHSession(ctx context.Context, servo map[string]string, session *ss return session.Run(strings.Join(args, " ")) } -func RunShellOnSSHSession(ctx context.Context, servo map[string]string, session *ssh.Session) error { +func (servoCmd *servoCommand) RunShellOnSSHSession(ctx context.Context, servo map[string]string, session *ssh.Session) error { fd := int(os.Stdin.Fd()) state, err := terminal.MakeRaw(fd) if err != nil { diff --git a/command/servo_test.go b/command/servo_test.go index ba32845..edd020d 100644 --- a/command/servo_test.go +++ b/command/servo_test.go @@ -73,7 +73,11 @@ func (s *ServoTestSuite) TestRunningServoLogsHelp() { } func (s *ServoTestSuite) TestRunningServoLogsInvalidServo() { - _, err := s.Execute("servo", "logs", "fake-name") + configFile := test.TempConfigFileWithObj(map[string]string{ + "app": "example.com/app", + "token": "123456", + }) + _, _, err := s.ExecuteC(test.Args("--config", configFile.Name(), "servo", "logs", "fake-name")...) s.Require().EqualError(err, `no such Servo "fake-name"`) } @@ -88,3 +92,15 @@ func (s *ServoTestSuite) TestRunningLogsTimestampsHelp() { s.Require().NoError(err) s.Require().Contains(output, "Show timestamps") } + +func (s *ServoTestSuite) TestRunningAddHelp() { + output, err := s.Execute("servo", "add", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Add a Servo") +} + +func (s *ServoTestSuite) TestRunningRemoveHelp() { + output, err := s.Execute("servo", "remove", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Remove a Servo") +} diff --git a/opsani/config.go b/opsani/config.go deleted file mode 100644 index 3be4c34..0000000 --- a/opsani/config.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2020 Opsani -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package opsani - -import ( - "fmt" - "net/url" - "os" - "path/filepath" - - "github.com/mitchellh/go-homedir" - "github.com/spf13/viper" -) - -// ConfigFile stores the path to the active Opsani configuration file -var ConfigFile string - -// DefaultConfigFile returns the full path to the default Opsani configuration file -func DefaultConfigFile() string { - home, err := homedir.Dir() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return filepath.Join(home, ".opsani", "config.yaml") -} - -// DefaultConfigPath returns the path to the directory storing the Opsani configuration file -func DefaultConfigPath() string { - return filepath.Dir(DefaultConfigFile()) -} - -// DefaultConfigType returns the -func DefaultConfigType() string { - return "yaml" -} - -// GetBaseURL returns the Opsani API base URL -func GetBaseURL() string { - return viper.GetString(KeyBaseURL) -} - -// GetBaseURLHostnameAndPort returns the hostname and port portion of Opsani base URL for summary display -func GetBaseURLHostnameAndPort() string { - u, err := url.Parse(GetBaseURL()) - if err != nil { - return GetBaseURL() - } - baseURLDescription := u.Hostname() - if port := u.Port(); port != "" && port != "80" && port != "443" { - baseURLDescription = baseURLDescription + ":" + port - } - return baseURLDescription -} - -// SetBaseURL sets the Opsani API base URL -func SetBaseURL(baseURL string) { - viper.Set(KeyBaseURL, baseURL) -} - -// GetAccessToken returns the Opsani API access token -func GetAccessToken() string { - return viper.GetString(KeyToken) -} - -// SetAccessToken sets the Opsani API access token -func SetAccessToken(accessToken string) { - viper.Set(KeyToken, accessToken) -} - -// GetApp returns the target Opsani app -func GetApp() string { - return viper.GetString(KeyApp) -} - -// SetApp sets the target Opsani app -func SetApp(app string) { - viper.Set(KeyApp, app) -} - -// GetAppComponents returns the organization name and app ID as separate path components -func GetAppComponents() (orgSlug string, appSlug string) { - app := GetApp() - org := filepath.Dir(app) - appID := filepath.Base(app) - return org, appID -} - -// GetAllSettings returns all configuration settings -func GetAllSettings() map[string]interface{} { - return viper.AllSettings() -} - -// GetDebugModeEnabled returns a boolean value indicating if debugging is enabled -func GetDebugModeEnabled() bool { - return viper.GetBool(KeyDebugMode) -} - -// GetRequestTracingEnabled returns a boolean value indicating if request tracing is enabled -func GetRequestTracingEnabled() bool { - return viper.GetBool(KeyRequestTracing) -} - -// IsInitialized returns a boolean value that indicates if the client has been initialized -func IsInitialized() bool { - return GetApp() != "" && GetAccessToken() != "" -} diff --git a/opsani/keys.go b/opsani/keys.go deleted file mode 100644 index d15457e..0000000 --- a/opsani/keys.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020 Opsani -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package opsani - -// Configuration keys (Cobra and Viper) -const ( - KeyBaseURL = "base-url" - KeyApp = "app" - KeyToken = "token" - KeyDebugMode = "debug" - KeyRequestTracing = "trace-requests" - KeyEnvPrefix = "OPSANI" - - DefaultBaseURL = "https://api.opsani.com/" -) diff --git a/test/suite.go b/test/suite.go index 80afae7..9723fc7 100644 --- a/test/suite.go +++ b/test/suite.go @@ -18,6 +18,7 @@ import ( "strings" "time" + "github.com/opsani/cli/command" "github.com/spf13/cobra" "github.com/stretchr/testify/suite" ) @@ -44,9 +45,14 @@ func (h *Suite) Command() *cobra.Command { return h.cmd } -// SetCommand sets the Cobra command under test +// SetCommand sets the Opsani command under test +func (h *Suite) SetCommand(cmd *command.BaseCommand) { + h.SetCobraCommand(cmd.CobraCommand()) +} + +// SetCobraCommand sets the Cobra command under test // Changing the command will reset the associated command executor and tester instances -func (h *Suite) SetCommand(cmd *cobra.Command) { +func (h *Suite) SetCobraCommand(cmd *cobra.Command) { if cmd != nil { cmdExecutor := NewCommandExecutor(cmd) ice := NewInteractiveCommandExecutor(cmd) From 58814f93d44db3968892b0fd3b963d104b9214bb Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 01:46:08 -0700 Subject: [PATCH 08/33] Model Servo -- add tests for add, remove, ls --- command/servo.go | 213 ++++++++++++++++++++++++++---------------- command/servo_test.go | 163 ++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+), 79 deletions(-) diff --git a/command/servo.go b/command/servo.go index 20a7612..9fbf138 100644 --- a/command/servo.go +++ b/command/servo.go @@ -35,6 +35,7 @@ import ( type servoCommand struct { *BaseCommand + force bool } // NewServoCommand returns a new instance of the servo command @@ -61,18 +62,21 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { RunE: servoCommand.RunServoList, }) servoCmd.AddCommand(&cobra.Command{ - Use: "Add", + Use: "add", Short: "Add a Servo", Args: cobra.MaximumNArgs(1), RunE: servoCommand.RunAddServo, }) - servoCmd.AddCommand(&cobra.Command{ - Use: "Remove", + + removeCmd := &cobra.Command{ + Use: "remove", Aliases: []string{"rm"}, Short: "Remove a Servo", Args: cobra.ExactArgs(1), RunE: servoCommand.RunRemoveServo, - }) + } + removeCmd.Flags().BoolVarP(&servoCommand.force, "force", "f", false, "Don't prompt for confirmation") + servoCmd.AddCommand(removeCmd) // Servo Lifecycle servoCmd.AddCommand(&cobra.Command{ @@ -136,15 +140,15 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { // -----END OPENSSH PRIVATE KEY----- // ` -var servos = []map[string]string{ - { - "name": "opsani-dev", - "host": "3.93.217.12", - "port": "22", - "user": "root", - "path": "/root/dev.opsani.com/blake/oco", - }, -} +// var servos = []map[string]string{ +// { +// "name": "opsani-dev", +// "host": "3.93.217.12", +// "port": "22", +// "user": "root", +// "path": "/root/dev.opsani.com/blake/oco", +// }, +// } func (servoCmd *servoCommand) SSHAgent() ssh.AuthMethod { if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { @@ -153,70 +157,130 @@ func (servoCmd *servoCommand) SSHAgent() ssh.AuthMethod { return nil } -func (servoCmd *servoCommand) RunAddServo(_ *cobra.Command, args []string) error { - // Name, [user@]host[:port]/path - return nil +// Servo represents a deployed Servo assembly running somewhere +type Servo struct { + Name string + User string + Host string + Port string + Path string +} - servos := []map[string]string{} - var servo struct { - Name string - Host string - Port string - Path string +func (s Servo) HostAndPort() string { + v := s.Host + if s.Port != "" && s.Port != "22" { + v = v + ":" + s.Port } - // var newServo servo + return v +} - if servo.Name == "" { +func (s Servo) DisplayPath() string { + if s.Path != "" { + return s.Path + } + return "~/" +} + +func (servoCmd *servoCommand) RunAddServo(_ *cobra.Command, args []string) error { + servo := Servo{} + if len(args) > 0 { servo.Name = args[0] } - confirmed := false + servos := make([]Servo, 0) + err := viper.UnmarshalKey("servos", &servos) + if err != nil { + return err + } + if servo.Name == "" { - err := survey.AskOne(&survey.Input{ + err := servoCmd.AskOne(&survey.Input{ Message: "Servo name?", }, &servo.Name, survey.WithValidator(survey.Required)) if err != nil { return err } - // prompt := &survey.Confirm{ - // Message: fmt.Sprintf("Write to %s?", opsani.ConfigFile), - // } - // cmd.AskOne(prompt, &servo.Name) - // servos = append(servos, servo) } - if confirmed { - viper.Set("servos", servos) - if err := viper.WriteConfig(); err != nil { + if servo.User == "" { + err := servoCmd.AskOne(&survey.Input{ + Message: "User?", + }, &servo.User, survey.WithValidator(survey.Required)) + if err != nil { + return err + } + } + + if servo.Host == "" { + err := servoCmd.AskOne(&survey.Input{ + Message: "Host?", + }, &servo.Host, survey.WithValidator(survey.Required)) + if err != nil { + return err + } + } + + if servo.Path == "" { + err := servoCmd.AskOne(&survey.Input{ + Message: "Path? (optional)", + }, &servo.Path, survey.WithValidator(survey.Required)) + if err != nil { return err } } + servos = append(servos, servo) + viper.Set("servos", servos) + if err := viper.WriteConfig(); err != nil { + return err + } + return nil } +// GetServos returns the Servos in the configuration +func (servoCmd *servoCommand) GetServos() ([]Servo, error) { + servos := make([]Servo, 0) + err := viper.UnmarshalKey("servos", &servos) + if err != nil { + return nil, err + } + return servos, nil +} + func (servoCmd *servoCommand) RunRemoveServo(_ *cobra.Command, args []string) error { name := args[0] - var servo map[string]string - for _, s := range servos { - if s["name"] == name { - servo = s + var servo *Servo + + // Find the target + servos, err := servoCmd.GetServos() + if err != nil { + return err + } + var index int + for i, s := range servos { + if s.Name == name { + servo = &s + index = i break } } - if len(servo) == 0 { - return fmt.Errorf("no such Servo %q", name) + + if servo == nil { + return fmt.Errorf("Unable to find Servo named %q", servo) } - confirmed := false + confirmed := servoCmd.force if !confirmed { - // prompt := &survey.Confirm{ - // Message: fmt.Sprintf("Remove Servo %q? from %q", servo["name"], opsani.ConfigFile), - // } - // servoCommand.AskOne(prompt, &confirmed) + prompt := &survey.Confirm{ + Message: fmt.Sprintf("Remove Servo %q?", servo.Name), + } + servoCmd.AskOne(prompt, &confirmed) } if confirmed { + servos = append(servos[:index], servos[index+1:]...) + viper.Set("servos", servos) if err := viper.WriteConfig(); err != nil { return err } @@ -225,7 +289,7 @@ func (servoCmd *servoCommand) RunRemoveServo(_ *cobra.Command, args []string) er return nil } -func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, runIt func(context.Context, map[string]string, *ssh.Session) error) error { +func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, runIt func(context.Context, Servo, *ssh.Session) error) error { // TODO: Recover from passphrase error // // signer, err := ssh.ParsePrivateKey([]byte(sshKey)) @@ -241,14 +305,15 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, // } // signer := ssh.NewSignerFromKey(key) - var servo map[string]string + var servo *Servo + servos, err := servoCmd.GetServos() for _, s := range servos { - if s["name"] == name { - servo = s + if s.Name == name { + servo = &s break } } - if len(servo) == 0 { + if servo == nil { return fmt.Errorf("no such Servo %q", name) } @@ -262,7 +327,7 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, log.Fatal("could not create hostkeycallback function: ", err) } config := &ssh.ClientConfig{ - User: servo["user"], + User: servo.User, // Auth: []ssh.AuthMethod{ // // ssh.Password(password), // ssh.PublicKeys(signer), @@ -276,7 +341,7 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, // // Connect to host fmt.Printf("Servos: %+v\nServo=%+v\n\n", servos, servo) - client, err := ssh.Dial("tcp", servo["host"]+":"+servo["port"], config) + client, err := ssh.Dial("tcp", servo.HostAndPort(), config) if err != nil { log.Fatal(err) } @@ -294,29 +359,19 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, client.Close() }() - return runIt(ctx, servo, session) + return runIt(ctx, *servo, session) } func (servoCmd *servoCommand) RunServoList(_ *cobra.Command, args []string) error { data := [][]string{} - - viper.Set("servos", servos) - if err := viper.WriteConfigAs(servoCmd.ConfigFile); err != nil { - return err - } - + servos, _ := servoCmd.GetServos() for _, servo := range servos { - name := servo["name"] - user := servo["user"] - host := servo["host"] - if port := servo["port"]; port != "" && port != "22" { - host = host + ":" + port - } - path := "~/" - if p := servo["path"]; p != "" { - path = p - } - data = append(data, []string{name, user, host, path}) + data = append(data, []string{ + servo.Name, + servo.User, + servo.HostAndPort(), + servo.DisplayPath(), + }) } table := tablewriter.NewWriter(os.Stdout) @@ -339,28 +394,28 @@ func (servoCmd *servoCommand) RunServoList(_ *cobra.Command, args []string) erro func (servoCmd *servoCommand) RunServoStatus(_ *cobra.Command, args []string) error { ctx := context.Background() - return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo Servo, session *ssh.Session) error { return servoCmd.runDockerComposeOverSSH("ps", nil, servo, session) }) } func (servoCmd *servoCommand) RunServoStart(_ *cobra.Command, args []string) error { ctx := context.Background() - return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo Servo, session *ssh.Session) error { return servoCmd.runDockerComposeOverSSH("start", nil, servo, session) }) } func (servoCmd *servoCommand) RunServoStop(_ *cobra.Command, args []string) error { ctx := context.Background() - return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo Servo, session *ssh.Session) error { return servoCmd.runDockerComposeOverSSH("stop", nil, servo, session) }) } func (servoCmd *servoCommand) RunServoRestart(_ *cobra.Command, args []string) error { ctx := context.Background() - return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo map[string]string, session *ssh.Session) error { + return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo Servo, session *ssh.Session) error { return servoCmd.runDockerComposeOverSSH("stop && docker-compse start", nil, servo, session) }) } @@ -376,23 +431,23 @@ func (servoCmd *servoCommand) RunServoSSH(_ *cobra.Command, args []string) error return servoCmd.runInSSHSession(ctx, args[0], servoCmd.RunShellOnSSHSession) } -func (servoCmd *servoCommand) runDockerComposeOverSSH(cmd string, args []string, servo map[string]string, session *ssh.Session) error { +func (servoCmd *servoCommand) runDockerComposeOverSSH(cmd string, args []string, servo Servo, session *ssh.Session) error { session.Stdout = os.Stdout session.Stderr = os.Stderr - if path := servo["path"]; path != "" { + if path := servo.Path; path != "" { args = append(args, "cd", path+"&&") } args = append(args, "docker-compose", cmd) return session.Run(strings.Join(args, " ")) } -func (servoCmd *servoCommand) RunLogsSSHSession(ctx context.Context, servo map[string]string, session *ssh.Session) error { +func (servoCmd *servoCommand) RunLogsSSHSession(ctx context.Context, servo Servo, session *ssh.Session) error { session.Stdout = os.Stdout session.Stderr = os.Stderr args := []string{} - if path := servo["path"]; path != "" { + if path := servo.Path; path != "" { args = append(args, "cd", path+"&&") } args = append(args, "docker-compose logs") @@ -406,7 +461,7 @@ func (servoCmd *servoCommand) RunLogsSSHSession(ctx context.Context, servo map[s return session.Run(strings.Join(args, " ")) } -func (servoCmd *servoCommand) RunShellOnSSHSession(ctx context.Context, servo map[string]string, session *ssh.Session) error { +func (servoCmd *servoCommand) RunShellOnSSHSession(ctx context.Context, servo Servo, session *ssh.Session) error { fd := int(os.Stdin.Fd()) state, err := terminal.MakeRaw(fd) if err != nil { diff --git a/command/servo_test.go b/command/servo_test.go index edd020d..18fd247 100644 --- a/command/servo_test.go +++ b/command/servo_test.go @@ -15,12 +15,15 @@ package command_test import ( + "io/ioutil" "testing" + "github.com/AlecAivazis/survey/v2/core" "github.com/opsani/cli/command" "github.com/opsani/cli/test" "github.com/spf13/viper" "github.com/stretchr/testify/suite" + "gopkg.in/yaml.v2" ) type ServoTestSuite struct { @@ -33,6 +36,7 @@ func TestServoTestSuite(t *testing.T) { func (s *ServoTestSuite) SetupTest() { viper.Reset() + core.DisableColor = true s.SetCommand(command.NewRootCommand()) } @@ -99,8 +103,167 @@ func (s *ServoTestSuite) TestRunningAddHelp() { s.Require().Contains(output, "Add a Servo") } +func (s *ServoTestSuite) TestRunningAddNoInput() { + configFile := test.TempConfigFileWithObj(map[string]string{ + "app": "example.com/app", + "token": "123456", + }) + args := test.Args("--config", configFile.Name(), "servo", "add") + context, err := s.ExecuteTestInteractively(args, func(t *test.InteractiveTestContext) error { + t.RequireString("? Servo name?") + t.SendLine("opsani-dev") + t.RequireString("? User?") + t.SendLine("blakewatters") + t.RequireString("? Host?") + t.SendLine("dev.opsani.com") + t.RequireString("? Path? (optional)") + t.SendLine("/servo") + t.ExpectEOF() + return nil + }) + s.T().Logf("The output buffer is: %v", context.OutputBuffer().String()) + s.Require().NoError(err) + + // Check the config file + var config = map[string]interface{}{} + body, _ := ioutil.ReadFile(configFile.Name()) + yaml.Unmarshal(body, &config) + expected := []interface{}( + []interface{}{ + map[interface{}]interface{}{ + "host": "dev.opsani.com", + "name": "opsani-dev", + "path": "/servo", + "port": "", + "user": "blakewatters", + }, + }, + ) + s.Require().EqualValues(expected, config["servos"]) +} + +// TODO: Override port and specifying some values on CLI + func (s *ServoTestSuite) TestRunningRemoveHelp() { output, err := s.Execute("servo", "remove", "--help") s.Require().NoError(err) s.Require().Contains(output, "Remove a Servo") } + +// TODO: add -f +func (s *ServoTestSuite) TestRunningRemoveServoConfirmed() { + configFile := test.TempConfigFileWithObj(map[string]interface{}{ + "app": "example.com/app", + "token": "123456", + "servos": []map[string]string{ + { + "host": "dev.opsani.com", + "name": "opsani-dev", + "path": "/servo", + "port": "", + "user": "blakewatters", + }, + }, + }) + args := test.Args("--config", configFile.Name(), "servo", "remove", "opsani-dev") + _, err := s.ExecuteTestInteractively(args, func(t *test.InteractiveTestContext) error { + t.RequireString(`? Remove Servo "opsani-dev"?`) + t.SendLine("Y") + t.ExpectEOF() + return nil + }) + s.Require().NoError(err) + + // Check the config file + var config = map[string]interface{}{} + body, _ := ioutil.ReadFile(configFile.Name()) + yaml.Unmarshal(body, &config) + s.Require().EqualValues([]interface{}{}, config["servos"]) +} + +func (s *ServoTestSuite) TestRunningRemoveServoForce() { + config := map[string]interface{}{ + "app": "example.com/app", + "token": "123456", + "servos": []map[string]string{ + { + "host": "dev.opsani.com", + "name": "opsani-dev", + "path": "/servo", + "port": "", + "user": "blakewatters", + }, + }, + } + configFile := test.TempConfigFileWithObj(config) + _, err := s.Execute("--config", configFile.Name(), "servo", "remove", "-f", "opsani-dev") + s.Require().NoError(err) + + // Check that the servo has been removed + var configState = map[string]interface{}{} + body, _ := ioutil.ReadFile(configFile.Name()) + yaml.Unmarshal(body, &configState) + s.Require().EqualValues([]interface{}{}, configState["servos"]) +} + +func (s *ServoTestSuite) TestRunningRemoveServoDeclined() { + configFile := test.TempConfigFileWithObj(map[string]interface{}{ + "app": "example.com/app", + "token": "123456", + "servos": []map[string]string{ + { + "host": "dev.opsani.com", + "name": "opsani-dev", + "path": "/servo", + "port": "", + "user": "blakewatters", + }, + }, + }) + args := test.Args("--config", configFile.Name(), "servo", "remove", "opsani-dev") + _, err := s.ExecuteTestInteractively(args, func(t *test.InteractiveTestContext) error { + t.RequireString(`? Remove Servo "opsani-dev"?`) + t.SendLine("N") + t.ExpectEOF() + return nil + }) + s.Require().NoError(err) + + // Check that the config file has not changed + expected := []interface{}( + []interface{}{ + map[interface{}]interface{}{ + "host": "dev.opsani.com", + "name": "opsani-dev", + "path": "/servo", + "port": "", + "user": "blakewatters", + }, + }, + ) + body, _ := ioutil.ReadFile(configFile.Name()) + var configState = map[string]interface{}{} + yaml.Unmarshal(body, &configState) + s.Require().EqualValues(expected, configState["servos"]) +} + +func (s *ServoTestSuite) TestRunningServoList() { + config := map[string]interface{}{ + "app": "example.com/app", + "token": "123456", + "servos": []map[string]string{ + { + "host": "dev.opsani.com", + "name": "opsani-dev", + "path": "/servo", + "port": "", + "user": "blakewatters", + }, + }, + } + configFile := test.TempConfigFileWithObj(config) + output, err := s.Execute("--config", configFile.Name(), "servo", "list") + s.Require().NoError(err) + s.Require().Contains("NAME USER HOST PATH ", output) + s.Require().Contains("opsani-dev blakewatters dev.opsani.com /servo ", output) +} From 670aa9a84fb45c98ca4452a276b3edda58d1ea24 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 01:55:53 -0700 Subject: [PATCH 09/33] Fix regressions --- command/servo.go | 13 +++++++++++-- command/servo_test.go | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/command/servo.go b/command/servo.go index 9fbf138..e389cd8 100644 --- a/command/servo.go +++ b/command/servo.go @@ -167,6 +167,15 @@ type Servo struct { } func (s Servo) HostAndPort() string { + h := s.Host + p := s.Port + if p == "" { + p = "22" + } + return strings.Join([]string{h, p}, ":") +} + +func (s Servo) DisplayHost() string { v := s.Host if s.Port != "" && s.Port != "22" { v = v + ":" + s.Port @@ -267,7 +276,7 @@ func (servoCmd *servoCommand) RunRemoveServo(_ *cobra.Command, args []string) er } if servo == nil { - return fmt.Errorf("Unable to find Servo named %q", servo) + return fmt.Errorf("Unable to find Servo named %q", name) } confirmed := servoCmd.force @@ -369,7 +378,7 @@ func (servoCmd *servoCommand) RunServoList(_ *cobra.Command, args []string) erro data = append(data, []string{ servo.Name, servo.User, - servo.HostAndPort(), + servo.DisplayHost(), servo.DisplayPath(), }) } diff --git a/command/servo_test.go b/command/servo_test.go index 18fd247..7067677 100644 --- a/command/servo_test.go +++ b/command/servo_test.go @@ -181,6 +181,16 @@ func (s *ServoTestSuite) TestRunningRemoveServoConfirmed() { s.Require().EqualValues([]interface{}{}, config["servos"]) } +func (s *ServoTestSuite) TestRunningRemoveServoUnknown() { + config := map[string]interface{}{ + "app": "example.com/app", + "token": "123456", + } + configFile := test.TempConfigFileWithObj(config) + _, err := s.Execute("--config", configFile.Name(), "servo", "remove", "unknown") + s.Require().EqualError(err, `Unable to find Servo named "unknown"`) +} + func (s *ServoTestSuite) TestRunningRemoveServoForce() { config := map[string]interface{}{ "app": "example.com/app", From e202bfab1067d5738cb12770f01a3afe62aa3aa2 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 02:13:52 -0700 Subject: [PATCH 10/33] Add verbose list and tests --- command/servo.go | 59 ++++++++++++++++++++++++++++++------------- command/servo_test.go | 24 ++++++++++++++++-- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/command/servo.go b/command/servo.go index e389cd8..9b86632 100644 --- a/command/servo.go +++ b/command/servo.go @@ -35,7 +35,8 @@ import ( type servoCommand struct { *BaseCommand - force bool + force bool + verbose bool } // NewServoCommand returns a new instance of the servo command @@ -54,13 +55,15 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { } // Servo registry - servoCmd.AddCommand(&cobra.Command{ + listCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List Servos", Args: cobra.NoArgs, RunE: servoCommand.RunServoList, - }) + } + listCmd.Flags().BoolVarP(&servoCommand.verbose, "verbose", "v", false, "Display verbose output") + servoCmd.AddCommand(listCmd) servoCmd.AddCommand(&cobra.Command{ Use: "add", Short: "Add a Servo", @@ -190,6 +193,17 @@ func (s Servo) DisplayPath() string { return "~/" } +func (s Servo) URL() string { + pathComponent := "" + if s.Path != "" { + if s.Port != "" && s.Port != "22" { + pathComponent = pathComponent + ":" + } + pathComponent = pathComponent + s.Path + } + return fmt.Sprintf("ssh://%s@%s:%s", s.User, s.DisplayHost(), pathComponent) +} + func (servoCmd *servoCommand) RunAddServo(_ *cobra.Command, args []string) error { servo := Servo{} if len(args) > 0 { @@ -372,19 +386,7 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, } func (servoCmd *servoCommand) RunServoList(_ *cobra.Command, args []string) error { - data := [][]string{} - servos, _ := servoCmd.GetServos() - for _, servo := range servos { - data = append(data, []string{ - servo.Name, - servo.User, - servo.DisplayHost(), - servo.DisplayPath(), - }) - } - - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"NAME", "USER", "HOST", "PATH"}) + table := tablewriter.NewWriter(servoCmd.OutOrStdout()) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) @@ -396,7 +398,30 @@ func (servoCmd *servoCommand) RunServoList(_ *cobra.Command, args []string) erro table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) - table.AppendBulk(data) // Add Bulk Data + + data := [][]string{} + servos, _ := servoCmd.GetServos() + + if servoCmd.verbose { + table.SetHeader([]string{"NAME", "USER", "HOST", "PATH"}) + for _, servo := range servos { + data = append(data, []string{ + servo.Name, + servo.User, + servo.DisplayHost(), + servo.DisplayPath(), + }) + } + } else { + for _, servo := range servos { + data = append(data, []string{ + servo.Name, + servo.URL(), + }) + } + } + + table.AppendBulk(data) table.Render() return nil } diff --git a/command/servo_test.go b/command/servo_test.go index 7067677..427da21 100644 --- a/command/servo_test.go +++ b/command/servo_test.go @@ -274,6 +274,26 @@ func (s *ServoTestSuite) TestRunningServoList() { configFile := test.TempConfigFileWithObj(config) output, err := s.Execute("--config", configFile.Name(), "servo", "list") s.Require().NoError(err) - s.Require().Contains("NAME USER HOST PATH ", output) - s.Require().Contains("opsani-dev blakewatters dev.opsani.com /servo ", output) + s.Require().Contains(output, "opsani-dev ssh://blakewatters@dev.opsani.com:/servo ") +} + +func (s *ServoTestSuite) TestRunningServoListVerbose() { + config := map[string]interface{}{ + "app": "example.com/app", + "token": "123456", + "servos": []map[string]string{ + { + "host": "dev.opsani.com", + "name": "opsani-dev", + "path": "/servo", + "port": "", + "user": "blakewatters", + }, + }, + } + configFile := test.TempConfigFileWithObj(config) + output, err := s.Execute("--config", configFile.Name(), "servo", "list", "-v") + s.Require().NoError(err) + s.Require().Contains(output, "NAME USER HOST PATH ") + s.Require().Contains(output, "opsani-dev blakewatters dev.opsani.com /servo ") } From a9a6031a18b3604f3ddf9f789ac80a34c3ac33f6 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 02:50:46 -0700 Subject: [PATCH 11/33] Add Servo config command --- command/servo.go | 92 +++++++++++++++++++++++++++++++++++++++++-- command/servo_test.go | 7 +++- go.mod | 4 +- go.sum | 11 ++++++ 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/command/servo.go b/command/servo.go index 9b86632..bd7ecc2 100644 --- a/command/servo.go +++ b/command/servo.go @@ -15,6 +15,7 @@ package command import ( + "bytes" "context" "fmt" "net" @@ -22,6 +23,10 @@ import ( "strings" "github.com/AlecAivazis/survey/v2" + "github.com/fatih/color" + "github.com/goccy/go-yaml/lexer" + "github.com/goccy/go-yaml/printer" + "github.com/mattn/go-colorable" "github.com/mitchellh/go-homedir" "github.com/olekukonko/tablewriter" "github.com/prometheus/common/log" @@ -108,6 +113,12 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { }) // Servo Access + servoCmd.AddCommand(&cobra.Command{ + Use: "config", + Short: "Display the Servo config file", + Args: cobra.ExactArgs(1), + RunE: servoCommand.RunServoConfig, + }) logsCmd := &cobra.Command{ Use: "logs", Short: "View logs on a Servo", @@ -358,12 +369,10 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, Auth: []ssh.AuthMethod{ servoCmd.SSHAgent(), }, - // Non-production only HostKeyCallback: hostKeyCallback, } - // // Connect to host - fmt.Printf("Servos: %+v\nServo=%+v\n\n", servos, servo) + // Connect to host client, err := ssh.Dial("tcp", servo.HostAndPort(), config) if err != nil { log.Fatal(err) @@ -454,6 +463,83 @@ func (servoCmd *servoCommand) RunServoRestart(_ *cobra.Command, args []string) e }) } +func (servoCmd *servoCommand) RunServoConfig(_ *cobra.Command, args []string) error { + ctx := context.Background() + outputBuffer := new(bytes.Buffer) + err := servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo Servo, session *ssh.Session) error { + session.Stdout = outputBuffer + session.Stderr = os.Stderr + + sshCmd := make([]string, 3) + if path := servo.Path; path != "" { + sshCmd = append(sshCmd, "cd", path+"&&") + } + sshCmd = append(sshCmd, "cat", "config.yaml") + return session.Run(strings.Join(sshCmd, " ")) + }) + + // We got the config, let's pretty print it + if err == nil { + servoCmd.prettyPrintYAML(outputBuffer.Bytes()) + } + return err +} + +const escape = "\x1b" + +func format(attr color.Attribute) string { + return fmt.Sprintf("%s[%dm", escape, attr) +} + +func (servoCmd *servoCommand) prettyPrintYAML(bytes []byte) error { + tokens := lexer.Tokenize(string(bytes)) + var p printer.Printer + p.LineNumber = true + p.LineNumberFormat = func(num int) string { + fn := color.New(color.Bold, color.FgHiWhite).SprintFunc() + return fn(fmt.Sprintf("%2d | ", num)) + } + p.Bool = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiMagenta), + Suffix: format(color.Reset), + } + } + p.Number = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiMagenta), + Suffix: format(color.Reset), + } + } + p.MapKey = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiCyan), + Suffix: format(color.Reset), + } + } + p.Anchor = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiYellow), + Suffix: format(color.Reset), + } + } + p.Alias = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiYellow), + Suffix: format(color.Reset), + } + } + p.String = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiGreen), + Suffix: format(color.Reset), + } + } + writer := colorable.NewColorableStdout() + writer.Write([]byte(p.PrintTokens(tokens) + "\n")) + return nil +} + func (servoCmd *servoCommand) RunServoLogs(_ *cobra.Command, args []string) error { ctx := context.Background() return servoCmd.runInSSHSession(ctx, args[0], servoCmd.RunLogsSSHSession) diff --git a/command/servo_test.go b/command/servo_test.go index 427da21..8b891e6 100644 --- a/command/servo_test.go +++ b/command/servo_test.go @@ -150,7 +150,12 @@ func (s *ServoTestSuite) TestRunningRemoveHelp() { s.Require().Contains(output, "Remove a Servo") } -// TODO: add -f +func (s *ServoTestSuite) TestRunningConfigHelp() { + output, err := s.Execute("servo", "config", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Display the Servo config file") +} + func (s *ServoTestSuite) TestRunningRemoveServoConfirmed() { configFile := test.TempConfigFileWithObj(map[string]interface{}{ "app": "example.com/app", diff --git a/go.mod b/go.mod index ecab50b..d31a138 100644 --- a/go.mod +++ b/go.mod @@ -19,8 +19,9 @@ require ( github.com/docker/docker v1.13.1 github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/fatih/color v1.9.0 // indirect + github.com/fatih/color v1.9.0 github.com/go-resty/resty/v2 v2.2.0 + github.com/goccy/go-yaml v1.4.3 github.com/golang/protobuf v1.4.0 // indirect github.com/googleapis/gnostic v0.4.1 // indirect github.com/gorilla/mux v1.7.4 // indirect @@ -29,6 +30,7 @@ require ( github.com/imdario/mergo v0.3.9 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/kr/pty v1.1.8 + github.com/mattn/go-colorable v0.1.4 github.com/mattn/go-isatty v0.0.12 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b github.com/mitchellh/go-homedir v1.1.0 diff --git a/go.sum b/go.sum index c24fd5a..9e2e624 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= @@ -98,9 +99,13 @@ github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+ github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-resty/resty/v2 v2.2.0 h1:vgZ1cdblp8Aw4jZj3ZsKh6yKAlMg3CHMrqFSFFd+jgY= github.com/go-resty/resty/v2 v2.2.0/go.mod h1:nYW/8rxqQCmI3bPz9Fsmjbr2FBjGuR2Mzt6kDh3zZ7w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-yaml v1.4.3 h1:+1jK1ost1TBEfWjciIMU8rciBq0poxurgS7XvLgQInM= +github.com/goccy/go-yaml v1.4.3/go.mod h1:PsEEJ29nIFZL07P/c8dv4P6rQkVFFXafQee85U+ERHA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= @@ -194,6 +199,7 @@ github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -204,6 +210,7 @@ github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaa github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= @@ -407,6 +414,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -445,6 +453,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= @@ -461,6 +471,7 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 7c5fe651f7bd75935069d3d331b843aea3175ca3 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 03:09:03 -0700 Subject: [PATCH 12/33] Move config over to displaying colorized YAML --- command/command.go | 59 ++++++++++++++++++++++++++++++++++++++++++++ command/config.go | 9 ++++++- command/servo.go | 61 +--------------------------------------------- 3 files changed, 68 insertions(+), 61 deletions(-) diff --git a/command/command.go b/command/command.go index cf60800..160261b 100644 --- a/command/command.go +++ b/command/command.go @@ -24,8 +24,12 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" + "github.com/fatih/color" "github.com/go-resty/resty/v2" + "github.com/goccy/go-yaml/lexer" + "github.com/goccy/go-yaml/printer" "github.com/hokaccha/go-prettyjson" + "github.com/mattn/go-colorable" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -171,6 +175,61 @@ func (cmd *BaseCommand) PrettyPrintJSONResponse(resp *resty.Response) error { return PrettyPrintJSONObject(result) } +const escape = "\x1b" + +func format(attr color.Attribute) string { + return fmt.Sprintf("%s[%dm", escape, attr) +} + +func (cmd *BaseCommand) prettyPrintYAML(bytes []byte, lineNumbers bool) error { + tokens := lexer.Tokenize(string(bytes)) + var p printer.Printer + p.LineNumber = lineNumbers + p.LineNumberFormat = func(num int) string { + fn := color.New(color.Bold, color.FgHiWhite).SprintFunc() + return fn(fmt.Sprintf("%2d | ", num)) + } + p.Bool = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiMagenta), + Suffix: format(color.Reset), + } + } + p.Number = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiMagenta), + Suffix: format(color.Reset), + } + } + p.MapKey = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiCyan), + Suffix: format(color.Reset), + } + } + p.Anchor = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiYellow), + Suffix: format(color.Reset), + } + } + p.Alias = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiYellow), + Suffix: format(color.Reset), + } + } + p.String = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiGreen), + Suffix: format(color.Reset), + } + } + writer := colorable.NewColorableStdout() + writer.Write([]byte(p.PrintTokens(tokens) + "\n")) + return nil +} + // BaseURL returns the Opsani API base URL func (cmd *BaseCommand) BaseURL() string { return viper.GetString(KeyBaseURL) diff --git a/command/config.go b/command/config.go index 7ac266b..bce5f04 100644 --- a/command/config.go +++ b/command/config.go @@ -15,6 +15,8 @@ package command import ( + "io/ioutil" + "github.com/spf13/cobra" ) @@ -37,5 +39,10 @@ func NewConfigCommand(baseCmd *BaseCommand) *cobra.Command { // RunConfig displays Opsani CLI config info func (configCmd *configCommand) Run(_ *cobra.Command, args []string) error { configCmd.Println("Using config from:", configCmd.ConfigFile) - return configCmd.PrettyPrintJSONObject(configCmd.GetAllSettings()) + + body, err := ioutil.ReadFile(configCmd.ConfigFile) + if err != nil { + return err + } + return configCmd.prettyPrintYAML(body, false) } diff --git a/command/servo.go b/command/servo.go index bd7ecc2..f06f066 100644 --- a/command/servo.go +++ b/command/servo.go @@ -23,10 +23,6 @@ import ( "strings" "github.com/AlecAivazis/survey/v2" - "github.com/fatih/color" - "github.com/goccy/go-yaml/lexer" - "github.com/goccy/go-yaml/printer" - "github.com/mattn/go-colorable" "github.com/mitchellh/go-homedir" "github.com/olekukonko/tablewriter" "github.com/prometheus/common/log" @@ -480,66 +476,11 @@ func (servoCmd *servoCommand) RunServoConfig(_ *cobra.Command, args []string) er // We got the config, let's pretty print it if err == nil { - servoCmd.prettyPrintYAML(outputBuffer.Bytes()) + servoCmd.prettyPrintYAML(outputBuffer.Bytes(), true) } return err } -const escape = "\x1b" - -func format(attr color.Attribute) string { - return fmt.Sprintf("%s[%dm", escape, attr) -} - -func (servoCmd *servoCommand) prettyPrintYAML(bytes []byte) error { - tokens := lexer.Tokenize(string(bytes)) - var p printer.Printer - p.LineNumber = true - p.LineNumberFormat = func(num int) string { - fn := color.New(color.Bold, color.FgHiWhite).SprintFunc() - return fn(fmt.Sprintf("%2d | ", num)) - } - p.Bool = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiMagenta), - Suffix: format(color.Reset), - } - } - p.Number = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiMagenta), - Suffix: format(color.Reset), - } - } - p.MapKey = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiCyan), - Suffix: format(color.Reset), - } - } - p.Anchor = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiYellow), - Suffix: format(color.Reset), - } - } - p.Alias = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiYellow), - Suffix: format(color.Reset), - } - } - p.String = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiGreen), - Suffix: format(color.Reset), - } - } - writer := colorable.NewColorableStdout() - writer.Write([]byte(p.PrintTokens(tokens) + "\n")) - return nil -} - func (servoCmd *servoCommand) RunServoLogs(_ *cobra.Command, args []string) error { ctx := context.Background() return servoCmd.runInSSHSession(ctx, args[0], servoCmd.RunLogsSSHSession) From 3783153ce669efca5d3ab30de806714a83a6a398 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 03:54:09 -0700 Subject: [PATCH 13/33] Cleanup servo subcommand --- command/command.go | 117 ++++++++++++++++++- command/config_test.go | 17 ++- command/servo.go | 259 +++++++++++------------------------------ 3 files changed, 194 insertions(+), 199 deletions(-) diff --git a/command/command.go b/command/command.go index 160261b..4826cb8 100644 --- a/command/command.go +++ b/command/command.go @@ -21,6 +21,7 @@ import ( "net/url" "os" "path/filepath" + "strings" "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" @@ -29,7 +30,6 @@ import ( "github.com/goccy/go-yaml/lexer" "github.com/goccy/go-yaml/printer" "github.com/hokaccha/go-prettyjson" - "github.com/mattn/go-colorable" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -225,8 +225,8 @@ func (cmd *BaseCommand) prettyPrintYAML(bytes []byte, lineNumbers bool) error { Suffix: format(color.Reset), } } - writer := colorable.NewColorableStdout() - writer.Write([]byte(p.PrintTokens(tokens) + "\n")) + // writer := colorable.NewColorableStdout() + cmd.OutOrStdout().Write([]byte(p.PrintTokens(tokens) + "\n")) return nil } @@ -295,3 +295,114 @@ func (cmd *BaseCommand) DebugModeEnabled() bool { func (cmd *BaseCommand) RequestTracingEnabled() bool { return cmd.requestTracingEnabled } + +// Servos returns the Servos in the configuration +func (cmd *BaseCommand) Servos() ([]Servo, error) { + servos := make([]Servo, 0) + err := viper.UnmarshalKey("servos", &servos) + if err != nil { + return nil, err + } + return servos, nil +} + +// lookupServo named returns the Servo with the given name and its index in the config +func (cmd *BaseCommand) lookupServo(name string) (*Servo, int) { + var servo *Servo + servos, err := cmd.Servos() + if err != nil { + return nil, 0 + } + var index int + for i, s := range servos { + if s.Name == name { + servo = &s + index = i + break + } + } + + return servo, index +} + +// ServoNamed named returns the Servo with the given name +func (cmd *BaseCommand) ServoNamed(name string) *Servo { + servo, _ := cmd.lookupServo(name) + return servo +} + +// AddServo adds a Servo to the config +func (cmd *BaseCommand) AddServo(servo Servo) error { + servos, err := cmd.Servos() + if err != nil { + return err + } + + servos = append(servos, servo) + viper.Set("servos", servos) + return viper.WriteConfig() +} + +// RemoveServoNamed removes a Servo from the config with the given name +func (cmd *BaseCommand) RemoveServoNamed(name string) error { + s, index := cmd.lookupServo(name) + if s == nil { + return fmt.Errorf("no such Servo %q", name) + } + servos, err := cmd.Servos() + if err != nil { + return err + } + servos = append(servos[:index], servos[index+1:]...) + viper.Set("servos", servos) + return viper.WriteConfig() +} + +// RemoveServo removes a Servo from the config +func (cmd *BaseCommand) RemoveServo(servo Servo) error { + return cmd.RemoveServoNamed(servo.Name) +} + +// Servo represents a deployed Servo assembly running somewhere +type Servo struct { + Name string + User string + Host string + Port string + Path string +} + +func (s Servo) HostAndPort() string { + h := s.Host + p := s.Port + if p == "" { + p = "22" + } + return strings.Join([]string{h, p}, ":") +} + +func (s Servo) DisplayHost() string { + v := s.Host + if s.Port != "" && s.Port != "22" { + v = v + ":" + s.Port + } + return v +} + +func (s Servo) DisplayPath() string { + if s.Path != "" { + return s.Path + } + return "~/" +} + +func (s Servo) URL() string { + pathComponent := "" + if s.Path != "" { + if s.Port != "" && s.Port != "22" { + pathComponent = pathComponent + ":" + } + pathComponent = pathComponent + s.Path + } + return fmt.Sprintf("ssh://%s@%s:%s", s.User, s.DisplayHost(), pathComponent) +} diff --git a/command/config_test.go b/command/config_test.go index a3bc8a5..42f98e9 100644 --- a/command/config_test.go +++ b/command/config_test.go @@ -17,6 +17,7 @@ package command_test import ( "fmt" "os" + "regexp" "testing" "github.com/opsani/cli/command" @@ -68,11 +69,21 @@ func (s *ConfigTestSuite) TestRunningConfigWithInvalidFile() { s.Require().EqualError(err, "error parsing configuration file: While parsing config: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `malform...` into map[string]interface {}") } +const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" + +var re = regexp.MustCompile(ansi) + +func Strip(str string) string { + return re.ReplaceAllString(str, "") +} + func (s *ConfigTestSuite) TestRunningWithInitializedConfig() { + configFile := test.TempConfigFileWithObj(map[string]interface{}{"app": "example.com/app1", "token": "123456"}) output, err := s.ExecuteArgs(ConfigFileArgs(configFile, "config")) s.Require().NoError(err) - s.Require().Contains(output, `"app": "example.com/app1"`) - s.Require().Contains(output, `"token": "123456"`) - s.Require().Contains(output, fmt.Sprintln("Using config from:", configFile.Name())) + yaml := Strip(output) + s.Require().Contains(yaml, `app: example.com/app1`) + s.Require().Contains(yaml, `token: "123456`) + s.Require().Contains(yaml, fmt.Sprintln("Using config from:", configFile.Name())) } diff --git a/command/servo.go b/command/servo.go index f06f066..2d373e2 100644 --- a/command/servo.go +++ b/command/servo.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "fmt" + "log" "net" "os" "strings" @@ -25,7 +26,6 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/mitchellh/go-homedir" "github.com/olekukonko/tablewriter" - "github.com/prometheus/common/log" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/crypto/ssh" @@ -140,89 +140,12 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { return servoCmd } -// const username = "root" -// const hostname = "3.93.217.12" -// const port = "22" - -// const sshKey = ` -// -----BEGIN OPENSSH PRIVATE KEY----- -// FAKE KEY -// -----END OPENSSH PRIVATE KEY----- -// ` - -// var servos = []map[string]string{ -// { -// "name": "opsani-dev", -// "host": "3.93.217.12", -// "port": "22", -// "user": "root", -// "path": "/root/dev.opsani.com/blake/oco", -// }, -// } - -func (servoCmd *servoCommand) SSHAgent() ssh.AuthMethod { - if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { - return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) - } - return nil -} - -// Servo represents a deployed Servo assembly running somewhere -type Servo struct { - Name string - User string - Host string - Port string - Path string -} - -func (s Servo) HostAndPort() string { - h := s.Host - p := s.Port - if p == "" { - p = "22" - } - return strings.Join([]string{h, p}, ":") -} - -func (s Servo) DisplayHost() string { - v := s.Host - if s.Port != "" && s.Port != "22" { - v = v + ":" + s.Port - } - return v -} - -func (s Servo) DisplayPath() string { - if s.Path != "" { - return s.Path - } - return "~/" -} - -func (s Servo) URL() string { - pathComponent := "" - if s.Path != "" { - if s.Port != "" && s.Port != "22" { - pathComponent = pathComponent + ":" - } - pathComponent = pathComponent + s.Path - } - return fmt.Sprintf("ssh://%s@%s:%s", s.User, s.DisplayHost(), pathComponent) -} - func (servoCmd *servoCommand) RunAddServo(_ *cobra.Command, args []string) error { servo := Servo{} if len(args) > 0 { servo.Name = args[0] } - servos := make([]Servo, 0) - err := viper.UnmarshalKey("servos", &servos) - if err != nil { - return err - } - if servo.Name == "" { err := servoCmd.AskOne(&survey.Input{ Message: "Servo name?", @@ -253,49 +176,18 @@ func (servoCmd *servoCommand) RunAddServo(_ *cobra.Command, args []string) error if servo.Path == "" { err := servoCmd.AskOne(&survey.Input{ Message: "Path? (optional)", - }, &servo.Path, survey.WithValidator(survey.Required)) + }, &servo.Path) if err != nil { return err } } - servos = append(servos, servo) - viper.Set("servos", servos) - if err := viper.WriteConfig(); err != nil { - return err - } - - return nil -} - -// GetServos returns the Servos in the configuration -func (servoCmd *servoCommand) GetServos() ([]Servo, error) { - servos := make([]Servo, 0) - err := viper.UnmarshalKey("servos", &servos) - if err != nil { - return nil, err - } - return servos, nil + return servoCmd.AddServo(servo) } func (servoCmd *servoCommand) RunRemoveServo(_ *cobra.Command, args []string) error { name := args[0] - var servo *Servo - - // Find the target - servos, err := servoCmd.GetServos() - if err != nil { - return err - } - var index int - for i, s := range servos { - if s.Name == name { - servo = &s - index = i - break - } - } - + servo := servoCmd.ServoNamed(name) if servo == nil { return fmt.Errorf("Unable to find Servo named %q", name) } @@ -309,87 +201,12 @@ func (servoCmd *servoCommand) RunRemoveServo(_ *cobra.Command, args []string) er } if confirmed { - servos = append(servos[:index], servos[index+1:]...) - viper.Set("servos", servos) - if err := viper.WriteConfig(); err != nil { - return err - } + return servoCmd.RemoveServo(*servo) } return nil } -func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, runIt func(context.Context, Servo, *ssh.Session) error) error { - - // TODO: Recover from passphrase error - // // signer, err := ssh.ParsePrivateKey([]byte(sshKey)) - // signer, err := ssh.ParsePrivateKey([]byte(sshKey)) - // signer, err := ssh.ParsePrivateKeyWithPassphrase([]byte(sshKey), []byte("THIS_IS_NOT_A_PASSPHRASE")) - // if err != nil { - // log.Base().Fatal(err) - // } - // fmt.Printf("Got signer %+v\n\n", signer) - // key, err := x509.ParsePKCS1PrivateKey(der) - // if err != nil { - // log.Fatal(err) - // } - // signer := ssh.NewSignerFromKey(key) - - var servo *Servo - servos, err := servoCmd.GetServos() - for _, s := range servos { - if s.Name == name { - servo = &s - break - } - } - if servo == nil { - return fmt.Errorf("no such Servo %q", name) - } - - // SSH client config - knownHosts, err := homedir.Expand("~/.ssh/known_hosts") - if err != nil { - return err - } - hostKeyCallback, err := knownhosts.New(knownHosts) - if err != nil { - log.Fatal("could not create hostkeycallback function: ", err) - } - config := &ssh.ClientConfig{ - User: servo.User, - // Auth: []ssh.AuthMethod{ - // // ssh.Password(password), - // ssh.PublicKeys(signer), - // }, - Auth: []ssh.AuthMethod{ - servoCmd.SSHAgent(), - }, - HostKeyCallback: hostKeyCallback, - } - - // Connect to host - client, err := ssh.Dial("tcp", servo.HostAndPort(), config) - if err != nil { - log.Fatal(err) - } - defer client.Close() - - // Create sesssion - session, err := client.NewSession() - if err != nil { - log.Fatal("Failed to create session: ", err) - } - defer session.Close() - - go func() { - <-ctx.Done() - client.Close() - }() - - return runIt(ctx, *servo, session) -} - func (servoCmd *servoCommand) RunServoList(_ *cobra.Command, args []string) error { table := tablewriter.NewWriter(servoCmd.OutOrStdout()) table.SetAutoWrapText(false) @@ -405,7 +222,7 @@ func (servoCmd *servoCommand) RunServoList(_ *cobra.Command, args []string) erro table.SetNoWhiteSpace(true) data := [][]string{} - servos, _ := servoCmd.GetServos() + servos, _ := servoCmd.Servos() if servoCmd.verbose { table.SetHeader([]string{"NAME", "USER", "HOST", "PATH"}) @@ -483,13 +300,13 @@ func (servoCmd *servoCommand) RunServoConfig(_ *cobra.Command, args []string) er func (servoCmd *servoCommand) RunServoLogs(_ *cobra.Command, args []string) error { ctx := context.Background() - return servoCmd.runInSSHSession(ctx, args[0], servoCmd.RunLogsSSHSession) + return servoCmd.runInSSHSession(ctx, args[0], servoCmd.runLogsSSHSession) } // RunConfig displays Opsani CLI config info func (servoCmd *servoCommand) RunServoSSH(_ *cobra.Command, args []string) error { ctx := context.Background() - return servoCmd.runInSSHSession(ctx, args[0], servoCmd.RunShellOnSSHSession) + return servoCmd.runInSSHSession(ctx, args[0], servoCmd.runShellOnSSHSession) } func (servoCmd *servoCommand) runDockerComposeOverSSH(cmd string, args []string, servo Servo, session *ssh.Session) error { @@ -503,7 +320,7 @@ func (servoCmd *servoCommand) runDockerComposeOverSSH(cmd string, args []string, return session.Run(strings.Join(args, " ")) } -func (servoCmd *servoCommand) RunLogsSSHSession(ctx context.Context, servo Servo, session *ssh.Session) error { +func (servoCmd *servoCommand) runLogsSSHSession(ctx context.Context, servo Servo, session *ssh.Session) error { session.Stdout = os.Stdout session.Stderr = os.Stderr @@ -522,7 +339,7 @@ func (servoCmd *servoCommand) RunLogsSSHSession(ctx context.Context, servo Servo return session.Run(strings.Join(args, " ")) } -func (servoCmd *servoCommand) RunShellOnSSHSession(ctx context.Context, servo Servo, session *ssh.Session) error { +func (servoCmd *servoCommand) runShellOnSSHSession(ctx context.Context, servo Servo, session *ssh.Session) error { fd := int(os.Stdin.Fd()) state, err := terminal.MakeRaw(fd) if err != nil { @@ -569,3 +386,59 @@ func (servoCmd *servoCommand) RunShellOnSSHSession(ctx context.Context, servo Se return err } + +/// +/// SSH Primitives +/// + +func (servoCmd *servoCommand) sshAgent() ssh.AuthMethod { + if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { + return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) + } + return nil +} + +func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, runIt func(context.Context, Servo, *ssh.Session) error) error { + servo := servoCmd.ServoNamed(name) + if servo == nil { + return fmt.Errorf("no such Servo %q", name) + } + + // SSH client config + knownHosts, err := homedir.Expand("~/.ssh/known_hosts") + if err != nil { + return err + } + hostKeyCallback, err := knownhosts.New(knownHosts) + if err != nil { + log.Fatal("could not create hostkeycallback function: ", err) + } + config := &ssh.ClientConfig{ + User: servo.User, + Auth: []ssh.AuthMethod{ + servoCmd.sshAgent(), + }, + HostKeyCallback: hostKeyCallback, + } + + // Connect to host + client, err := ssh.Dial("tcp", servo.HostAndPort(), config) + if err != nil { + log.Fatal(err) + } + defer client.Close() + + // Create sesssion + session, err := client.NewSession() + if err != nil { + log.Fatal("Failed to create session: ", err) + } + defer session.Close() + + go func() { + <-ctx.Done() + client.Close() + }() + + return runIt(ctx, *servo, session) +} From 278452a89a318ed1b32be8a5915a0e67c181c28b Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 04:36:15 -0700 Subject: [PATCH 14/33] Use a private viper and add option for controlling colored output --- command/app_config_test.go | 2 - command/app_lifecycle_test.go | 2 - command/app_test.go | 2 - command/command.go | 108 +++++++++++++++++++--------------- command/completion_test.go | 2 - command/config_test.go | 2 - command/init.go | 7 +-- command/init_test.go | 12 ++-- command/login_test.go | 2 - command/root.go | 40 +++++++------ command/servo.go | 13 ++-- command/servo_test.go | 16 ++--- integration/config_test.go | 5 +- integration/init_test.go | 2 - 14 files changed, 102 insertions(+), 113 deletions(-) diff --git a/command/app_config_test.go b/command/app_config_test.go index ea9bfd3..7207a27 100644 --- a/command/app_config_test.go +++ b/command/app_config_test.go @@ -19,7 +19,6 @@ import ( "github.com/opsani/cli/command" "github.com/opsani/cli/test" - "github.com/spf13/viper" "github.com/stretchr/testify/suite" ) @@ -32,7 +31,6 @@ func TestAppConfigTestSuite(t *testing.T) { } func (s *AppConfigTestSuite) SetupTest() { - viper.Reset() s.SetCommand(command.NewRootCommand()) } diff --git a/command/app_lifecycle_test.go b/command/app_lifecycle_test.go index c8a6cea..26091ba 100644 --- a/command/app_lifecycle_test.go +++ b/command/app_lifecycle_test.go @@ -19,7 +19,6 @@ import ( "github.com/opsani/cli/command" "github.com/opsani/cli/test" - "github.com/spf13/viper" "github.com/stretchr/testify/suite" ) @@ -32,7 +31,6 @@ func TestAppLifecycleTestSuite(t *testing.T) { } func (s *AppLifecycleTestSuite) SetupTest() { - viper.Reset() s.SetCommand(command.NewRootCommand()) } diff --git a/command/app_test.go b/command/app_test.go index bd1fece..bfa1ee1 100644 --- a/command/app_test.go +++ b/command/app_test.go @@ -19,7 +19,6 @@ import ( "github.com/opsani/cli/command" "github.com/opsani/cli/test" - "github.com/spf13/viper" "github.com/stretchr/testify/suite" ) @@ -32,7 +31,6 @@ func TestAppTestSuite(t *testing.T) { } func (s *AppTestSuite) SetupTest() { - viper.Reset() s.SetCommand(command.NewRootCommand()) } diff --git a/command/command.go b/command/command.go index 4826cb8..b759fb9 100644 --- a/command/command.go +++ b/command/command.go @@ -49,12 +49,13 @@ func SetStdio(stdio terminal.Stdio) { // It contains the root command for Cobra and is designed for embedding // into other command structures to add subcommand functionality type BaseCommand struct { - rootCmd *cobra.Command - // viper *viper + rootCmd *cobra.Command + viperCfg *viper.Viper ConfigFile string requestTracingEnabled bool debugModeEnabled bool + disableColors bool } // stdio is a test helper for returning terminal file descriptors usable by Survey @@ -185,46 +186,49 @@ func (cmd *BaseCommand) prettyPrintYAML(bytes []byte, lineNumbers bool) error { tokens := lexer.Tokenize(string(bytes)) var p printer.Printer p.LineNumber = lineNumbers - p.LineNumberFormat = func(num int) string { - fn := color.New(color.Bold, color.FgHiWhite).SprintFunc() - return fn(fmt.Sprintf("%2d | ", num)) - } - p.Bool = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiMagenta), - Suffix: format(color.Reset), + if cmd.ColorOutput() { + p.LineNumberFormat = func(num int) string { + fn := color.New(color.Bold, color.FgHiWhite).SprintFunc() + return fn(fmt.Sprintf("%2d | ", num)) } - } - p.Number = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiMagenta), - Suffix: format(color.Reset), + p.Bool = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiMagenta), + Suffix: format(color.Reset), + } } - } - p.MapKey = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiCyan), - Suffix: format(color.Reset), + p.Number = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiMagenta), + Suffix: format(color.Reset), + } } - } - p.Anchor = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiYellow), - Suffix: format(color.Reset), + p.MapKey = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiCyan), + Suffix: format(color.Reset), + } } - } - p.Alias = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiYellow), - Suffix: format(color.Reset), + p.Anchor = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiYellow), + Suffix: format(color.Reset), + } } - } - p.String = func() *printer.Property { - return &printer.Property{ - Prefix: format(color.FgHiGreen), - Suffix: format(color.Reset), + p.Alias = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiYellow), + Suffix: format(color.Reset), + } + } + p.String = func() *printer.Property { + return &printer.Property{ + Prefix: format(color.FgHiGreen), + Suffix: format(color.Reset), + } } } + // writer := colorable.NewColorableStdout() cmd.OutOrStdout().Write([]byte(p.PrintTokens(tokens) + "\n")) return nil @@ -232,7 +236,7 @@ func (cmd *BaseCommand) prettyPrintYAML(bytes []byte, lineNumbers bool) error { // BaseURL returns the Opsani API base URL func (cmd *BaseCommand) BaseURL() string { - return viper.GetString(KeyBaseURL) + return cmd.viperCfg.GetString(KeyBaseURL) } // BaseURLHostnameAndPort returns the hostname and port portion of Opsani base URL for summary display @@ -250,27 +254,27 @@ func (cmd *BaseCommand) BaseURLHostnameAndPort() string { // SetBaseURL sets the Opsani API base URL func (cmd *BaseCommand) SetBaseURL(baseURL string) { - viper.Set(KeyBaseURL, baseURL) + cmd.viperCfg.Set(KeyBaseURL, baseURL) } // AccessToken returns the Opsani API access token func (cmd *BaseCommand) AccessToken() string { - return viper.GetString(KeyToken) + return cmd.viperCfg.GetString(KeyToken) } // SetAccessToken sets the Opsani API access token func (cmd *BaseCommand) SetAccessToken(accessToken string) { - viper.Set(KeyToken, accessToken) + cmd.viperCfg.Set(KeyToken, accessToken) } // App returns the target Opsani app func (cmd *BaseCommand) App() string { - return viper.GetString(KeyApp) + return cmd.viperCfg.GetString(KeyApp) } // SetApp sets the target Opsani app func (cmd *BaseCommand) SetApp(app string) { - viper.Set(KeyApp, app) + cmd.viperCfg.Set(KeyApp, app) } // AppComponents returns the organization name and app ID as separate path components @@ -283,7 +287,7 @@ func (cmd *BaseCommand) AppComponents() (orgSlug string, appSlug string) { // AllSettings returns all configuration settings func (cmd *BaseCommand) AllSettings() map[string]interface{} { - return viper.AllSettings() + return cmd.viperCfg.AllSettings() } // DebugModeEnabled returns a boolean value indicating if debugging is enabled @@ -296,10 +300,20 @@ func (cmd *BaseCommand) RequestTracingEnabled() bool { return cmd.requestTracingEnabled } +// ColorOutput indicates if ANSI colors will be used for output +func (cmd *BaseCommand) ColorOutput() bool { + return !cmd.disableColors +} + +// SetColorOutput sets whether or not ANSI colors will be used for output +func (cmd *BaseCommand) SetColorOutput(colorOutput bool) { + cmd.disableColors = !colorOutput +} + // Servos returns the Servos in the configuration func (cmd *BaseCommand) Servos() ([]Servo, error) { servos := make([]Servo, 0) - err := viper.UnmarshalKey("servos", &servos) + err := cmd.viperCfg.UnmarshalKey("servos", &servos) if err != nil { return nil, err } @@ -339,8 +353,8 @@ func (cmd *BaseCommand) AddServo(servo Servo) error { } servos = append(servos, servo) - viper.Set("servos", servos) - return viper.WriteConfig() + cmd.viperCfg.Set("servos", servos) + return cmd.viperCfg.WriteConfig() } // RemoveServoNamed removes a Servo from the config with the given name @@ -354,8 +368,8 @@ func (cmd *BaseCommand) RemoveServoNamed(name string) error { return err } servos = append(servos[:index], servos[index+1:]...) - viper.Set("servos", servos) - return viper.WriteConfig() + cmd.viperCfg.Set("servos", servos) + return cmd.viperCfg.WriteConfig() } // RemoveServo removes a Servo from the config diff --git a/command/completion_test.go b/command/completion_test.go index e741576..3b0d414 100644 --- a/command/completion_test.go +++ b/command/completion_test.go @@ -19,7 +19,6 @@ import ( "github.com/opsani/cli/command" "github.com/opsani/cli/test" - "github.com/spf13/viper" "github.com/stretchr/testify/suite" ) @@ -32,7 +31,6 @@ func TestCompletionTestSuite(t *testing.T) { } func (s *CompletionTestSuite) SetupTest() { - viper.Reset() s.SetCommand(command.NewRootCommand()) } diff --git a/command/config_test.go b/command/config_test.go index 42f98e9..50f1509 100644 --- a/command/config_test.go +++ b/command/config_test.go @@ -22,7 +22,6 @@ import ( "github.com/opsani/cli/command" "github.com/opsani/cli/test" - "github.com/spf13/viper" "github.com/stretchr/testify/suite" ) @@ -39,7 +38,6 @@ func TestConfigTestSuite(t *testing.T) { } func (s *ConfigTestSuite) SetupTest() { - viper.Reset() s.SetCommand(command.NewRootCommand()) } diff --git a/command/init.go b/command/init.go index 3156c14..75679c0 100644 --- a/command/init.go +++ b/command/init.go @@ -23,7 +23,6 @@ import ( "github.com/AlecAivazis/survey/v2/terminal" "github.com/mgutz/ansi" "github.com/spf13/cobra" - "github.com/spf13/viper" ) const confirmedArg = "confirmed" @@ -34,10 +33,6 @@ type initCommand struct { confirmed bool } -/// -/// TODO: Swap out config accessors -/// - // RunInitCommand initializes Opsani CLI config func (initCmd *initCommand) RunInitCommand(_ *cobra.Command, args []string) error { // Handle reinitialization case @@ -105,7 +100,7 @@ func (initCmd *initCommand) RunInitCommand(_ *cobra.Command, args []string) erro return err } } - if err := viper.WriteConfigAs(initCmd.ConfigFile); err != nil { + if err := initCmd.viperCfg.WriteConfigAs(initCmd.ConfigFile); err != nil { return err } initCmd.Println("\nOpsani CLI initialized") diff --git a/command/init_test.go b/command/init_test.go index af16439..67b7fa9 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -20,12 +20,10 @@ import ( "testing" "github.com/AlecAivazis/survey/v2" - "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" "github.com/Netflix/go-expect" "github.com/opsani/cli/command" "github.com/opsani/cli/test" - "github.com/spf13/viper" "github.com/stretchr/testify/suite" "gopkg.in/yaml.v2" ) @@ -39,9 +37,6 @@ func TestInitTestSuite(t *testing.T) { } func (s *InitTestSuite) SetupTest() { - // Colors make the tests flaky - core.DisableColor = true - viper.Reset() s.SetCommand(command.NewRootCommand()) } @@ -95,7 +90,7 @@ func (s *InitTestSuite) TestInitWithExistingConfigDeclinedL() { context, err := s.ExecuteTestInteractively(test.Args("--config", configFile.Name(), "init"), func(t *test.InteractiveTestContext) error { t.RequireStringf("Using config from: %s", configFile.Name()) - t.RequireStringf("? Existing config found. Overwrite %s?", configFile.Name()) + t.RequireStringf("Existing config found. Overwrite %s?", configFile.Name()) t.SendLine("N") t.ExpectEOF() return nil @@ -113,7 +108,7 @@ func (s *InitTestSuite) TestInitWithExistingConfigDeclined() { context, err := s.ExecuteTestInteractively(test.Args("--config", configFile.Name(), "init"), func(t *test.InteractiveTestContext) error { t.RequireStringf("Using config from: %s", configFile.Name()) - t.RequireStringf("? Existing config found. Overwrite %s?", configFile.Name()) + t.RequireStringf("Existing config found. Overwrite %s?", configFile.Name()) t.SendLine("N") t.ExpectEOF() return nil @@ -131,7 +126,7 @@ func (s *InitTestSuite) TestInitWithExistingConfigAccepted() { context, err := s.ExecuteTestInteractively(test.Args("--config", configFile.Name(), "init"), func(t *test.InteractiveTestContext) error { t.RequireStringf("Using config from: %s", configFile.Name()) - t.RequireStringf("? Existing config found. Overwrite %s?", configFile.Name()) + t.RequireStringf("Existing config found. Overwrite %s?", configFile.Name()) t.SendLine("Y") t.ExpectMatch(expect.RegexpPattern("Opsani app")) t.SendLine("dev.opsani.com/amazing-app") @@ -144,6 +139,7 @@ func (s *InitTestSuite) TestInitWithExistingConfigAccepted() { return nil }) s.Require().NoError(err, context.OutputBuffer().String()) + s.T().Logf("Output buffer = %v", context.OutputBuffer().String()) // Check the config file var config = map[string]interface{}{} diff --git a/command/login_test.go b/command/login_test.go index e292c5a..b6667de 100644 --- a/command/login_test.go +++ b/command/login_test.go @@ -19,7 +19,6 @@ import ( "github.com/opsani/cli/command" "github.com/opsani/cli/test" - "github.com/spf13/viper" "github.com/stretchr/testify/suite" ) @@ -32,7 +31,6 @@ func TestLoginTestSuite(t *testing.T) { } func (s *LoginTestSuite) SetupTest() { - viper.Reset() s.SetCommand(command.NewRootCommand()) } diff --git a/command/root.go b/command/root.go index f008511..4a19aa0 100644 --- a/command/root.go +++ b/command/root.go @@ -22,6 +22,7 @@ import ( "path/filepath" "strings" + "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" "github.com/mitchellh/go-homedir" "github.com/opsani/cli/opsani" @@ -46,9 +47,8 @@ const ( // NewRootCommand returns a new instance of the root command for Opsani CLI func NewRootCommand() *BaseCommand { // Create our base command to bind configuration - // viper := viper.New() - // baseCommand := &BaseCommand{viper: viper} - rootCmd := &BaseCommand{} + viperCfg := viper.New() + rootCmd := &BaseCommand{viperCfg: viperCfg} cobraCmd := &cobra.Command{ Use: "opsani", @@ -71,17 +71,18 @@ We'd love to hear your feedback at `, // Bind our global configuration parameters cobraCmd.PersistentFlags().String(KeyBaseURL, DefaultBaseURL, "Base URL for accessing the Opsani API") cobraCmd.PersistentFlags().MarkHidden(KeyBaseURL) - viper.BindPFlag(KeyBaseURL, cobraCmd.PersistentFlags().Lookup(KeyBaseURL)) + viperCfg.BindPFlag(KeyBaseURL, cobraCmd.PersistentFlags().Lookup(KeyBaseURL)) cobraCmd.PersistentFlags().String(KeyApp, "", "App to control (overrides config file and OPSANI_APP)") - viper.BindPFlag(KeyApp, cobraCmd.PersistentFlags().Lookup(KeyApp)) + viperCfg.BindPFlag(KeyApp, cobraCmd.PersistentFlags().Lookup(KeyApp)) cobraCmd.PersistentFlags().String(KeyToken, "", "API token to authenticate with (overrides config file and OPSANI_TOKEN)") - viper.BindPFlag(KeyToken, cobraCmd.PersistentFlags().Lookup(KeyToken)) + viperCfg.BindPFlag(KeyToken, cobraCmd.PersistentFlags().Lookup(KeyToken)) // Not stored in Viper cobraCmd.PersistentFlags().BoolVarP(&rootCmd.debugModeEnabled, KeyDebugMode, "D", false, "Enable debug mode") cobraCmd.PersistentFlags().BoolVar(&rootCmd.requestTracingEnabled, KeyRequestTracing, false, "Enable request tracing") + cobraCmd.PersistentFlags().BoolVar(&rootCmd.disableColors, "no-colors", false, "Disable colorized output") configFileUsage := fmt.Sprintf("Location of config file (default \"%s\")", rootCmd.DefaultConfigFile()) cobraCmd.PersistentFlags().StringVar(&rootCmd.ConfigFile, "config", "", configFileUsage) @@ -208,8 +209,6 @@ func ReduceRunEFuncs(runFuncs ...RunEFunc) RunEFunc { } } -// TODO: Move all of these onto BaseCommand as methods - // InitConfigRunE initializes client configuration and aborts execution if an error is encountered func (baseCmd *BaseCommand) InitConfigRunE(cmd *cobra.Command, args []string) error { return baseCmd.initConfig() @@ -240,23 +239,23 @@ func (baseCmd *BaseCommand) RequireInitRunE(cmd *cobra.Command, args []string) e func (baseCmd *BaseCommand) initConfig() error { if baseCmd.ConfigFile != "" { - viper.SetConfigFile(baseCmd.ConfigFile) + baseCmd.viperCfg.SetConfigFile(baseCmd.ConfigFile) } else { // Find Opsani config in home directory - viper.AddConfigPath(baseCmd.DefaultConfigPath()) - viper.SetConfigName("config") - viper.SetConfigType(baseCmd.DefaultConfigType()) + baseCmd.viperCfg.AddConfigPath(baseCmd.DefaultConfigPath()) + baseCmd.viperCfg.SetConfigName("config") + baseCmd.viperCfg.SetConfigType(baseCmd.DefaultConfigType()) baseCmd.ConfigFile = baseCmd.DefaultConfigFile() } // Set up environment variables - viper.SetEnvPrefix(KeyEnvPrefix) - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - viper.AutomaticEnv() + baseCmd.viperCfg.SetEnvPrefix(KeyEnvPrefix) + baseCmd.viperCfg.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + baseCmd.viperCfg.AutomaticEnv() // Load the configuration - if err := viper.ReadInConfig(); err == nil { - baseCmd.ConfigFile = viper.ConfigFileUsed() + if err := baseCmd.viperCfg.ReadInConfig(); err == nil { + baseCmd.ConfigFile = baseCmd.viperCfg.ConfigFileUsed() } else { switch err.(type) { @@ -267,6 +266,9 @@ func (baseCmd *BaseCommand) initConfig() error { return fmt.Errorf("error parsing configuration file: %w", err) } } + + core.DisableColor = baseCmd.disableColors + return nil } @@ -323,7 +325,7 @@ func (baseCmd *BaseCommand) DefaultConfigType() string { // GetBaseURL returns the Opsani API base URL func (baseCmd *BaseCommand) GetBaseURL() string { - return viper.GetString(KeyBaseURL) + return baseCmd.viperCfg.GetString(KeyBaseURL) } // GetAppComponents returns the organization name and app ID as separate path components @@ -336,7 +338,7 @@ func (baseCmd *BaseCommand) GetAppComponents() (orgSlug string, appSlug string) // GetAllSettings returns all configuration settings func (baseCmd *BaseCommand) GetAllSettings() map[string]interface{} { - return viper.AllSettings() + return baseCmd.viperCfg.AllSettings() } // IsInitialized returns a boolean value that indicates if the client has been initialized diff --git a/command/servo.go b/command/servo.go index 2d373e2..8a15a01 100644 --- a/command/servo.go +++ b/command/servo.go @@ -27,7 +27,6 @@ import ( "github.com/mitchellh/go-homedir" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" - "github.com/spf13/viper" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" "golang.org/x/crypto/ssh/knownhosts" @@ -123,11 +122,11 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { } logsCmd.Flags().BoolP("follow", "f", false, "Follow log output") - viper.BindPFlag("follow", logsCmd.Flags().Lookup("follow")) + baseCmd.viperCfg.BindPFlag("follow", logsCmd.Flags().Lookup("follow")) logsCmd.Flags().BoolP("timestamps", "t", false, "Show timestamps") - viper.BindPFlag("timestamps", logsCmd.Flags().Lookup("timestamps")) + baseCmd.viperCfg.BindPFlag("timestamps", logsCmd.Flags().Lookup("timestamps")) logsCmd.Flags().StringP("lines", "l", "25", `Number of lines to show from the end of the logs (or "all").`) - viper.BindPFlag("lines", logsCmd.Flags().Lookup("lines")) + baseCmd.viperCfg.BindPFlag("lines", logsCmd.Flags().Lookup("lines")) servoCmd.AddCommand(logsCmd) servoCmd.AddCommand(&cobra.Command{ @@ -329,11 +328,11 @@ func (servoCmd *servoCommand) runLogsSSHSession(ctx context.Context, servo Servo args = append(args, "cd", path+"&&") } args = append(args, "docker-compose logs") - args = append(args, "--tail "+viper.GetString("lines")) - if viper.GetBool("follow") { + args = append(args, "--tail "+servoCmd.viperCfg.GetString("lines")) + if servoCmd.viperCfg.GetBool("follow") { args = append(args, "--follow") } - if viper.GetBool("timestamps") { + if servoCmd.viperCfg.GetBool("timestamps") { args = append(args, "--timestamps") } return session.Run(strings.Join(args, " ")) diff --git a/command/servo_test.go b/command/servo_test.go index 8b891e6..cd54cc4 100644 --- a/command/servo_test.go +++ b/command/servo_test.go @@ -18,10 +18,8 @@ import ( "io/ioutil" "testing" - "github.com/AlecAivazis/survey/v2/core" "github.com/opsani/cli/command" "github.com/opsani/cli/test" - "github.com/spf13/viper" "github.com/stretchr/testify/suite" "gopkg.in/yaml.v2" ) @@ -35,8 +33,6 @@ func TestServoTestSuite(t *testing.T) { } func (s *ServoTestSuite) SetupTest() { - viper.Reset() - core.DisableColor = true s.SetCommand(command.NewRootCommand()) } @@ -110,13 +106,13 @@ func (s *ServoTestSuite) TestRunningAddNoInput() { }) args := test.Args("--config", configFile.Name(), "servo", "add") context, err := s.ExecuteTestInteractively(args, func(t *test.InteractiveTestContext) error { - t.RequireString("? Servo name?") + t.RequireString("Servo name?") t.SendLine("opsani-dev") - t.RequireString("? User?") + t.RequireString("User?") t.SendLine("blakewatters") - t.RequireString("? Host?") + t.RequireString("Host?") t.SendLine("dev.opsani.com") - t.RequireString("? Path? (optional)") + t.RequireString("Path? (optional)") t.SendLine("/servo") t.ExpectEOF() return nil @@ -172,7 +168,7 @@ func (s *ServoTestSuite) TestRunningRemoveServoConfirmed() { }) args := test.Args("--config", configFile.Name(), "servo", "remove", "opsani-dev") _, err := s.ExecuteTestInteractively(args, func(t *test.InteractiveTestContext) error { - t.RequireString(`? Remove Servo "opsani-dev"?`) + t.RequireString(`Remove Servo "opsani-dev"?`) t.SendLine("Y") t.ExpectEOF() return nil @@ -237,7 +233,7 @@ func (s *ServoTestSuite) TestRunningRemoveServoDeclined() { }) args := test.Args("--config", configFile.Name(), "servo", "remove", "opsani-dev") _, err := s.ExecuteTestInteractively(args, func(t *test.InteractiveTestContext) error { - t.RequireString(`? Remove Servo "opsani-dev"?`) + t.RequireString(`Remove Servo "opsani-dev"?`) t.SendLine("N") t.ExpectEOF() return nil diff --git a/integration/config_test.go b/integration/config_test.go index 29834f8..147efae 100644 --- a/integration/config_test.go +++ b/integration/config_test.go @@ -59,13 +59,14 @@ func (s *ConfigTestSuite) TestRunningConfigInitialized() { WriteConfigFile(defaultConfig) cmd := exec.Command(opsaniBinaryPath, "--config", opsaniConfigPath, + "--no-colors", "config", ) output, err := cmd.CombinedOutput() s.Require().NoError(err) - s.Require().Contains(string(output), `"app": "example.com/app1"`) - s.Require().Contains(string(output), `"token": "123456"`) + s.Require().Contains(string(output), `app: example.com/app1`) + s.Require().Contains(string(output), `token: "123456`) } func (s *ConfigTestSuite) TestRunningConfigFileInvalidData() { diff --git a/integration/init_test.go b/integration/init_test.go index e5cd0f8..374eb3a 100644 --- a/integration/init_test.go +++ b/integration/init_test.go @@ -25,7 +25,6 @@ import ( "runtime" "testing" - "github.com/spf13/viper" "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" ) @@ -42,7 +41,6 @@ var ( ) func TestMain(m *testing.M) { - viper.Reset() tmpDir, err := ioutil.TempDir("", "integration-opsani") if err != nil { panic("failed to create temp dir") From 572474b78272d7201d359fcab839879fd3a15165 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 04:48:39 -0700 Subject: [PATCH 15/33] Respect NO_COLOR --- command/root.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/command/root.go b/command/root.go index 4a19aa0..cbce768 100644 --- a/command/root.go +++ b/command/root.go @@ -82,7 +82,11 @@ We'd love to hear your feedback at `, // Not stored in Viper cobraCmd.PersistentFlags().BoolVarP(&rootCmd.debugModeEnabled, KeyDebugMode, "D", false, "Enable debug mode") cobraCmd.PersistentFlags().BoolVar(&rootCmd.requestTracingEnabled, KeyRequestTracing, false, "Enable request tracing") - cobraCmd.PersistentFlags().BoolVar(&rootCmd.disableColors, "no-colors", false, "Disable colorized output") + + // Respect NO_COLOR from env to be a good sport + // https://no-color.org/ + _, disableColors := os.LookupEnv("NO_COLOR") + cobraCmd.PersistentFlags().BoolVar(&rootCmd.disableColors, "no-colors", disableColors, "Disable colorized output") configFileUsage := fmt.Sprintf("Location of config file (default \"%s\")", rootCmd.DefaultConfigFile()) cobraCmd.PersistentFlags().StringVar(&rootCmd.ConfigFile, "config", "", configFileUsage) From bf258a751d5ed8e6e2adc59902a3ddb39df9662d Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 05:44:26 -0700 Subject: [PATCH 16/33] Implemented bastion support --- command/command.go | 21 ++++++++++++++++----- command/servo.go | 47 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/command/command.go b/command/command.go index b759fb9..40b88ee 100644 --- a/command/command.go +++ b/command/command.go @@ -379,11 +379,12 @@ func (cmd *BaseCommand) RemoveServo(servo Servo) error { // Servo represents a deployed Servo assembly running somewhere type Servo struct { - Name string - User string - Host string - Port string - Path string + Name string + User string + Host string + Port string + Path string + Bastion string } func (s Servo) HostAndPort() string { @@ -420,3 +421,13 @@ func (s Servo) URL() string { } return fmt.Sprintf("ssh://%s@%s:%s", s.User, s.DisplayHost(), pathComponent) } + +func (s Servo) BastionComponents() (string, string) { + components := strings.Split(s.Bastion, "@") + user := components[0] + host := components[1] + if !strings.Contains(host, ":") { + host = host + ":22" + } + return user, host +} diff --git a/command/servo.go b/command/servo.go index 8a15a01..7612d74 100644 --- a/command/servo.go +++ b/command/servo.go @@ -420,15 +420,48 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, HostKeyCallback: hostKeyCallback, } - // Connect to host - client, err := ssh.Dial("tcp", servo.HostAndPort(), config) - if err != nil { - log.Fatal(err) + // Support bastion hosts via redialing + var sshClient *ssh.Client + if servo.Bastion != "" { + user, host := servo.BastionComponents() + bastionConfig := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + servoCmd.sshAgent(), + }, + HostKeyCallback: hostKeyCallback, + } + + // Dial the bastion host + bastionClient, err := ssh.Dial("tcp", host, bastionConfig) + if err != nil { + log.Fatal(err) + } + + // Establish a new connection thrrough the bastion + conn, err := bastionClient.Dial("tcp", servo.HostAndPort()) + if err != nil { + log.Fatal(err) + } + + // Build a new SSH connection on top of the bastion connection + ncc, chans, reqs, err := ssh.NewClientConn(conn, servo.HostAndPort(), config) + if err != nil { + log.Fatal(err) + } + + // Now connection a client on top of it + sshClient = ssh.NewClient(ncc, chans, reqs) + } else { + sshClient, err = ssh.Dial("tcp", servo.HostAndPort(), config) + if err != nil { + log.Fatal(err) + } } - defer client.Close() + defer sshClient.Close() // Create sesssion - session, err := client.NewSession() + session, err := sshClient.NewSession() if err != nil { log.Fatal("Failed to create session: ", err) } @@ -436,7 +469,7 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, go func() { <-ctx.Done() - client.Close() + sshClient.Close() }() return runIt(ctx, *servo, session) From 3e4dfc5b5f4b59f014b4c9e73752e3cb2a8e66bf Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 05:50:42 -0700 Subject: [PATCH 17/33] Support bastions in servo list --- command/servo.go | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/command/servo.go b/command/servo.go index 7612d74..dce09cd 100644 --- a/command/servo.go +++ b/command/servo.go @@ -224,21 +224,33 @@ func (servoCmd *servoCommand) RunServoList(_ *cobra.Command, args []string) erro servos, _ := servoCmd.Servos() if servoCmd.verbose { - table.SetHeader([]string{"NAME", "USER", "HOST", "PATH"}) + headers := []string{"NAME", "USER", "HOST", "PATH"} for _, servo := range servos { - data = append(data, []string{ + row := []string{ servo.Name, servo.User, servo.DisplayHost(), servo.DisplayPath(), - }) + } + if servo.Bastion != "" { + row = append(row, servo.Bastion) + if len(headers) == 4 { + headers = append(headers, "BASTION") + } + } + data = append(data, row) } + table.SetHeader(headers) } else { for _, servo := range servos { - data = append(data, []string{ + row := []string{ servo.Name, servo.URL(), - }) + } + if servo.Bastion != "" { + row = append(row, fmt.Sprintf("(via %s)", servo.Bastion)) + } + data = append(data, row) } } From 01c358410cdbfb38a8c4f340e402be685aa5a7c0 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 06:58:41 -0700 Subject: [PATCH 18/33] Bastion host add support, more cleanups --- command/command.go | 24 ++++++++++---------- command/root.go | 6 ++--- command/servo.go | 45 ++++++++++++++++++++++++------------ command/servo_test.go | 53 +++++++++++++++++++++++++++++++++++++++---- test/suite.go | 2 +- 5 files changed, 95 insertions(+), 35 deletions(-) diff --git a/command/command.go b/command/command.go index 40b88ee..8b9df5b 100644 --- a/command/command.go +++ b/command/command.go @@ -49,8 +49,8 @@ func SetStdio(stdio terminal.Stdio) { // It contains the root command for Cobra and is designed for embedding // into other command structures to add subcommand functionality type BaseCommand struct { - rootCmd *cobra.Command - viperCfg *viper.Viper + rootCobraCommand *cobra.Command + viperCfg *viper.Viper ConfigFile string requestTracingEnabled bool @@ -71,9 +71,9 @@ func (cmd *BaseCommand) stdio() terminal.Stdio { } } -// CobraCommand returns the Cobra instance underlying the Opsani CLI command -func (cmd *BaseCommand) CobraCommand() *cobra.Command { - return cmd.rootCmd +// RootCobraCommand returns the root Cobra command of the Opsani CLI command +func (cmd *BaseCommand) RootCobraCommand() *cobra.Command { + return cmd.rootCobraCommand } // Viper returns the Viper configuration object underlying the Opsani CLI command @@ -85,37 +85,37 @@ func (cmd *BaseCommand) CobraCommand() *cobra.Command { // OutOrStdout returns output to stdout. func (cmd *BaseCommand) OutOrStdout() io.Writer { - return cmd.rootCmd.OutOrStdout() + return cmd.rootCobraCommand.OutOrStdout() } // Print is a convenience method to Print to the defined output, fallback to Stderr if not set. func (cmd *BaseCommand) Print(i ...interface{}) { - cmd.rootCmd.Print(i...) + cmd.rootCobraCommand.Print(i...) } // Println is a convenience method to Println to the defined output, fallback to Stderr if not set. func (cmd *BaseCommand) Println(i ...interface{}) { - cmd.rootCmd.Println(i...) + cmd.rootCobraCommand.Println(i...) } // Printf is a convenience method to Printf to the defined output, fallback to Stderr if not set. func (cmd *BaseCommand) Printf(format string, i ...interface{}) { - cmd.rootCmd.Printf(format, i...) + cmd.rootCobraCommand.Printf(format, i...) } // PrintErr is a convenience method to Print to the defined Err output, fallback to Stderr if not set. func (cmd *BaseCommand) PrintErr(i ...interface{}) { - cmd.rootCmd.PrintErr(i...) + cmd.rootCobraCommand.PrintErr(i...) } // PrintErrln is a convenience method to Println to the defined Err output, fallback to Stderr if not set. func (cmd *BaseCommand) PrintErrln(i ...interface{}) { - cmd.rootCmd.PrintErrln(i...) + cmd.rootCobraCommand.PrintErrln(i...) } // PrintErrf is a convenience method to Printf to the defined Err output, fallback to Stderr if not set. func (cmd *BaseCommand) PrintErrf(format string, i ...interface{}) { - cmd.rootCmd.PrintErrf(format, i...) + cmd.rootCobraCommand.PrintErrf(format, i...) } // Proxy the Survey library to follow our output directives diff --git a/command/root.go b/command/root.go index cbce768..459d467 100644 --- a/command/root.go +++ b/command/root.go @@ -66,7 +66,7 @@ We'd love to hear your feedback at `, } // Link our root command to Cobra - rootCmd.rootCmd = cobraCmd + rootCmd.rootCobraCommand = cobraCmd // Bind our global configuration parameters cobraCmd.PersistentFlags().String(KeyBaseURL, DefaultBaseURL, "Base URL for accessing the Opsani API") @@ -158,14 +158,14 @@ func subCommandPath(rootCmd *cobra.Command, cmd *cobra.Command) string { // All commands with RunE will bubble errors back here func Execute() (cmd *cobra.Command, err error) { rootCmd := NewRootCommand() - cobraCmd := rootCmd.rootCmd + cobraCmd := rootCmd.rootCobraCommand if err := rootCmd.initConfig(); err != nil { rootCmd.PrintErr(err) return cobraCmd, err } - executedCmd, err := rootCmd.rootCmd.ExecuteC() + executedCmd, err := rootCmd.rootCobraCommand.ExecuteC() if err != nil { // Exit silently if the user bailed with control-c if errors.Is(err, terminal.InterruptErr) { diff --git a/command/servo.go b/command/servo.go index dce09cd..e552a2f 100644 --- a/command/servo.go +++ b/command/servo.go @@ -33,10 +33,14 @@ import ( "golang.org/x/crypto/ssh/terminal" ) +// NOTE: Binding vars instead of using flags because the call stack is messy atm type servoCommand struct { *BaseCommand - force bool - verbose bool + force bool + verbose bool + follow bool + timestamps bool + lines string } // NewServoCommand returns a new instance of the servo command @@ -64,12 +68,15 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { } listCmd.Flags().BoolVarP(&servoCommand.verbose, "verbose", "v", false, "Display verbose output") servoCmd.AddCommand(listCmd) - servoCmd.AddCommand(&cobra.Command{ + addCmd := &cobra.Command{ Use: "add", Short: "Add a Servo", Args: cobra.MaximumNArgs(1), RunE: servoCommand.RunAddServo, - }) + } + addCmd.Flags().BoolP("bastion", "b", false, "Use a bastion host for access") + addCmd.Flags().String("bastion-host", "", "Specify the bastion host (format is user@host[:port])") + servoCmd.AddCommand(addCmd) removeCmd := &cobra.Command{ Use: "remove", @@ -121,12 +128,9 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { RunE: servoCommand.RunServoLogs, } - logsCmd.Flags().BoolP("follow", "f", false, "Follow log output") - baseCmd.viperCfg.BindPFlag("follow", logsCmd.Flags().Lookup("follow")) - logsCmd.Flags().BoolP("timestamps", "t", false, "Show timestamps") - baseCmd.viperCfg.BindPFlag("timestamps", logsCmd.Flags().Lookup("timestamps")) - logsCmd.Flags().StringP("lines", "l", "25", `Number of lines to show from the end of the logs (or "all").`) - baseCmd.viperCfg.BindPFlag("lines", logsCmd.Flags().Lookup("lines")) + logsCmd.Flags().BoolVarP(&servoCommand.force, "follow", "f", false, "Follow log output") + logsCmd.Flags().BoolVarP(&servoCommand.timestamps, "timestamps", "t", false, "Show timestamps") + logsCmd.Flags().StringVarP(&servoCommand.lines, "lines", "l", "25", `Number of lines to show from the end of the logs (or "all").`) servoCmd.AddCommand(logsCmd) servoCmd.AddCommand(&cobra.Command{ @@ -139,7 +143,7 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { return servoCmd } -func (servoCmd *servoCommand) RunAddServo(_ *cobra.Command, args []string) error { +func (servoCmd *servoCommand) RunAddServo(c *cobra.Command, args []string) error { servo := Servo{} if len(args) > 0 { servo.Name = args[0] @@ -181,6 +185,19 @@ func (servoCmd *servoCommand) RunAddServo(_ *cobra.Command, args []string) error } } + // Handle bastion hosts + if flagSet, _ := c.Flags().GetBool("bastion"); flagSet { + servo.Bastion, _ = c.Flags().GetString("bastion-host") + if servo.Bastion == "" { + err := servoCmd.AskOne(&survey.Input{ + Message: "Bastion host? (format is user@host[:port])", + }, &servo.Bastion) + if err != nil { + return err + } + } + } + return servoCmd.AddServo(servo) } @@ -340,11 +357,11 @@ func (servoCmd *servoCommand) runLogsSSHSession(ctx context.Context, servo Servo args = append(args, "cd", path+"&&") } args = append(args, "docker-compose logs") - args = append(args, "--tail "+servoCmd.viperCfg.GetString("lines")) - if servoCmd.viperCfg.GetBool("follow") { + args = append(args, "--tail "+servoCmd.lines) + if servoCmd.follow { args = append(args, "--follow") } - if servoCmd.viperCfg.GetBool("timestamps") { + if servoCmd.timestamps { args = append(args, "--timestamps") } return session.Run(strings.Join(args, " ")) diff --git a/command/servo_test.go b/command/servo_test.go index cd54cc4..b0cabc3 100644 --- a/command/servo_test.go +++ b/command/servo_test.go @@ -127,11 +127,54 @@ func (s *ServoTestSuite) TestRunningAddNoInput() { expected := []interface{}( []interface{}{ map[interface{}]interface{}{ - "host": "dev.opsani.com", - "name": "opsani-dev", - "path": "/servo", - "port": "", - "user": "blakewatters", + "host": "dev.opsani.com", + "name": "opsani-dev", + "path": "/servo", + "port": "", + "user": "blakewatters", + "bastion": "", + }, + }, + ) + s.Require().EqualValues(expected, config["servos"]) +} + +func (s *ServoTestSuite) TestRunningAddNoInputWithBastion() { + configFile := test.TempConfigFileWithObj(map[string]string{ + "app": "example.com/app", + "token": "123456", + }) + args := test.Args("--config", configFile.Name(), "servo", "add", "--bastion") + context, err := s.ExecuteTestInteractively(args, func(t *test.InteractiveTestContext) error { + t.RequireString("Servo name?") + t.SendLine("opsani-dev") + t.RequireString("User?") + t.SendLine("blakewatters") + t.RequireString("Host?") + t.SendLine("dev.opsani.com") + t.RequireString("Path? (optional)") + t.SendLine("/servo") + t.RequireString("Bastion host? (format is user@host[:port])") + t.SendLine("blake@ssh.opsani.com:5555") + t.ExpectEOF() + return nil + }) + s.T().Logf("The output buffer is: %v", context.OutputBuffer().String()) + s.Require().NoError(err) + + // Check the config file + var config = map[string]interface{}{} + body, _ := ioutil.ReadFile(configFile.Name()) + yaml.Unmarshal(body, &config) + expected := []interface{}( + []interface{}{ + map[interface{}]interface{}{ + "host": "dev.opsani.com", + "name": "opsani-dev", + "path": "/servo", + "port": "", + "user": "blakewatters", + "bastion": "blake@ssh.opsani.com:5555", }, }, ) diff --git a/test/suite.go b/test/suite.go index 9723fc7..db5cae0 100644 --- a/test/suite.go +++ b/test/suite.go @@ -47,7 +47,7 @@ func (h *Suite) Command() *cobra.Command { // SetCommand sets the Opsani command under test func (h *Suite) SetCommand(cmd *command.BaseCommand) { - h.SetCobraCommand(cmd.CobraCommand()) + h.SetCobraCommand(cmd.RootCobraCommand()) } // SetCobraCommand sets the Cobra command under test From e39fa99ab057593445a41179e2566746690c7a33 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 07:17:47 -0700 Subject: [PATCH 19/33] Fix docker-compose invocations --- command/command.go | 6 +++--- command/servo.go | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/command/command.go b/command/command.go index 8b9df5b..659f4c9 100644 --- a/command/command.go +++ b/command/command.go @@ -77,9 +77,9 @@ func (cmd *BaseCommand) RootCobraCommand() *cobra.Command { } // Viper returns the Viper configuration object underlying the Opsani CLI command -// func (cmd *BaseCommand) Viper() *viper { -// return viper -// } +func (cmd *BaseCommand) Viper() *viper.Viper { + return cmd.viperCfg +} // Proxy the Cobra I/O methods for convenience diff --git a/command/servo.go b/command/servo.go index e552a2f..8bde19a 100644 --- a/command/servo.go +++ b/command/servo.go @@ -140,6 +140,9 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { RunE: servoCommand.RunServoSSH, }) + // Add nested children + servoCmd.AddCommand(NewServoImageCommand(baseCmd)) + return servoCmd } @@ -286,21 +289,21 @@ func (servoCmd *servoCommand) RunServoStatus(_ *cobra.Command, args []string) er func (servoCmd *servoCommand) RunServoStart(_ *cobra.Command, args []string) error { ctx := context.Background() return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo Servo, session *ssh.Session) error { - return servoCmd.runDockerComposeOverSSH("start", nil, servo, session) + return servoCmd.runDockerComposeOverSSH("up -d", nil, servo, session) }) } func (servoCmd *servoCommand) RunServoStop(_ *cobra.Command, args []string) error { ctx := context.Background() return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo Servo, session *ssh.Session) error { - return servoCmd.runDockerComposeOverSSH("stop", nil, servo, session) + return servoCmd.runDockerComposeOverSSH("down", nil, servo, session) }) } func (servoCmd *servoCommand) RunServoRestart(_ *cobra.Command, args []string) error { ctx := context.Background() return servoCmd.runInSSHSession(ctx, args[0], func(ctx context.Context, servo Servo, session *ssh.Session) error { - return servoCmd.runDockerComposeOverSSH("stop && docker-compse start", nil, servo, session) + return servoCmd.runDockerComposeOverSSH("down && docker-compse up -d", nil, servo, session) }) } From 2b4c646febc2fced4d98e91ca7ca212ca3ae2185 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 07:19:39 -0700 Subject: [PATCH 20/33] Fix broken log follow binding --- command/servo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/servo.go b/command/servo.go index 8bde19a..dd592f4 100644 --- a/command/servo.go +++ b/command/servo.go @@ -128,7 +128,7 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { RunE: servoCommand.RunServoLogs, } - logsCmd.Flags().BoolVarP(&servoCommand.force, "follow", "f", false, "Follow log output") + logsCmd.Flags().BoolVarP(&servoCommand.follow, "follow", "f", false, "Follow log output") logsCmd.Flags().BoolVarP(&servoCommand.timestamps, "timestamps", "t", false, "Show timestamps") logsCmd.Flags().StringVarP(&servoCommand.lines, "lines", "l", "25", `Number of lines to show from the end of the logs (or "all").`) From 632fae6b5c82ba59eb247f1f3150ee787fcb5ceb Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 07:59:46 -0700 Subject: [PATCH 21/33] Move Docker pull to servo images --- command/discover.go | 25 -------- command/root.go | 1 - command/servo_image.go | 139 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 26 deletions(-) create mode 100644 command/servo_image.go diff --git a/command/discover.go b/command/discover.go index 55a4bca..18aa9b4 100644 --- a/command/discover.go +++ b/command/discover.go @@ -240,31 +240,6 @@ func runDiscoveryCommand(cmd *cobra.Command, args []string) error { return nil } -func newPullCommand(baseCmd *BaseCommand) *cobra.Command { - pullCmd := &cobra.Command{ - Use: "pull", - Short: "Pull a Docker image", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - dockerHost, err := cmd.Flags().GetString(hostArg) - if err != nil { - return err - } - - di, err := NewDockerInterface(dockerHost) - if err != nil { - return err - } - - return di.PullImageWithProgressReporting(context.Background(), args[0]) - }, - } - - pullCmd.Flags().StringP(hostArg, "H", "", "Docket host to connect to (overriding DOCKER_HOST)") - - return pullCmd -} - func newDiscoverCommand(baseCmd *BaseCommand) *cobra.Command { discoverCmd := &cobra.Command{ Use: "discover", diff --git a/command/root.go b/command/root.go index 459d467..d4c8b04 100644 --- a/command/root.go +++ b/command/root.go @@ -106,7 +106,6 @@ We'd love to hear your feedback at `, cobraCmd.AddCommand(newDiscoverCommand(rootCmd)) cobraCmd.AddCommand(newIMBCommand(rootCmd)) - cobraCmd.AddCommand(newPullCommand(rootCmd)) cobraCmd.AddCommand(NewConfigCommand(rootCmd)) cobraCmd.AddCommand(NewCompletionCommand(rootCmd)) diff --git a/command/servo_image.go b/command/servo_image.go new file mode 100644 index 0000000..d5d56b2 --- /dev/null +++ b/command/servo_image.go @@ -0,0 +1,139 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" +) + +type servoImageCommand struct { + *BaseCommand +} + +// NewServoImageCommand returns a new instance of the servo image command +func NewServoImageCommand(baseCmd *BaseCommand) *cobra.Command { + servoImageCommand := servoImageCommand{BaseCommand: baseCmd} + + servoImageCobra := &cobra.Command{ + Use: "image", + Short: "Manage Servo Images", + Args: cobra.NoArgs, + PersistentPreRunE: ReduceRunEFuncs( + baseCmd.InitConfigRunE, + baseCmd.RequireConfigFileFlagToExistRunE, + baseCmd.RequireInitRunE, + ), + } + + servoImageCobra.AddCommand(&cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List Servo images", + Args: cobra.NoArgs, + RunE: servoImageCommand.RunList, + }) + servoImageCobra.AddCommand(&cobra.Command{ + Use: "search", + Short: "Search for Servo images", + Args: cobra.ExactArgs(1), + RunE: servoImageCommand.RunSearch, + }) + servoImageCobra.AddCommand(&cobra.Command{ + Use: "info", + Short: "Get info about a Servo image", + Args: cobra.ExactArgs(1), + RunE: servoImageCommand.RunInfo, + }) + pullCmd := &cobra.Command{ + Use: "pull", + Short: "Pull a Servo image with Docker", + Args: cobra.ExactArgs(1), + RunE: servoImageCommand.RunPull, + } + pullCmd.Flags().StringP(hostArg, "H", "", "Docket host to connect to (overriding DOCKER_HOST)") + servoImageCobra.AddCommand(pullCmd) + + return servoImageCobra +} + +func (cmd *servoImageCommand) RunList(_ *cobra.Command, args []string) error { + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return err + } + + images, err := cli.ImageList(ctx, types.ImageListOptions{}) + if err != nil { + return err + } + + table := tablewriter.NewWriter(cmd.OutOrStdout()) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") // pad with tabs + table.SetNoWhiteSpace(true) + + data := [][]string{} + for _, image := range images { + for _, repoTag := range image.RepoTags { + if strings.HasPrefix(repoTag, "opsani/") { + data = append(data, []string{ + repoTag, image.ID, + }) + } + } + } + + table.AppendBulk(data) + table.Render() + + return nil +} + +func (cmd *servoImageCommand) RunSearch(_ *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoImageCommand) RunInfo(_ *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoImageCommand) RunPull(c *cobra.Command, args []string) error { + dockerHost, err := c.Flags().GetString(hostArg) + if err != nil { + return err + } + + di, err := NewDockerInterface(dockerHost) + if err != nil { + return err + } + + return di.PullImageWithProgressReporting(context.Background(), args[0]) +} From 84a38ccc14295859e1887e7881aa7bc5ca161151 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 08:58:20 -0700 Subject: [PATCH 22/33] Remove fatal errors from SSH --- command/servo.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/command/servo.go b/command/servo.go index dd592f4..d0773fe 100644 --- a/command/servo.go +++ b/command/servo.go @@ -18,7 +18,6 @@ import ( "bytes" "context" "fmt" - "log" "net" "os" "strings" @@ -442,7 +441,7 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, } hostKeyCallback, err := knownhosts.New(knownHosts) if err != nil { - log.Fatal("could not create hostkeycallback function: ", err) + return err } config := &ssh.ClientConfig{ User: servo.User, @@ -467,19 +466,19 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, // Dial the bastion host bastionClient, err := ssh.Dial("tcp", host, bastionConfig) if err != nil { - log.Fatal(err) + return err } // Establish a new connection thrrough the bastion conn, err := bastionClient.Dial("tcp", servo.HostAndPort()) if err != nil { - log.Fatal(err) + return err } // Build a new SSH connection on top of the bastion connection ncc, chans, reqs, err := ssh.NewClientConn(conn, servo.HostAndPort(), config) if err != nil { - log.Fatal(err) + return err } // Now connection a client on top of it @@ -487,7 +486,7 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, } else { sshClient, err = ssh.Dial("tcp", servo.HostAndPort(), config) if err != nil { - log.Fatal(err) + return err } } defer sshClient.Close() @@ -495,7 +494,7 @@ func (servoCmd *servoCommand) runInSSHSession(ctx context.Context, name string, // Create sesssion session, err := sshClient.NewSession() if err != nil { - log.Fatal("Failed to create session: ", err) + return err } defer session.Close() From 48fc871bbdf108b7dd1080d1609cb93614cd7cde Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 09:45:10 -0700 Subject: [PATCH 23/33] Scaffold everything but Vital --- command/auth.go | 90 +++++++++++++++++++++ command/{login_test.go => auth_test.go} | 20 +++-- command/discover_test.go | 6 -- command/login.go | 58 ------------- command/root.go | 2 +- command/servo.go | 2 + command/servo_assembly.go | 103 ++++++++++++++++++++++++ command/servo_assembly_test.go | 77 ++++++++++++++++++ command/servo_image_test.go | 15 ++++ command/servo_plugin.go | 103 ++++++++++++++++++++++++ command/servo_plugin_test.go | 77 ++++++++++++++++++ 11 files changed, 481 insertions(+), 72 deletions(-) create mode 100644 command/auth.go rename command/{login_test.go => auth_test.go} (64%) create mode 100644 command/servo_assembly.go create mode 100644 command/servo_assembly_test.go create mode 100644 command/servo_image_test.go create mode 100644 command/servo_plugin.go create mode 100644 command/servo_plugin_test.go diff --git a/command/auth.go b/command/auth.go new file mode 100644 index 0000000..0cea5dd --- /dev/null +++ b/command/auth.go @@ -0,0 +1,90 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "fmt" + + "github.com/AlecAivazis/survey/v2" + "github.com/mgutz/ansi" + "github.com/spf13/cobra" +) + +type authCommand struct { + *BaseCommand +} + +// NewAuthCommand returns a new `opani login` command instance +func NewAuthCommand(baseCmd *BaseCommand) *cobra.Command { + authCommand := authCommand{BaseCommand: baseCmd} + + authCobra := &cobra.Command{ + Use: "auth", + Short: "Manage authentication", + Args: cobra.NoArgs, + PersistentPreRunE: ReduceRunEFuncs( + baseCmd.InitConfigRunE, + baseCmd.RequireConfigFileFlagToExistRunE, + baseCmd.RequireInitRunE, + ), + } + + loginCobra := &cobra.Command{ + Use: "login", + Short: "Login to Opsani", + Args: cobra.NoArgs, + RunE: authCommand.runLoginCommand, + } + + loginCobra.Flags().StringP("username", "u", "", "Opsani Username") + loginCobra.Flags().StringP("password", "p", "", "Password") + authCobra.AddCommand(loginCobra) + + authCobra.AddCommand(&cobra.Command{ + Use: "logout", + Short: "Logout from Opsani", + Args: cobra.NoArgs, + }) + + return authCobra +} + +func (cmd *authCommand) runLoginCommand(c *cobra.Command, args []string) error { + fmt.Println("Logging into", cmd.GetBaseURLHostnameAndPort()) + + whiteBold := ansi.ColorCode("white+b") + username, _ := c.Flags().GetString("username") + if username == "" { + err := survey.AskOne(&survey.Input{ + Message: "Username:", + }, &username, survey.WithValidator(survey.Required)) + if err != nil { + return err + } + } else { + fmt.Printf("%si %sUsername: %s%s%s%s\n", ansi.Blue, whiteBold, ansi.Reset, ansi.LightCyan, username, ansi.Reset) + } + + password, _ := c.Flags().GetString("password") + if password == "" { + err := survey.AskOne(&survey.Password{ + Message: "Password:", + }, &password, survey.WithValidator(survey.Required)) + if err != nil { + return err + } + } + return nil +} diff --git a/command/login_test.go b/command/auth_test.go similarity index 64% rename from command/login_test.go rename to command/auth_test.go index b6667de..5442fc2 100644 --- a/command/login_test.go +++ b/command/auth_test.go @@ -22,20 +22,26 @@ import ( "github.com/stretchr/testify/suite" ) -type LoginTestSuite struct { +type AuthTestSuite struct { test.Suite } -func TestLoginTestSuite(t *testing.T) { - suite.Run(t, new(LoginTestSuite)) +func TestAuthTestSuite(t *testing.T) { + suite.Run(t, new(AuthTestSuite)) } -func (s *LoginTestSuite) SetupTest() { +func (s *AuthTestSuite) SetupTest() { s.SetCommand(command.NewRootCommand()) } -func (s *LoginTestSuite) TestRunningInitHelp() { - output, err := s.Execute("login", "--help") +func (s *AuthTestSuite) TestLoginHelp() { + output, err := s.Execute("auth", "login", "--help") s.Require().NoError(err) - s.Require().Contains(output, "Login to the Opsani API") + s.Require().Contains(output, "Login to Opsani") +} + +func (s *AuthTestSuite) TestLogoutHelp() { + output, err := s.Execute("auth", "logout", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Logout from Opsani") } diff --git a/command/discover_test.go b/command/discover_test.go index 6d01edc..a737866 100644 --- a/command/discover_test.go +++ b/command/discover_test.go @@ -45,9 +45,3 @@ func (s *DiscoverTestSuite) TestRunningIMBHelp() { s.Require().NoError(err) s.Require().Contains(output, "Run the intelligent manifest builder") } - -func (s *DiscoverTestSuite) TestRunningPullHelp() { - output, err := s.Execute("pull", "--help") - s.Require().NoError(err) - s.Require().Contains(output, "Pull a Docker image") -} diff --git a/command/login.go b/command/login.go index 8fbb184..2564665 100644 --- a/command/login.go +++ b/command/login.go @@ -13,61 +13,3 @@ // limitations under the License. package command - -import ( - "fmt" - - "github.com/AlecAivazis/survey/v2" - "github.com/mgutz/ansi" - "github.com/spf13/cobra" -) - -type loginCommand struct { - *BaseCommand - - username string - password string -} - -func (loginCmd *loginCommand) runLoginCommand(_ *cobra.Command, args []string) error { - fmt.Println("Logging into", loginCmd.GetBaseURLHostnameAndPort()) - - whiteBold := ansi.ColorCode("white+b") - if loginCmd.username == "" { - err := survey.AskOne(&survey.Input{ - Message: "Username:", - }, &loginCmd.username, survey.WithValidator(survey.Required)) - if err != nil { - return err - } - } else { - fmt.Printf("%si %sUsername: %s%s%s%s\n", ansi.Blue, whiteBold, ansi.Reset, ansi.LightCyan, loginCmd.username, ansi.Reset) - } - - if loginCmd.password == "" { - err := survey.AskOne(&survey.Password{ - Message: "Password:", - }, &loginCmd.password, survey.WithValidator(survey.Required)) - if err != nil { - return err - } - } - return nil -} - -// NewLoginCommand returns a new `opani login` command instance -func NewLoginCommand(baseCmd *BaseCommand) *cobra.Command { - loginCmd := loginCommand{BaseCommand: baseCmd} - c := &cobra.Command{ - Use: "login", - Short: "Login to the Opsani API", - Long: `Login to the Opsani API and persist access credentials.`, - Args: cobra.NoArgs, - RunE: loginCmd.runLoginCommand, - } - - c.Flags().StringVarP(&loginCmd.username, "username", "u", "", "Opsani Username") - c.Flags().StringVarP(&loginCmd.password, "password", "p", "", "Password") - - return c -} diff --git a/command/root.go b/command/root.go index d4c8b04..3edf83a 100644 --- a/command/root.go +++ b/command/root.go @@ -102,7 +102,7 @@ We'd love to hear your feedback at `, // Add all sub-commands cobraCmd.AddCommand(NewInitCommand(rootCmd)) cobraCmd.AddCommand(NewAppCommand(rootCmd)) - cobraCmd.AddCommand(NewLoginCommand(rootCmd)) + cobraCmd.AddCommand(NewAuthCommand(rootCmd)) cobraCmd.AddCommand(newDiscoverCommand(rootCmd)) cobraCmd.AddCommand(newIMBCommand(rootCmd)) diff --git a/command/servo.go b/command/servo.go index d0773fe..772c998 100644 --- a/command/servo.go +++ b/command/servo.go @@ -141,6 +141,8 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { // Add nested children servoCmd.AddCommand(NewServoImageCommand(baseCmd)) + servoCmd.AddCommand(NewServoAssemblyCommand(baseCmd)) + servoCmd.AddCommand(NewServoPluginCommand(baseCmd)) return servoCmd } diff --git a/command/servo_assembly.go b/command/servo_assembly.go new file mode 100644 index 0000000..019f693 --- /dev/null +++ b/command/servo_assembly.go @@ -0,0 +1,103 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "github.com/spf13/cobra" +) + +type servoAssemblyCommand struct { + *BaseCommand +} + +// NewServoAssemblyCommand returns a new instance of the servo image command +func NewServoAssemblyCommand(baseCmd *BaseCommand) *cobra.Command { + servoAssemblyCommand := servoAssemblyCommand{BaseCommand: baseCmd} + + servoAssemblyCobra := &cobra.Command{ + Use: "assembly", + Short: "Manage Servo Assemblies", + Args: cobra.NoArgs, + PersistentPreRunE: ReduceRunEFuncs( + baseCmd.InitConfigRunE, + baseCmd.RequireConfigFileFlagToExistRunE, + baseCmd.RequireInitRunE, + ), + } + + servoAssemblyCobra.AddCommand(&cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List Servo assemblies", + Args: cobra.NoArgs, + RunE: servoAssemblyCommand.RunList, + }) + servoAssemblyCobra.AddCommand(&cobra.Command{ + Use: "search", + Short: "Search for Servo assemblies", + Args: cobra.ExactArgs(1), + RunE: servoAssemblyCommand.RunSearch, + }) + servoAssemblyCobra.AddCommand(&cobra.Command{ + Use: "info", + Short: "Get info about a Servo assembly", + Args: cobra.ExactArgs(1), + RunE: servoAssemblyCommand.RunInfo, + }) + servoAssemblyCobra.AddCommand(&cobra.Command{ + Use: "clone", + Short: "Clone a Servo assembly with Git", + Args: cobra.ExactArgs(1), + RunE: servoAssemblyCommand.RunClone, + }) + servoAssemblyCobra.AddCommand(&cobra.Command{ + Use: "fork", + Short: "Fork a Servo assembly on GitHub", + Args: cobra.ExactArgs(1), + RunE: servoAssemblyCommand.RunFork, + }) + servoAssemblyCobra.AddCommand(&cobra.Command{ + Use: "create", + Short: "Create a new Servo assembly", + Args: cobra.ExactArgs(1), + RunE: servoAssemblyCommand.RunCreate, + }) + + return servoAssemblyCobra +} + +func (cmd *servoAssemblyCommand) RunList(_ *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoAssemblyCommand) RunSearch(_ *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoAssemblyCommand) RunInfo(_ *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoAssemblyCommand) RunClone(c *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoAssemblyCommand) RunFork(c *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoAssemblyCommand) RunCreate(c *cobra.Command, args []string) error { + return nil +} diff --git a/command/servo_assembly_test.go b/command/servo_assembly_test.go new file mode 100644 index 0000000..3e038ec --- /dev/null +++ b/command/servo_assembly_test.go @@ -0,0 +1,77 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command_test + +import ( + "testing" + + "github.com/opsani/cli/command" + "github.com/opsani/cli/test" + "github.com/stretchr/testify/suite" +) + +type ServoAssemblyTestSuite struct { + test.Suite +} + +func TestServoAssemblyTestSuite(t *testing.T) { + suite.Run(t, new(ServoAssemblyTestSuite)) +} + +func (s *ServoAssemblyTestSuite) SetupTest() { + s.SetCommand(command.NewRootCommand()) +} + +func (s *ServoAssemblyTestSuite) TestRootHelp() { + output, err := s.Execute("servo", "assembly", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Manage Servo Assemblies") +} + +func (s *ServoAssemblyTestSuite) TestListtHelp() { + output, err := s.Execute("servo", "assembly", "list", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "List Servo assemblies") +} + +func (s *ServoAssemblyTestSuite) TestSearchHelp() { + output, err := s.Execute("servo", "assembly", "search", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Search for Servo assemblies") +} + +func (s *ServoAssemblyTestSuite) TestInfoHelp() { + output, err := s.Execute("servo", "assembly", "info", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Get info about a Servo assembly") +} + +func (s *ServoAssemblyTestSuite) TestCloneHelp() { + output, err := s.Execute("servo", "assembly", "clone", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Clone a Servo assembly with Git") +} + +func (s *ServoAssemblyTestSuite) TestForkHelp() { + output, err := s.Execute("servo", "assembly", "fork", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Fork a Servo assembly on GitHub") +} + +func (s *ServoAssemblyTestSuite) TestCreateHelp() { + output, err := s.Execute("servo", "assembly", "create", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Create a new Servo assembly") +} diff --git a/command/servo_image_test.go b/command/servo_image_test.go new file mode 100644 index 0000000..21168d5 --- /dev/null +++ b/command/servo_image_test.go @@ -0,0 +1,15 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command_test diff --git a/command/servo_plugin.go b/command/servo_plugin.go new file mode 100644 index 0000000..dd0ff29 --- /dev/null +++ b/command/servo_plugin.go @@ -0,0 +1,103 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "github.com/spf13/cobra" +) + +type servoPluginCommand struct { + *BaseCommand +} + +// NewServoPluginCommand returns a new instance of the servo image command +func NewServoPluginCommand(baseCmd *BaseCommand) *cobra.Command { + servoPluginCommand := servoPluginCommand{BaseCommand: baseCmd} + + servoPluginCobra := &cobra.Command{ + Use: "plugin", + Short: "Manage Servo Plugins", + Args: cobra.NoArgs, + PersistentPreRunE: ReduceRunEFuncs( + baseCmd.InitConfigRunE, + baseCmd.RequireConfigFileFlagToExistRunE, + baseCmd.RequireInitRunE, + ), + } + + servoPluginCobra.AddCommand(&cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List Servo plugins", + Args: cobra.NoArgs, + RunE: servoPluginCommand.RunList, + }) + servoPluginCobra.AddCommand(&cobra.Command{ + Use: "search", + Short: "Search for Servo plugins", + Args: cobra.ExactArgs(1), + RunE: servoPluginCommand.RunSearch, + }) + servoPluginCobra.AddCommand(&cobra.Command{ + Use: "info", + Short: "Get info about a Servo plugin", + Args: cobra.ExactArgs(1), + RunE: servoPluginCommand.RunInfo, + }) + servoPluginCobra.AddCommand(&cobra.Command{ + Use: "clone", + Short: "Clone a Servo plugin with Git", + Args: cobra.ExactArgs(1), + RunE: servoPluginCommand.RunClone, + }) + servoPluginCobra.AddCommand(&cobra.Command{ + Use: "fork", + Short: "Fork a Servo plugin on GitHub", + Args: cobra.ExactArgs(1), + RunE: servoPluginCommand.RunFork, + }) + servoPluginCobra.AddCommand(&cobra.Command{ + Use: "create", + Short: "Create a new Servo plugin", + Args: cobra.ExactArgs(1), + RunE: servoPluginCommand.RunCreate, + }) + + return servoPluginCobra +} + +func (cmd *servoPluginCommand) RunList(_ *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoPluginCommand) RunSearch(_ *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoPluginCommand) RunInfo(_ *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoPluginCommand) RunClone(c *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoPluginCommand) RunFork(c *cobra.Command, args []string) error { + return nil +} + +func (cmd *servoPluginCommand) RunCreate(c *cobra.Command, args []string) error { + return nil +} diff --git a/command/servo_plugin_test.go b/command/servo_plugin_test.go new file mode 100644 index 0000000..9acc16c --- /dev/null +++ b/command/servo_plugin_test.go @@ -0,0 +1,77 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command_test + +import ( + "testing" + + "github.com/opsani/cli/command" + "github.com/opsani/cli/test" + "github.com/stretchr/testify/suite" +) + +type ServoPluginTestSuite struct { + test.Suite +} + +func TestServoPluginTestSuite(t *testing.T) { + suite.Run(t, new(ServoPluginTestSuite)) +} + +func (s *ServoPluginTestSuite) SetupTest() { + s.SetCommand(command.NewRootCommand()) +} + +func (s *ServoPluginTestSuite) TestRootHelp() { + output, err := s.Execute("servo", "plugin", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Manage Servo Plugins") +} + +func (s *ServoPluginTestSuite) TestListtHelp() { + output, err := s.Execute("servo", "plugin", "list", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "List Servo plugins") +} + +func (s *ServoPluginTestSuite) TestSearchHelp() { + output, err := s.Execute("servo", "plugin", "search", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Search for Servo plugins") +} + +func (s *ServoPluginTestSuite) TestInfoHelp() { + output, err := s.Execute("servo", "plugin", "info", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Get info about a Servo plugin") +} + +func (s *ServoPluginTestSuite) TestCloneHelp() { + output, err := s.Execute("servo", "plugin", "clone", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Clone a Servo plugin with Git") +} + +func (s *ServoPluginTestSuite) TestForkHelp() { + output, err := s.Execute("servo", "plugin", "fork", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Fork a Servo plugin on GitHub") +} + +func (s *ServoPluginTestSuite) TestCreateHelp() { + output, err := s.Execute("servo", "plugin", "create", "--help") + s.Require().NoError(err) + s.Require().Contains(output, "Create a new Servo plugin") +} From 25463a309c881efbb7a5de716d4d50c50bf11b16 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 13:43:47 -0700 Subject: [PATCH 24/33] Craft the help and usage text into groups using annotations --- command/auth.go | 6 ++ command/completion.go | 5 +- command/config.go | 3 +- command/root.go | 207 +++++++++++++++++++++++++++++++++++++++++- command/servo.go | 31 ++++--- 5 files changed, 234 insertions(+), 18 deletions(-) diff --git a/command/auth.go b/command/auth.go index 0cea5dd..1091f22 100644 --- a/command/auth.go +++ b/command/auth.go @@ -56,6 +56,7 @@ func NewAuthCommand(baseCmd *BaseCommand) *cobra.Command { Use: "logout", Short: "Logout from Opsani", Args: cobra.NoArgs, + RunE: authCommand.runLogoutCommand, }) return authCobra @@ -88,3 +89,8 @@ func (cmd *authCommand) runLoginCommand(c *cobra.Command, args []string) error { } return nil } + +func (cmd *authCommand) runLogoutCommand(c *cobra.Command, args []string) error { + fmt.Println("Logged out from", cmd.GetBaseURLHostnameAndPort()) + return nil +} diff --git a/command/completion.go b/command/completion.go index 3c925ca..b0f3c32 100644 --- a/command/completion.go +++ b/command/completion.go @@ -27,8 +27,9 @@ import ( // NewCompletionCommand returns a new Opsani CLI cmpletion command instance func NewCompletionCommand(baseCmd *BaseCommand) *cobra.Command { completionCmd := &cobra.Command{ - Use: "completion", - Short: "Generate shell completion scripts", + Use: "completion", + Annotations: map[string]string{"other": "true"}, + Short: "Generate shell completion scripts", Long: `Generate shell completion scripts for Opsani CLI commands. The output of this command will be computer code and is meant to be saved to a diff --git a/command/config.go b/command/config.go index bce5f04..8c9ea66 100644 --- a/command/config.go +++ b/command/config.go @@ -29,7 +29,8 @@ func NewConfigCommand(baseCmd *BaseCommand) *cobra.Command { cfgCmd := configCommand{BaseCommand: baseCmd} return &cobra.Command{ Use: "config", - Short: "Manages client configuration", + Short: "Display configuration", + Annotations: map[string]string{"other": "true"}, Args: cobra.NoArgs, RunE: cfgCmd.Run, PersistentPreRunE: ReduceRunEFuncs(baseCmd.InitConfigRunE, baseCmd.RequireConfigFileFlagToExistRunE, baseCmd.RequireInitRunE), diff --git a/command/root.go b/command/root.go index 3edf83a..0db5df9 100644 --- a/command/root.go +++ b/command/root.go @@ -24,6 +24,7 @@ import ( "github.com/AlecAivazis/survey/v2/core" "github.com/AlecAivazis/survey/v2/terminal" + "github.com/docker/docker/pkg/term" "github.com/mitchellh/go-homedir" "github.com/opsani/cli/opsani" "github.com/spf13/cobra" @@ -91,10 +92,11 @@ We'd love to hear your feedback at `, configFileUsage := fmt.Sprintf("Location of config file (default \"%s\")", rootCmd.DefaultConfigFile()) cobraCmd.PersistentFlags().StringVar(&rootCmd.ConfigFile, "config", "", configFileUsage) cobraCmd.MarkPersistentFlagFilename("config", "*.yaml", "*.yml") - cobraCmd.SetVersionTemplate("Opsani CLI version {{.Version}}\n") cobraCmd.Flags().Bool("version", false, "Display version and exit") cobraCmd.PersistentFlags().Bool("help", false, "Display help and exit") cobraCmd.PersistentFlags().MarkHidden("help") + cobraCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") + cobraCmd.SetHelpCommand(&cobra.Command{ Hidden: true, }) @@ -110,6 +112,26 @@ We'd love to hear your feedback at `, cobraCmd.AddCommand(NewConfigCommand(rootCmd)) cobraCmd.AddCommand(NewCompletionCommand(rootCmd)) cobraCmd.AddCommand(NewServoCommand(rootCmd)) + cobraCmd.AddCommand(NewVitalCommand(rootCmd)) + + // Usage and help layout + cobra.AddTemplateFunc("hasSubCommands", hasSubCommands) + cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands) + cobra.AddTemplateFunc("operationSubCommands", operationSubCommands) + cobra.AddTemplateFunc("managementSubCommands", managementSubCommands) + cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages) + + cobra.AddTemplateFunc("hasOtherSubCommands", hasOtherSubCommands) + cobra.AddTemplateFunc("otherSubCommands", otherSubCommands) + + cobra.AddTemplateFunc("hasRegistrySubCommands", hasRegistrySubCommands) + cobra.AddTemplateFunc("registrySubCommands", registrySubCommands) + + cobraCmd.SetUsageTemplate(usageTemplate) + cobraCmd.SetHelpTemplate(helpTemplate) + // cobraCmd.SetFlagErrorFunc(FlagErrorFunc) + cobraCmd.SetHelpCommand(helpCommand) + cobraCmd.SetVersionTemplate("Opsani CLI version {{.Version}}\n") // See Execute() cobraCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { @@ -348,3 +370,186 @@ func (baseCmd *BaseCommand) GetAllSettings() map[string]interface{} { func (baseCmd *BaseCommand) IsInitialized() bool { return baseCmd.App() != "" && baseCmd.AccessToken() != "" } + +var helpCommand = &cobra.Command{ + Use: "help [command]", + Short: "Help about the command", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + PersistentPostRun: func(cmd *cobra.Command, args []string) {}, + RunE: func(c *cobra.Command, args []string) error { + cmd, args, e := c.Root().Find(args) + if cmd == nil || e != nil || len(args) > 0 { + return fmt.Errorf("unknown help topic: %v", strings.Join(args, " ")) + } + + helpFunc := cmd.HelpFunc() + helpFunc(cmd, args) + return nil + }, +} + +/// Help and usage + +// FlagErrorFunc prints an error message which matches the format of the +// docker/cli/cli error messages +// func FlagErrorFunc(cmd *cobra.Command, err error) error { +// if err == nil { +// return nil +// } + +// usage := "" +// if cmd.HasSubCommands() { +// usage = "\n\n" + cmd.UsageString() +// } +// return StatusError{ +// Status: fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage), +// StatusCode: 125, +// } +// } + +func hasSubCommands(cmd *cobra.Command) bool { + return len(operationSubCommands(cmd)) > 0 +} + +func hasManagementSubCommands(cmd *cobra.Command) bool { + return len(managementSubCommands(cmd)) > 0 +} + +func operationSubCommands(cmd *cobra.Command) []*cobra.Command { + cmds := []*cobra.Command{} + for _, sub := range cmd.Commands() { + // if isOtherCommand(sub) { + if len(sub.Annotations) > 0 { + continue + } + if sub.IsAvailableCommand() && !sub.HasSubCommands() { + cmds = append(cmds, sub) + } + } + return cmds +} + +func wrappedFlagUsages(cmd *cobra.Command) string { + width := 80 + if ws, err := term.GetWinsize(0); err == nil { + width = int(ws.Width) + } + return cmd.Flags().FlagUsagesWrapped(width - 1) +} + +func managementSubCommands(cmd *cobra.Command) []*cobra.Command { + cmds := []*cobra.Command{} + for _, sub := range cmd.Commands() { + if isOtherCommand(sub) { + continue + } + if sub.IsAvailableCommand() && sub.HasSubCommands() { + cmds = append(cmds, sub) + } + } + return cmds +} + +func hasOtherSubCommands(cmd *cobra.Command) bool { + return len(otherSubCommands(cmd)) > 0 +} + +func otherSubCommands(cmd *cobra.Command) []*cobra.Command { + cmds := []*cobra.Command{} + for _, sub := range cmd.Commands() { + if sub.IsAvailableCommand() && isOtherCommand(sub) { + cmds = append(cmds, sub) + } + } + return cmds +} + +func isOtherCommand(cmd *cobra.Command) bool { + return cmd.Annotations["other"] == "true" +} + +func hasRegistrySubCommands(cmd *cobra.Command) bool { + return len(registrySubCommands(cmd)) > 0 +} + +func registrySubCommands(cmd *cobra.Command) []*cobra.Command { + cmds := []*cobra.Command{} + for _, sub := range cmd.Commands() { + if sub.IsAvailableCommand() && isRegistryCommand(sub) { + cmds = append(cmds, sub) + } + } + return cmds +} + +func isRegistryCommand(cmd *cobra.Command) bool { + return cmd.Annotations["registry"] == "true" +} + +var usageTemplate = `Usage: + +{{- if not .HasSubCommands}} {{.UseLine}}{{end}} +{{- if .HasSubCommands}} {{ .CommandPath}}{{- if .HasAvailableFlags}} [OPTIONS]{{end}} COMMAND{{end}} + +{{if ne .Long ""}}{{ .Long | trim }}{{ else }}{{ .Short | trim }}{{end}} + +{{- if gt .Aliases 0}} + +Aliases: + {{.NameAndAliases}} + +{{- end}} +{{- if .HasExample}} + +Examples: +{{ .Example }} + +{{- end}} +{{- if .HasAvailableFlags}} + +Options: +{{ wrappedFlagUsages . | trimRightSpace}} + +{{- end}} +{{- if hasManagementSubCommands . }} + +Management Commands: + +{{- range managementSubCommands . }} + {{rpad .Name .NamePadding }} {{.Short}} +{{- end}} +{{- end}} +{{- if hasRegistrySubCommands . }} + +Registry Commands: + +{{- range registrySubCommands . }} + {{rpad .Name .NamePadding }} {{.Short}} +{{- end}} +{{- end}} +{{- if hasSubCommands .}} + +Commands: + +{{- range operationSubCommands . }} + {{rpad .Name .NamePadding }} {{.Short}} +{{- end}} +{{- end}} + +{{- if hasOtherSubCommands .}} + +Other Commands: + +{{- range otherSubCommands . }} + {{rpad .Name .NamePadding }} {{.Short}} +{{- end}} +{{- end}} + +{{- if .HasSubCommands }} + +Run '{{.CommandPath}} COMMAND --help' for more information on a command. +{{- end}} +` + +var helpTemplate = ` +{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` diff --git a/command/servo.go b/command/servo.go index 772c998..fcf024b 100644 --- a/command/servo.go +++ b/command/servo.go @@ -59,30 +59,33 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { // Servo registry listCmd := &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "List Servos", - Args: cobra.NoArgs, - RunE: servoCommand.RunServoList, + Use: "list", + Annotations: map[string]string{"registry": "true"}, + Aliases: []string{"ls"}, + Short: "List Servos", + Args: cobra.NoArgs, + RunE: servoCommand.RunServoList, } listCmd.Flags().BoolVarP(&servoCommand.verbose, "verbose", "v", false, "Display verbose output") servoCmd.AddCommand(listCmd) addCmd := &cobra.Command{ - Use: "add", - Short: "Add a Servo", - Args: cobra.MaximumNArgs(1), - RunE: servoCommand.RunAddServo, + Use: "add", + Annotations: map[string]string{"registry": "true"}, + Short: "Add a Servo", + Args: cobra.MaximumNArgs(1), + RunE: servoCommand.RunAddServo, } addCmd.Flags().BoolP("bastion", "b", false, "Use a bastion host for access") addCmd.Flags().String("bastion-host", "", "Specify the bastion host (format is user@host[:port])") servoCmd.AddCommand(addCmd) removeCmd := &cobra.Command{ - Use: "remove", - Aliases: []string{"rm"}, - Short: "Remove a Servo", - Args: cobra.ExactArgs(1), - RunE: servoCommand.RunRemoveServo, + Use: "remove", + Annotations: map[string]string{"registry": "true"}, + Aliases: []string{"rm"}, + Short: "Remove a Servo", + Args: cobra.ExactArgs(1), + RunE: servoCommand.RunRemoveServo, } removeCmd.Flags().BoolVarP(&servoCommand.force, "force", "f", false, "Don't prompt for confirmation") servoCmd.AddCommand(removeCmd) From 54a18cdfbd03df96ade23608943ba6c3bf4a2379 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 14:06:22 -0700 Subject: [PATCH 25/33] Add a functional Dockerfile --- Dockerfile | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 014a13c..8837dcd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM scratch -COPY ./bin/opsani / -ENTRYPOINT ["/opsani"] +FROM golang:alpine + +# Set necessary environmet variables needed for our image +ENV GO111MODULE=on \ + CGO_ENABLED=0 \ + GOOS=linux \ + GOARCH=amd64 + +# Move to working directory /build +WORKDIR /build + +# Copy and download dependency using go mod +COPY go.mod . +COPY go.sum . +RUN go mod download + +# Build our binary +COPY . . +RUN go build -o bin/opsani . +WORKDIR /dist +RUN cp /build/bin/opsani . + +CMD ["/dist/opsani"] From 49d21ffb82a91663a138464269013ae2e0a995d8 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 14:07:11 -0700 Subject: [PATCH 26/33] Pin dependencies to deal with upstream breakage --- go.mod | 4 +++- go.sum | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d31a138..9042418 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 github.com/alecthomas/colour v0.1.0 // indirect github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect + github.com/charmbracelet/glamour v0.1.0 github.com/containerd/containerd v1.3.3 // indirect github.com/containerd/continuity v0.0.0-20200228182428-0f16d7a0959c // indirect github.com/creack/pty v1.1.9 // indirect @@ -23,12 +24,13 @@ require ( github.com/go-resty/resty/v2 v2.2.0 github.com/goccy/go-yaml v1.4.3 github.com/golang/protobuf v1.4.0 // indirect - github.com/googleapis/gnostic v0.4.1 // indirect + github.com/googleapis/gnostic v0.4.0 // indirect github.com/gorilla/mux v1.7.4 // indirect github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e github.com/imdario/mergo v0.3.9 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/kr/pretty v0.2.0 // indirect github.com/kr/pty v1.1.8 github.com/mattn/go-colorable v0.1.4 github.com/mattn/go-isatty v0.0.12 diff --git a/go.sum b/go.sum index 9e2e624..e560789 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA= @@ -27,10 +29,18 @@ github.com/Netflix/go-expect v0.0.0-20200312175327-da48e75238e2/go.mod h1:oX5x61 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= +github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c h1:MVVbswUlqicyj8P/JljoocA7AyCo62gzD0O7jfvrhtE= github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= @@ -42,6 +52,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/charmbracelet/glamour v0.1.0 h1:BHCtc+YJjoBjNUnFKBtXyyM4Bp9u7L2kf49qV+/AGYw= +github.com/charmbracelet/glamour v0.1.0/go.mod h1:Z1C2JkVGBom/RYfoKcPBZ81lHMR3xp3W6OCLNWWEIMc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/containerd v1.3.3 h1:LoIzb5y9x5l8VKAlyrbusNPXqBY0+kviRloxFUMFwKc= @@ -59,11 +71,16 @@ github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg= +github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/docker/cli v0.0.0-20200303215952-eb310fca4956 h1:5/ZRsUbguX7xFNLlbxVQY/yhD3Psy+vylKZrNme5BJs= github.com/docker/cli v0.0.0-20200303215952-eb310fca4956/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= @@ -143,13 +160,19 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.4.0 h1:BXDUo8p/DaxC+4FJY/SSx3gvnx9C1VdHNgaUkiEL5mk= +github.com/googleapis/gnostic v0.4.0/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= +github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -170,6 +193,7 @@ github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -200,14 +224,20 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23 h1:Wp7NjqGKGN9te9N/rvXYRhlVcrulGdxnz8zadXWs7fc= +github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= @@ -220,6 +250,8 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -234,9 +266,12 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/reflow v0.0.0-20191216070243-e5efeac4e302 h1:jOh3Kh03uOFkRPV3PI4Am5tqACv2aELgbPgr7YgNX00= +github.com/muesli/reflow v0.0.0-20191216070243-e5efeac4e302/go.mod h1:I9bWAt7QTg/que/qmUCJBGlj7wEq8OAFBjPNjc6xK4I= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= @@ -279,6 +314,7 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -344,8 +380,12 @@ github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U= github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.19 h1:0s2/60x0XsFCXHeFut+F3azDVAAyIMyUfJRbRexiTYs= +github.com/yuin/goldmark v1.1.19/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= From 66821ffaf8e85a139a102c2744bd18e8976ee13b Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 15:13:38 -0700 Subject: [PATCH 27/33] Play with command format templating --- command/root.go | 14 +++++++---- command/servo.go | 26 +++++++++++--------- command/servo_image.go | 9 +++---- command/servo_test.go | 2 +- command/vital.go | 54 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 command/vital.go diff --git a/command/root.go b/command/root.go index 0db5df9..8fbe1df 100644 --- a/command/root.go +++ b/command/root.go @@ -61,9 +61,10 @@ We'd love to hear your feedback at `, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, - SilenceUsage: true, - SilenceErrors: true, - Version: "0.0.1", + SilenceUsage: true, + SilenceErrors: true, + Version: "0.0.1", + DisableFlagsInUseLine: true, } // Link our root command to Cobra @@ -505,10 +506,13 @@ Examples: {{ .Example }} {{- end}} -{{- if .HasAvailableFlags}} +{{- if .HasAvailableLocalFlags}} Options: -{{ wrappedFlagUsages . | trimRightSpace}} +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Options: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} {{- end}} {{- if hasManagementSubCommands . }} diff --git a/command/servo.go b/command/servo.go index fcf024b..8fec67d 100644 --- a/command/servo.go +++ b/command/servo.go @@ -69,23 +69,27 @@ func NewServoCommand(baseCmd *BaseCommand) *cobra.Command { listCmd.Flags().BoolVarP(&servoCommand.verbose, "verbose", "v", false, "Display verbose output") servoCmd.AddCommand(listCmd) addCmd := &cobra.Command{ - Use: "add", - Annotations: map[string]string{"registry": "true"}, - Short: "Add a Servo", - Args: cobra.MaximumNArgs(1), - RunE: servoCommand.RunAddServo, + Use: "add [OPTIONS] [NAME]", + Long: "Add a Servo to the local registry", + Annotations: map[string]string{"registry": "true"}, + Short: "Add a Servo", + Args: cobra.MaximumNArgs(1), + RunE: servoCommand.RunAddServo, + DisableFlagsInUseLine: true, } addCmd.Flags().BoolP("bastion", "b", false, "Use a bastion host for access") addCmd.Flags().String("bastion-host", "", "Specify the bastion host (format is user@host[:port])") servoCmd.AddCommand(addCmd) removeCmd := &cobra.Command{ - Use: "remove", - Annotations: map[string]string{"registry": "true"}, - Aliases: []string{"rm"}, - Short: "Remove a Servo", - Args: cobra.ExactArgs(1), - RunE: servoCommand.RunRemoveServo, + Use: "remove [OPTIONS] [NAME]", + Long: "Remove a Servo from the local registry", + Annotations: map[string]string{"registry": "true"}, + Aliases: []string{"rm"}, + Short: "Remove a Servo", + Args: cobra.ExactArgs(1), + RunE: servoCommand.RunRemoveServo, + DisableFlagsInUseLine: true, } removeCmd.Flags().BoolVarP(&servoCommand.force, "force", "f", false, "Don't prompt for confirmation") servoCmd.AddCommand(removeCmd) diff --git a/command/servo_image.go b/command/servo_image.go index d5d56b2..251987e 100644 --- a/command/servo_image.go +++ b/command/servo_image.go @@ -63,10 +63,11 @@ func NewServoImageCommand(baseCmd *BaseCommand) *cobra.Command { RunE: servoImageCommand.RunInfo, }) pullCmd := &cobra.Command{ - Use: "pull", - Short: "Pull a Servo image with Docker", - Args: cobra.ExactArgs(1), - RunE: servoImageCommand.RunPull, + Use: "pull [OPTIONS] IMAGE", + Short: "Pull a Servo image with Docker", + Args: cobra.ExactArgs(1), + RunE: servoImageCommand.RunPull, + DisableFlagsInUseLine: true, } pullCmd.Flags().StringP(hostArg, "H", "", "Docket host to connect to (overriding DOCKER_HOST)") servoImageCobra.AddCommand(pullCmd) diff --git a/command/servo_test.go b/command/servo_test.go index b0cabc3..0e1e287 100644 --- a/command/servo_test.go +++ b/command/servo_test.go @@ -96,7 +96,7 @@ func (s *ServoTestSuite) TestRunningLogsTimestampsHelp() { func (s *ServoTestSuite) TestRunningAddHelp() { output, err := s.Execute("servo", "add", "--help") s.Require().NoError(err) - s.Require().Contains(output, "Add a Servo") + s.Require().Contains(output, "Add a Servo to the local registry") } func (s *ServoTestSuite) TestRunningAddNoInput() { diff --git a/command/vital.go b/command/vital.go new file mode 100644 index 0000000..8059060 --- /dev/null +++ b/command/vital.go @@ -0,0 +1,54 @@ +// Copyright 2020 Opsani +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "fmt" + + "github.com/charmbracelet/glamour" + "github.com/spf13/cobra" +) + +type vitalCommand struct { + *BaseCommand +} + +// NewVitalCommand returns a new instance of the vital command +func NewVitalCommand(baseCmd *BaseCommand) *cobra.Command { + vitalCommand := vitalCommand{BaseCommand: baseCmd} + cobraCmd := &cobra.Command{ + Use: "vital", + Short: "Start optimizing", + Args: cobra.NoArgs, + PersistentPreRunE: nil, + RunE: vitalCommand.RunVital, + } + + return cobraCmd +} + +func (vitalCommand *vitalCommand) RunVital(cobraCmd *cobra.Command, args []string) error { + in := `# Opsani Vital + + This is a simple example of glamour! + Check out the [other examples](https://github.com/charmbracelet/glamour/tree/master/examples). + + Bye! + ` + + out, _ := glamour.Render(in, "dark") + fmt.Print(out) + return nil +} From 7c52c9e30819181be9b7e15fc5a9b3bfae14aaf9 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 18:30:57 -0700 Subject: [PATCH 28/33] List images and plugins --- command/servo_image.go | 13 ++++++++- command/servo_plugin.go | 59 +++++++++++++++++++++++++++++++++++++++++ go.mod | 3 +++ go.sum | 6 +++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/command/servo_image.go b/command/servo_image.go index 251987e..378714d 100644 --- a/command/servo_image.go +++ b/command/servo_image.go @@ -17,9 +17,11 @@ package command import ( "context" "strings" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/client" + "github.com/dustin/go-humanize" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -75,6 +77,11 @@ func NewServoImageCommand(baseCmd *BaseCommand) *cobra.Command { return servoImageCobra } +func abbreviateImageID(id string) string { + id = strings.TrimPrefix(id, "sha256:") + return id[0:12] +} + func (cmd *servoImageCommand) RunList(_ *cobra.Command, args []string) error { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) @@ -99,13 +106,17 @@ func (cmd *servoImageCommand) RunList(_ *cobra.Command, args []string) error { table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) + table.SetHeader([]string{"IMAGE", "ID", "CREATED", "SIZE"}) data := [][]string{} for _, image := range images { for _, repoTag := range image.RepoTags { if strings.HasPrefix(repoTag, "opsani/") { data = append(data, []string{ - repoTag, image.ID, + repoTag, + abbreviateImageID(image.ID), + humanize.Time(time.Unix(image.Created, 0)), + humanize.Bytes(uint64(image.Size)), }) } } diff --git a/command/servo_plugin.go b/command/servo_plugin.go index dd0ff29..f4575d2 100644 --- a/command/servo_plugin.go +++ b/command/servo_plugin.go @@ -15,6 +15,13 @@ package command import ( + "context" + "fmt" + "strings" + + "github.com/dustin/go-humanize" + "github.com/google/go-github/github" + "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -79,6 +86,58 @@ func NewServoPluginCommand(baseCmd *BaseCommand) *cobra.Command { } func (cmd *servoPluginCommand) RunList(_ *cobra.Command, args []string) error { + client := github.NewClient(nil) + + ctx := context.Background() + opt := new(github.RepositoryListByOrgOptions) + var allRepos []*github.Repository + for { + repos, resp, err := client.Repositories.ListByOrg(ctx, "opsani", opt) + if err != nil { + return err + } + for _, repo := range repos { + // Skip non-servo repos + if !strings.HasPrefix(*repo.Name, "servo-") { + continue + } + allRepos = append(allRepos, repo) + } + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + // Build a table outputting all the servo plugins + table := tablewriter.NewWriter(cmd.OutOrStdout()) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") // pad with tabs + table.SetNoWhiteSpace(true) + + data := [][]string{} + headers := []string{"NAME", "DESCRIPTION", "UPDATED", "URL"} + for _, repo := range allRepos { + row := []string{ + repo.GetName(), + repo.GetDescription(), + humanize.Time(repo.GetUpdatedAt().Time), + fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\\n", repo.GetHTMLURL(), repo.GetHTMLURL()), + } + data = append(data, row) + } + table.SetHeader(headers) + table.AppendBulk(data) + table.Render() + return nil } diff --git a/go.mod b/go.mod index 9042418..05a0c6e 100644 --- a/go.mod +++ b/go.mod @@ -20,10 +20,13 @@ require ( github.com/docker/docker v1.13.1 github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.9.0 github.com/go-resty/resty/v2 v2.2.0 github.com/goccy/go-yaml v1.4.3 github.com/golang/protobuf v1.4.0 // indirect + github.com/google/go-github v17.0.0+incompatible + github.com/google/go-querystring v1.0.0 // indirect github.com/googleapis/gnostic v0.4.0 // indirect github.com/gorilla/mux v1.7.4 // indirect github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 diff --git a/go.sum b/go.sum index e560789..f95bac8 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -151,6 +153,10 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= From f23cc309d7934f704cca8f15e26bd5d597b1e1f4 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 21:14:33 -0700 Subject: [PATCH 29/33] Experiments in a CLI driven activation workflow --- command/discover.go | 5 ++- command/vital.go | 104 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/command/discover.go b/command/discover.go index 18aa9b4..aa026dd 100644 --- a/command/discover.go +++ b/command/discover.go @@ -43,7 +43,6 @@ const hostArg = "host" const kubeconfigArg = "kubeconfig" func runIntelligentManifestBuilderCommand(cmd *cobra.Command, args []string) error { - ctx := context.Background() imageRef, err := cmd.Flags().GetString(imageArg) if err != nil { return err @@ -53,7 +52,11 @@ func runIntelligentManifestBuilderCommand(cmd *cobra.Command, args []string) err if err != nil { return err } + return runIntelligentManifestBuilder(dockerHost, imageRef) +} +func runIntelligentManifestBuilder(dockerHost string, imageRef string) error { + ctx := context.Background() di, err := NewDockerInterface(dockerHost) if err != nil { return err diff --git a/command/vital.go b/command/vital.go index 8059060..aef7b0f 100644 --- a/command/vital.go +++ b/command/vital.go @@ -15,9 +15,12 @@ package command import ( + "context" "fmt" + "github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/glamour" + "github.com/mgutz/ansi" "github.com/spf13/cobra" ) @@ -40,15 +43,98 @@ func NewVitalCommand(baseCmd *BaseCommand) *cobra.Command { } func (vitalCommand *vitalCommand) RunVital(cobraCmd *cobra.Command, args []string) error { - in := `# Opsani Vital - - This is a simple example of glamour! - Check out the [other examples](https://github.com/charmbracelet/glamour/tree/master/examples). - - Bye! - ` - - out, _ := glamour.Render(in, "dark") + in := + `# Opsani Vital + +## Let's talk about your cloud costs + +It's the worst kept secret in modern tech. We're all spending way more on infrastructure than is necesary. + +But it's not our fault. Our applications have become too big and complicated to optimize. + +Until now. + +## Better living through machine learning... + +Opsani utilizes state of the art machine learning technology to continuously optimize your applications for *cost* and *performance*. + +## Getting started + +To start optimizing, a Servo must be deployed into your environment. + +A Servo is a lightweight container that lets Opsani know what is going on in your application and recommend optimizations. + +This app is designed to assist you in assembling and deploying a Servo through the miracle of automation and sensible defaults. + +The process looks like... + +- [x] Register for Vital +- [x] Install Opsani +- [x] Read this doc +- [ ] Deploy the Servo +- [ ] Start optimizing + +## Things to keep in mind + +All software run and deployed is Open Source. Opsani supports manual and assisted integrations if you like to do things the hard way. + +Over the next 20 minutes, we will gather details about your application, the deployment environment, and your optimization goals. + +The process will involve cloning Git repositories, connecting to your metrics & orchestration systems, and running Docker containers. + +As tasks are completed, artifacts will be generated and saved onto this workstation. + +Everything is logged, you can be pause and resume at any time, and important items will require confirmation. + +Once this is wrapped up, you can start optimizing immediately. +` + r, _ := glamour.NewTermRenderer( + // detect background color and pick either the default dark or light theme + glamour.WithStandardStyle("dark"), + // wrap output at specific width + glamour.WithWordWrap(80), + ) + out, _ := r.Render(in) fmt.Print(out) + + confirmed := false + prompt := &survey.Confirm{ + Message: "Ready to get started?", + } + vitalCommand.AskOne(prompt, &confirmed) + + if confirmed { + fmt.Printf("\n💥 Let's do this thing.\n") + return vitalCommand.RunVitalDiscovery(cobraCmd, args) + } + return nil } + +func (vitalCommand *vitalCommand) RunVitalDiscovery(cobraCmd *cobra.Command, args []string) error { + ctx := context.Background() + + // cache escape codes and build strings manually + // lime := ansi.ColorCode("green+h:black") + blue := ansi.Blue + reset := ansi.ColorCode("reset") + whiteBold := ansi.ColorCode("white+b") + // lightCyan := ansi.LightCyan + + // Pul the IMB image + imageRef := fmt.Sprintf("%s:%s", imbImageName, imbTargetVersion) + fmt.Printf("\n%s==>%s %sPulling %s...%s\n", blue, reset, whiteBold, imageRef, reset) + di, err := NewDockerInterface("") + if err != nil { + return err + } + + err = di.PullImageWithProgressReporting(ctx, imageRef) + if err != nil { + return err + } + + // Start discovery + fmt.Printf("\n%s==>%s %sLaunching container...%s\n", blue, reset, whiteBold, reset) + return runIntelligentManifestBuilder("", imageRef) +} From 6a4f5ba6139a7729c37ab658dce30e6256d8f58f Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 21:15:14 -0700 Subject: [PATCH 30/33] Uncapitalize assemblies, images, and plugins as they aren't first class concepts --- command/servo_assembly.go | 2 +- command/servo_image.go | 2 +- command/servo_plugin.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command/servo_assembly.go b/command/servo_assembly.go index 019f693..49a92b7 100644 --- a/command/servo_assembly.go +++ b/command/servo_assembly.go @@ -28,7 +28,7 @@ func NewServoAssemblyCommand(baseCmd *BaseCommand) *cobra.Command { servoAssemblyCobra := &cobra.Command{ Use: "assembly", - Short: "Manage Servo Assemblies", + Short: "Manage Servo assemblies", Args: cobra.NoArgs, PersistentPreRunE: ReduceRunEFuncs( baseCmd.InitConfigRunE, diff --git a/command/servo_image.go b/command/servo_image.go index 378714d..21e1c38 100644 --- a/command/servo_image.go +++ b/command/servo_image.go @@ -36,7 +36,7 @@ func NewServoImageCommand(baseCmd *BaseCommand) *cobra.Command { servoImageCobra := &cobra.Command{ Use: "image", - Short: "Manage Servo Images", + Short: "Manage Servo images", Args: cobra.NoArgs, PersistentPreRunE: ReduceRunEFuncs( baseCmd.InitConfigRunE, diff --git a/command/servo_plugin.go b/command/servo_plugin.go index f4575d2..3873c93 100644 --- a/command/servo_plugin.go +++ b/command/servo_plugin.go @@ -35,7 +35,7 @@ func NewServoPluginCommand(baseCmd *BaseCommand) *cobra.Command { servoPluginCobra := &cobra.Command{ Use: "plugin", - Short: "Manage Servo Plugins", + Short: "Manage Servo plugins", Args: cobra.NoArgs, PersistentPreRunE: ReduceRunEFuncs( baseCmd.InitConfigRunE, From 3b927b783b6d33933a461f6d85c303a4f9dc62a2 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 21:38:52 -0700 Subject: [PATCH 31/33] Truncate repo descriptions at 48 characters --- command/servo_plugin.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/command/servo_plugin.go b/command/servo_plugin.go index 3873c93..68e2084 100644 --- a/command/servo_plugin.go +++ b/command/servo_plugin.go @@ -29,6 +29,13 @@ type servoPluginCommand struct { *BaseCommand } +func truncateStringToLimit(str string, limit int) string { + if len(str) <= limit { + return str + } + return str[0:limit] + "..." +} + // NewServoPluginCommand returns a new instance of the servo image command func NewServoPluginCommand(baseCmd *BaseCommand) *cobra.Command { servoPluginCommand := servoPluginCommand{BaseCommand: baseCmd} @@ -128,9 +135,9 @@ func (cmd *servoPluginCommand) RunList(_ *cobra.Command, args []string) error { for _, repo := range allRepos { row := []string{ repo.GetName(), - repo.GetDescription(), + truncateStringToLimit(repo.GetDescription(), 48), humanize.Time(repo.GetUpdatedAt().Time), - fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\\n", repo.GetHTMLURL(), repo.GetHTMLURL()), + fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\\n", repo.GetHTMLURL(), strings.TrimPrefix(repo.GetHTMLURL(), "https://github.com/")), } data = append(data, row) } From a30db00ad770893ff93910f45f8ff2142372a3fc Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 22:26:30 -0700 Subject: [PATCH 32/33] Remove escape sequence hyperlinking trick because it messes up table output --- command/servo_plugin.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/command/servo_plugin.go b/command/servo_plugin.go index 68e2084..77f7577 100644 --- a/command/servo_plugin.go +++ b/command/servo_plugin.go @@ -16,7 +16,6 @@ package command import ( "context" - "fmt" "strings" "github.com/dustin/go-humanize" @@ -137,7 +136,7 @@ func (cmd *servoPluginCommand) RunList(_ *cobra.Command, args []string) error { repo.GetName(), truncateStringToLimit(repo.GetDescription(), 48), humanize.Time(repo.GetUpdatedAt().Time), - fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\\n", repo.GetHTMLURL(), strings.TrimPrefix(repo.GetHTMLURL(), "https://github.com/")), + repo.GetHTMLURL(), } data = append(data, row) } From a39801243cfcfa35a6e8a794bc03da4c0244cb23 Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Sun, 26 Apr 2020 23:51:01 -0700 Subject: [PATCH 33/33] Implement plugin clone operation --- command/servo_plugin.go | 20 +++++++++++++++++- go.mod | 5 ++++- go.sum | 45 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/command/servo_plugin.go b/command/servo_plugin.go index 77f7577..020a99a 100644 --- a/command/servo_plugin.go +++ b/command/servo_plugin.go @@ -16,9 +16,11 @@ package command import ( "context" + "os" "strings" "github.com/dustin/go-humanize" + "github.com/go-git/go-git/v5" "github.com/google/go-github/github" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" @@ -156,7 +158,23 @@ func (cmd *servoPluginCommand) RunInfo(_ *cobra.Command, args []string) error { } func (cmd *servoPluginCommand) RunClone(c *cobra.Command, args []string) error { - return nil + repoName := strings.TrimPrefix(args[0], "opsani/") + + // Get repo details from GitHub + ctx := context.Background() + client := github.NewClient(nil) + repo, _, err := client.Repositories.Get(ctx, "opsani", repoName) + if err != nil { + return err + } + + // Clone the given repository to the given directory + pwd, _ := os.Getwd() + _, err = git.PlainClone(pwd, false, &git.CloneOptions{ + URL: *repo.CloneURL, + Progress: os.Stdout, + }) + return err } func (cmd *servoPluginCommand) RunFork(c *cobra.Command, args []string) error { diff --git a/go.mod b/go.mod index 05a0c6e..24801f2 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,8 @@ require ( github.com/docker/go-units v0.4.0 // indirect github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.9.0 + github.com/go-git/go-git v4.7.0+incompatible + github.com/go-git/go-git/v5 v5.0.0 github.com/go-resty/resty/v2 v2.2.0 github.com/goccy/go-yaml v1.4.3 github.com/golang/protobuf v1.4.0 // indirect @@ -57,13 +59,14 @@ require ( github.com/stretchr/testify v1.5.1 github.com/tidwall/gjson v1.6.0 github.com/tidwall/sjson v1.1.1 - golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 + golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect google.golang.org/genproto v0.0.0-20200413115906-b5235f65be36 // indirect google.golang.org/grpc v1.28.1 // indirect gopkg.in/ini.v1 v1.55.0 // indirect + gopkg.in/src-d/go-git.v4 v4.13.1 // indirect gopkg.in/yaml.v2 v2.2.8 gotest.tools v2.2.0+incompatible // indirect k8s.io/apimachinery v0.18.1 diff --git a/go.sum b/go.sum index f95bac8..b20738e 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,7 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= @@ -47,7 +48,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5Vpd github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -98,6 +101,8 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -106,10 +111,22 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git v1.0.0 h1:YcN9iDGDoXuIw0vHls6rINwV416HYa0EB2X+RBsyYp4= +github.com/go-git/go-git v4.7.0+incompatible h1:+W9rgGY4DOKKdX2x6HxSR7HNeTxqiKrOvKnuittYVdA= +github.com/go-git/go-git v4.7.0+incompatible/go.mod h1:6+421e08gnZWn30y26Vchf7efgYLe4dl5OQbBSUXShE= +github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg= +github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -199,6 +216,8 @@ github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -211,6 +230,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -229,6 +250,7 @@ github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23 h1:Wp7NjqGKGN9te9N/rvXYRhlVcrulGdxnz8zadXWs7fc= github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= @@ -277,6 +299,7 @@ github.com/muesli/reflow v0.0.0-20191216070243-e5efeac4e302/go.mod h1:I9bWAt7QTg github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= @@ -295,6 +318,7 @@ github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVo github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= @@ -362,9 +386,12 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -388,6 +415,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.19 h1:0s2/60x0XsFCXHeFut+F3azDVAAyIMyUfJRbRexiTYs= @@ -400,12 +429,16 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -422,9 +455,12 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= @@ -460,6 +496,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -496,6 +533,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= @@ -508,8 +546,15 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=