diff --git a/client/client.go b/client/client.go index 0315e288..5fe61008 100644 --- a/client/client.go +++ b/client/client.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "net/url" "path" @@ -26,18 +25,27 @@ type Client struct { buildType string buildFilePath string + out io.Writer + sshRunner SSHSession verifySSL bool } // NewClient sets up a client to communicate to the daemon at // the given named remote. -func NewClient(remoteName string, config *cfg.Config) (*Client, bool) { +func NewClient(remoteName string, config *cfg.Config, out ...io.Writer) (*Client, bool) { remote, found := config.GetRemote(remoteName) if !found { return nil, false } + var writer io.Writer + if len(out) > 0 { + writer = out[0] + } else { + writer = common.DevNull{} + } + return &Client{ RemoteVPS: remote, version: config.Version, @@ -45,6 +53,8 @@ func NewClient(remoteName string, config *cfg.Config) (*Client, bool) { buildType: config.BuildType, buildFilePath: config.BuildFilePath, sshRunner: NewSSHRunner(remote), + + out: writer, }, true } @@ -59,15 +69,15 @@ func (c *Client) SetSSLVerification(verify bool) { // public-private key-pair. It outputs configuration information // for the user. func (c *Client) BootstrapRemote(repoName string) error { - println("Setting up remote \"" + c.Name + "\" at " + c.IP) + fmt.Fprintf(c.out, "Setting up remote %s at %s", c.Name, c.IP) - println(">> Step 1/4: Installing docker...") + fmt.Fprint(c.out, ">> Step 1/4: Installing docker...") err := c.installDocker(c.sshRunner) if err != nil { return err } - println("\n>> Step 2/4: Building deploy key...") + fmt.Fprint(c.out, "\n>> Step 2/4: Building deploy key...") if err != nil { return err } @@ -78,7 +88,7 @@ func (c *Client) BootstrapRemote(repoName string) error { // This step needs to run before any other commands that rely on // the daemon image, since the daemon is loaded here. - println("\n>> Step 3/4: Starting daemon...") + fmt.Fprint(c.out, "\n>> Step 3/4: Starting daemon...") if err != nil { return err } @@ -87,34 +97,34 @@ func (c *Client) BootstrapRemote(repoName string) error { return err } - println("\n>> Step 4/4: Fetching daemon API token...") + fmt.Fprint(c.out, "\n>> Step 4/4: Fetching daemon API token...") token, err := c.getDaemonAPIToken(c.sshRunner, c.version) if err != nil { return err } c.Daemon.Token = token - println("\nInertia has been set up and daemon is running on remote!") - println("You may have to wait briefly for Inertia to set up some dependencies.") - fmt.Printf("Use 'inertia %s logs --stream' to check on the daemon's setup progress.\n\n", c.Name) + fmt.Fprint(c.out, "\nInertia has been set up and daemon is running on remote!") + fmt.Fprint(c.out, "You may have to wait briefly for Inertia to set up some dependencies.") + fmt.Fprintf(c.out, "Use 'inertia %s logs --stream' to check on the daemon's setup progress.\n\n", c.Name) - println("=============================\n") + fmt.Fprint(c.out, "=============================\n") // Output deploy key to user. - println(">> GitHub Deploy Key (add to https://www.github.com/" + repoName + "/settings/keys/new): ") - println(pub.String()) + fmt.Fprintf(c.out, ">> GitHub Deploy Key (add to https://www.github.com/%s/settings/keys/new): ", repoName) + fmt.Fprint(c.out, pub.String()) // Output Webhook url to user. - println(">> GitHub WebHook URL (add to https://www.github.com/" + repoName + "/settings/hooks/new): ") - println("WebHook Address: https://" + c.IP + ":" + c.Daemon.Port + "/webhook") - println("WebHook Secret: " + c.Daemon.WebHookSecret) - println(`Note that you will have to disable SSH verification in your webhook + fmt.Fprintf(c.out, ">> GitHub WebHook URL (add to https://www.github.com/%s/settings/hooks/new): ", repoName) + fmt.Fprintf(c.out, "WebHook Address: https://%s:%s/webhook", c.IP, c.Daemon.Port) + fmt.Fprint(c.out, "WebHook Secret: "+c.Daemon.WebHookSecret) + fmt.Fprint(c.out, `Note that you will have to disable SSH verification in your webhook settings - Inertia uses self-signed certificates that GitHub won't -be able to verify.` + "\n") +be able to verify.`+"\n") - println(`Inertia daemon successfully deployed! Add your webhook url and deploy + fmt.Fprint(c.out, `Inertia daemon successfully deployed! Add your webhook url and deploy key to enable continuous deployment.`) - fmt.Printf("Then run 'inertia %s up' to deploy your application.\n", c.Name) + fmt.Fprintf(c.out, "Then run 'inertia %s up' to deploy your application.\n", c.Name) return nil } @@ -140,8 +150,7 @@ func (c *Client) DaemonDown() error { _, stderr, err := c.sshRunner.Run(string(scriptBytes)) if err != nil { - println(stderr.String()) - return err + return fmt.Errorf("daemon shutdown failed: %s: %s", err.Error(), stderr.String()) } return nil @@ -158,8 +167,7 @@ func (c *Client) installDocker(session SSHSession) error { cmdStr := string(installDockerSh) _, stderr, err := session.Run(cmdStr) if err != nil { - println(stderr.String()) - return err + return fmt.Errorf("docker installation: %s: %s", err.Error(), stderr.String()) } return nil @@ -177,8 +185,7 @@ func (c *Client) keyGen(session SSHSession) (*bytes.Buffer, error) { result, stderr, err := session.Run(string(scriptBytes)) if err != nil { - log.Println(stderr.String()) - return nil, err + return nil, fmt.Errorf("key generation failed: %s: %s", err.Error(), stderr.String()) } return result, nil @@ -195,8 +202,7 @@ func (c *Client) getDaemonAPIToken(session SSHSession, daemonVersion string) (st stdout, stderr, err := session.Run(daemonCmdStr) if err != nil { - log.Println(stderr.String()) - return "", err + return "", fmt.Errorf("api token generation failed: %s: %s", err.Error(), stderr.String()) } // There may be a newline, remove it. diff --git a/client/client_test.go b/client/client_test.go index 8c2570e9..fc1930db 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" @@ -47,6 +48,7 @@ func getMockClient(ts *httptest.Server) *Client { return &Client{ RemoteVPS: mockRemote, + out: os.Stdout, project: "test_project", } } @@ -66,12 +68,14 @@ func getIntegrationClient(mockRunner *mockSSHRunner) *Client { return &Client{ version: "test", RemoteVPS: remote, + out: os.Stdout, sshRunner: mockRunner, } } return &Client{ version: "test", RemoteVPS: remote, + out: os.Stdout, sshRunner: NewSSHRunner(remote), } } diff --git a/cmd/deploy.go b/cmd/deploy.go index 1c557057..8255dd21 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -3,7 +3,6 @@ package cmd import ( "bufio" "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" @@ -138,7 +137,7 @@ var cmdDeploymentUp = &cobra.Command{ to be active on your remote - do this by running 'inertia [REMOTE] init'`, Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -202,7 +201,7 @@ var cmdDeploymentDown = &cobra.Command{ Requires project to be online - do this by running 'inertia [REMOTE] up`, Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -239,7 +238,7 @@ var cmdDeploymentStatus = &cobra.Command{ running 'inertia [REMOTE] up'`, Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -286,7 +285,7 @@ var cmdDeploymentLogs = &cobra.Command{ status' to see what containers are accessible.`, Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -346,7 +345,7 @@ var cmdDeploymentSSH = &cobra.Command{ Long: `Starts up an interact SSH session with your remote.`, Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath) + deployment, _, err := local.GetClient(remoteName, configFilePath) if err != nil { log.Fatal(err) } @@ -366,7 +365,7 @@ deployment. Provide a relative path to your file.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -422,27 +421,21 @@ request access to the repository via a public key, and will listen for updates to this repository's remote master branch.`, Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Use, " ")[0] + cli, write, err := local.GetClient(remoteName, configFilePath, cmd) + if err != nil { + log.Fatal(err) + } - // Bootstrap needs to write to configuration. - config, path, err := local.GetProjectConfigFromDisk(configFilePath) + url, err := local.GetRepoRemote("origin") if err != nil { log.Fatal(err) } - cli, found := client.NewClient(remoteName, config) - if found { - url, err := local.GetRepoRemote("origin") - if err != nil { - log.Fatal(err) - } - repoName := common.ExtractRepository(common.GetSSHRemoteURL(url)) - err = cli.BootstrapRemote(repoName) - if err != nil { - log.Fatal(err) - } - config.Write(path) - } else { - log.Fatal(errors.New("There does not appear to be a remote with this name. Have you modified the Inertia configuration file?")) + repoName := common.ExtractRepository(common.GetSSHRemoteURL(url)) + err = cli.BootstrapRemote(repoName) + if err != nil { + log.Fatal(err) } + write() }, } @@ -456,7 +449,7 @@ remote. Requires Inertia daemon to be active on your remote - do this by running 'inertia [REMOTE] init'`, Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } diff --git a/cmd/env.go b/cmd/env.go index ec46bd4d..35829ada 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -23,7 +23,7 @@ variables are applied to all deployed containers.`, Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -55,7 +55,7 @@ and persistent environment storage.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -79,7 +79,7 @@ var cmdDeploymentEnvList = &cobra.Command{ Short: "List currently set and saved environment variables", Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } diff --git a/cmd/provision.go b/cmd/provision.go index cd67e749..576a0005 100644 --- a/cmd/provision.go +++ b/cmd/provision.go @@ -66,9 +66,9 @@ var cmdProvisionECS = &cobra.Command{ if err != nil { log.Fatal(err) } - prov, err = provision.NewEC2Provisioner(id, key) + prov, err = provision.NewEC2Provisioner(id, key, os.Stdout) } else { - prov, err = provision.NewEC2ProvisionerFromEnv() + prov, err = provision.NewEC2ProvisionerFromEnv(os.Stdout) } if err != nil { log.Fatal(err) @@ -137,7 +137,7 @@ var cmdProvisionECS = &cobra.Command{ config.Write(path) // Create inertia client - inertia, found := client.NewClient(args[0], config) + inertia, found := client.NewClient(args[0], config, os.Stdout) if !found { log.Fatal("vps setup did not complete properly") } diff --git a/cmd/users.go b/cmd/users.go index 9145f8e2..81a2e7ba 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -31,7 +31,7 @@ Use the --admin flag to create an admin user.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -80,7 +80,7 @@ deployment from the web app.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -117,7 +117,7 @@ from the web app.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } @@ -151,7 +151,7 @@ var cmdDeploymentListUsers = &cobra.Command{ Long: `List all users with access to Inertia Web on your remote.`, Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Parent().Use, " ")[0] - deployment, err := local.GetClient(remoteName, configFilePath, cmd) + deployment, _, err := local.GetClient(remoteName, configFilePath, cmd) if err != nil { log.Fatal(err) } diff --git a/common/util.go b/common/util.go index faa45f3f..34127819 100644 --- a/common/util.go +++ b/common/util.go @@ -10,6 +10,13 @@ import ( "time" ) +// DevNull writes to null, since a nil io.Writer will break shit +type DevNull struct{} + +func (dn DevNull) Write(p []byte) (n int, err error) { + return len(p), nil +} + // GetFullPath returns the absolute path of the config file. func GetFullPath(relPath string) (string, error) { path, err := os.Getwd() diff --git a/local/storage.go b/local/storage.go index a39284ce..2d487900 100644 --- a/local/storage.go +++ b/local/storage.go @@ -88,26 +88,28 @@ func GetProjectConfigFromDisk(relPath string) (*cfg.Config, string, error) { } // GetClient returns a local deployment setup -func GetClient(name, relPath string, cmd ...*cobra.Command) (*client.Client, error) { - config, _, err := GetProjectConfigFromDisk(relPath) +func GetClient(name, relPath string, cmd ...*cobra.Command) (*client.Client, func() error, error) { + config, path, err := GetProjectConfigFromDisk(relPath) if err != nil { - return nil, err + return nil, nil, err } - client, found := client.NewClient(name, config) + client, found := client.NewClient(name, config, os.Stdout) if !found { - return nil, errors.New("Remote not found") + return nil, nil, errors.New("Remote not found") } if len(cmd) == 1 && cmd[0] != nil { verify, err := cmd[0].Flags().GetBool("verify-ssl") if err != nil { - return nil, err + return nil, nil, err } client.SetSSLVerification(verify) } - return client, nil + return client, func() error { + return config.Write(path) + }, nil } // SaveKey writes a key to given path diff --git a/local/storage_test.go b/local/storage_test.go index dff3b16e..ccbc1a85 100644 --- a/local/storage_test.go +++ b/local/storage_test.go @@ -63,11 +63,11 @@ func TestConfigCreateAndWriteAndRead(t *testing.T) { assert.Equal(t, config.Remotes["test2"], readConfig.Remotes["test2"]) // Test client read - client, err := GetClient("test2", "inertia.toml") + client, _, err := GetClient("test2", "inertia.toml") assert.Nil(t, err) assert.Equal(t, "test2", client.Name) assert.Equal(t, "12343:80801", client.GetIPAndPort()) - _, err = GetClient("asdf", "inertia.toml") + _, _, err = GetClient("asdf", "inertia.toml") assert.NotNil(t, err) // Test config remove diff --git a/provision/ec2.go b/provision/ec2.go index d2cb31da..3c188c83 100644 --- a/provision/ec2.go +++ b/provision/ec2.go @@ -2,6 +2,7 @@ package provision import ( "fmt" + "io" "net" "os" "path/filepath" @@ -20,6 +21,7 @@ import ( // EC2Provisioner creates Amazon EC2 instances type EC2Provisioner struct { + out io.Writer user string session *session.Session client *ec2.EC2 @@ -27,16 +29,16 @@ type EC2Provisioner struct { // NewEC2Provisioner creates a client to interact with Amazon EC2 using the // given credentials -func NewEC2Provisioner(id, key string) (*EC2Provisioner, error) { +func NewEC2Provisioner(id, key string, out ...io.Writer) (*EC2Provisioner, error) { prov := &EC2Provisioner{} - return prov, prov.init(credentials.NewStaticCredentials(id, key, "")) + return prov, prov.init(credentials.NewStaticCredentials(id, key, ""), out) } // NewEC2ProvisionerFromEnv creates a client to interact with Amazon EC2 using // credentials from environment -func NewEC2ProvisionerFromEnv() (*EC2Provisioner, error) { +func NewEC2ProvisionerFromEnv(out ...io.Writer) (*EC2Provisioner, error) { prov := &EC2Provisioner{} - return prov, prov.init(credentials.NewEnvCredentials()) + return prov, prov.init(credentials.NewEnvCredentials(), out) } // GetUser returns the user attached to given credentials @@ -164,7 +166,7 @@ func (p *EC2Provisioner) CreateInstance(opts EC2CreateInstanceOptions) (*cfg.Rem } // Loop until intance is running - println("Checking status of requested instance...") + fmt.Fprint(p.out, "Checking status of requested instance...") attempts := 0 var instanceStatus *ec2.DescribeInstancesOutput for { @@ -180,16 +182,16 @@ func (p *EC2Provisioner) CreateInstance(opts EC2CreateInstanceOptions) (*cfg.Rem // A reservation corresponds to a command to start instances time.Sleep(3 * time.Second) continue - } else if *result.Reservations[0].Instances[0].State.Code != 16 { - // Code 16 indicates instance is running - println("Instance status: " + *result.Reservations[0].Instances[0].State.Name) - time.Sleep(3 * time.Second) - continue - } else { + } else if *result.Reservations[0].Instances[0].State.Code == 16 { // Code 16 means we can continue! - println("Instance is running!") + fmt.Fprint(p.out, "Instance is running!") instanceStatus = result break + } else { + // Keep polling + fmt.Fprint(p.out, "Instance status: "+*result.Reservations[0].Instances[0].State.Name) + time.Sleep(3 * time.Second) + continue } } @@ -207,15 +209,18 @@ func (p *EC2Provisioner) CreateInstance(opts EC2CreateInstanceOptions) (*cfg.Rem }, }, }) + if err != nil { + fmt.Fprintf(p.out, "Failed to set tags: %s", err.Error()) + } // Poll for SSH port to open - println("Waiting for port 22 to open...") + fmt.Fprint(p.out, "Waiting for ports to open...") for { time.Sleep(3 * time.Second) - println("Checking port...") + fmt.Fprint(p.out, "Checking ports...") conn, err := net.Dial("tcp", *instanceStatus.Reservations[0].Instances[0].PublicDnsName+":22") if err == nil { - println("Connection established!") + fmt.Fprint(p.out, "Connection established!") conn.Close() break } @@ -224,7 +229,8 @@ func (p *EC2Provisioner) CreateInstance(opts EC2CreateInstanceOptions) (*cfg.Rem // Generate webhook secret webhookSecret, err := common.GenerateRandomString() if err != nil { - println(err.Error()) + fmt.Fprint(p.out, err.Error()) + fmt.Fprint(p.out, "Using default secret 'inertia'") webhookSecret = "interia" } @@ -279,7 +285,12 @@ func (p *EC2Provisioner) exposePorts(securityGroupID string, daemonPort int64, p return err } -func (p *EC2Provisioner) init(creds *credentials.Credentials) error { +func (p *EC2Provisioner) init(creds *credentials.Credentials, out []io.Writer) error { + if len(out) > 0 { + p.out = out[0] + } else { + p.out = common.DevNull{} + } // Set default user p.user = "ec2-user"