diff --git a/Makefile b/Makefile index 2bcfdee0..2ddd8ff4 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,3 @@ -.PHONY: ls inertia inertia-tagged clean test test-v test-all test-integration test-integration-fast testenv testdaemon daemon bootstrap web-deps web-run web-build - TAG = `git describe --tags` SSH_PORT = 22 VPS_VERSION = latest @@ -9,10 +7,12 @@ RELEASE = canary all: deps bootstrap inertia # List all commands +.PHONY: ls ls: @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs # Sets up all dependencies +.PHONY: deps deps: go get -u github.com/jteeuwen/go-bindata/... dep ensure @@ -20,49 +20,62 @@ deps: bash test/deps.sh # Install Inertia with release version +.PHONY: inertia inertia: go install -ldflags "-X main.Version=$(RELEASE)" # Install Inertia with git tag as release version +.PHONY: inertia-tagged inertia-tagged: go install -ldflags "-X main.Version=$(TAG)" # Remove Inertia binaries +.PHONY: clean clean: rm -f ./inertia find . -type f -name inertia.\* -exec rm {} \; +.PHONY: lint lint: PATH=$(PATH):./bin bash -c './bin/gometalinter --vendor --deadline=60s ./...' # Run unit test suite +.PHONY: test test: go test ./... -short -ldflags "-X main.Version=test" --cover # Run unit test suite verbosely +.PHONY: test-v test-v: go test ./... -short -ldflags "-X main.Version=test" -v --cover # Run unit and integration tests - creates fresh test VPS and test daemon beforehand # Also attempts to run linter +.PHONY: test-all test-all: make lint make testenv VPS_OS=$(VPS_OS) VPS_VERSION=$(VPS_VERSION) make testdaemon + go clean -testcache go test ./... -ldflags "-X main.Version=test" --cover # Run integration tests verbosely - creates fresh test VPS and test daemon beforehand +.PHONY: test-integration test-integration: + go clean -testcache make testenv VPS_OS=$(VPS_OS) VPS_VERSION=$(VPS_VERSION) make testdaemon go test ./... -v -run 'Integration' -ldflags "-X main.Version=test" --cover # Run integration tests verbosely without recreating test VPS +.PHONY: test-integration-fast test-integration-fast: + go clean -testcache make testdaemon go test ./... -v -run 'Integration' -ldflags "-X main.Version=test" --cover # Create test VPS +.PHONY: testenv testenv: docker stop testvps || true && docker rm testvps || true docker build -f ./test/vps/Dockerfile.$(VPS_OS) \ @@ -73,6 +86,7 @@ testenv: # Create test daemon and scp the image to the test VPS for use. # Requires Inertia version to be "test" +.PHONY: testdaemon testdaemon: rm -f ./inertia-daemon-image docker build --build-arg INERTIA_VERSION=$(TAG) \ @@ -90,6 +104,7 @@ testdaemon: # Creates a daemon release and pushes it to Docker Hub repository. # Requires access to the UBC Launch Pad Docker Hub. +.PHONY: daemon daemon: docker build --build-arg INERTIA_VERSION=$(RELEASE) \ -t ubclaunchpad/inertia:$(RELEASE) . @@ -97,17 +112,21 @@ daemon: # Recompiles assets. Use whenever a script in client/bootstrap is # modified. +.PHONY: bootstrap bootstrap: go-bindata -o client/bootstrap.go -pkg client client/bootstrap/... # Install Inertia Web dependencies. Use PACKAGE to install something. +.PHONY: web-deps web-deps: (cd ./daemon/web; npm install $(PACKAGE)) # Run local development instance of Inertia Web. +.PHONY: web-run web-run: (cd ./daemon/web; npm start) # Build and minify Inertia Web. +.PHONY: web-build web-build: (cd ./daemon/web; npm install --production; npm run build) diff --git a/README.md b/README.md index 5ac4bf35..da6587da 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ---------------- -Inertia is a simple cross-platform command line application that enables effortless setup and management of continuous, automated deployment all sorts of projects on any virtual private server. It is built and maintained with :heart: by [UBC Launch Pad](https://www.ubclaunchpad.com/). +Inertia is a simple cross-platform command line application that enables quick and easy setup and management of continuous, automated deployment of a variety of project types on any virtual private server. It is used, built, and maintained with :heart: by [UBC Launch Pad](https://www.ubclaunchpad.com/).
@@ -63,13 +63,13 @@ Inertia is a simple cross-platform command line application that enables effortl All you need to get started is a [compatible project](https://github.com/ubclaunchpad/inertia/wiki/Project-Compatibility), the Inertia CLI, and access to a virtual private server. -**MacOS** - the CLI can be installed using [Homebrew](https://brew.sh): +MacOS users can install the CLI using [Homebrew](https://brew.sh): ```bash $> brew install ubclaunchpad/tap/inertia ``` -**Windows** - the CLI can be installed using [Scoop](http://scoop.sh): +Windows users can install the CLI using [Scoop](http://scoop.sh): ```bash $> scoop bucket add ubclaunchpad https://github.com/ubclaunchpad/scoop-bucket diff --git a/client/README.md b/client/README.md index 9b716c5f..d5eb7868 100644 --- a/client/README.md +++ b/client/README.md @@ -2,5 +2,27 @@ [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/ubclaunchpad/inertia/client) -This package contains Inertia's clientside configuration and interface to remote Inertia daemons. +This package contains Inertia's clientside configuration and interface to remote Inertia daemons. It can be imported for use if you don't like the CLI - for example: +```go +package main + +import "github.com/ubclaunchpad/inertia/client" + +func main() { + // Set up Inertia + config := client.NewConfig( + "0.3.0", "inertia-deploy-test", "docker-compose", + ) + + // Add your remote + config.AddRemote(&client.RemoteVPS{ + Name: "gcloud", // ...params + }) + + // Set up client, remote, and deploy your project + cli, _ := client.NewClient("gcloud", config) + cli.BootstrapRemote() + cli.Up("git@github.com:ubclaunchpad/inertia.git", "", false) +} +``` diff --git a/client/client.go b/client/client.go new file mode 100644 index 00000000..67a25afe --- /dev/null +++ b/client/client.go @@ -0,0 +1,288 @@ +package client + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strconv" + "strings" + + "github.com/ubclaunchpad/inertia/common" +) + +// Client manages a deployment +type Client struct { + *RemoteVPS + version string + project string + buildType string + sshRunner SSHSession +} + +// NewClient sets up a client to communicate to the daemon at +// the given named remote. +func NewClient(remoteName string, config *Config) (*Client, bool) { + remote, found := config.GetRemote(remoteName) + if !found { + return nil, false + } + + return &Client{ + RemoteVPS: remote, + sshRunner: NewSSHRunner(remote), + }, false +} + +// BootstrapRemote configures a remote vps for continuous deployment +// by installing docker, starting the daemon and building a +// 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) + + println(">> Step 1/4: Installing docker...") + err := c.installDocker(c.sshRunner) + if err != nil { + return err + } + + println("\n>> Step 2/4: Building deploy key...") + if err != nil { + return err + } + pub, err := c.keyGen(c.sshRunner) + if err != nil { + return err + } + + // 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...") + if err != nil { + return err + } + err = c.DaemonUp(c.version, c.IP, c.Daemon.Port) + if err != nil { + return err + } + + println("\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) + + println("=============================\n") + + // Output deploy key to user. + println(">> GitHub Deploy Key (add to https://www.github.com/" + repoName + "/settings/keys/new): ") + println(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.Secret) + println(`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") + + println(`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) + + return nil +} + +// DaemonUp brings the daemon up on the remote instance. +func (c *Client) DaemonUp(daemonVersion, host, daemonPort string) error { + scriptBytes, err := Asset("client/bootstrap/daemon-up.sh") + if err != nil { + return err + } + + // Run inertia daemon. + daemonCmdStr := fmt.Sprintf(string(scriptBytes), daemonVersion, daemonPort, host) + return c.sshRunner.RunStream(daemonCmdStr, false) +} + +// DaemonDown brings the daemon down on the remote instance +func (c *Client) DaemonDown() error { + scriptBytes, err := Asset("client/bootstrap/daemon-down.sh") + if err != nil { + return err + } + + _, stderr, err := c.sshRunner.Run(string(scriptBytes)) + if err != nil { + println(stderr.String()) + return err + } + + return nil +} + +// Up brings the project up on the remote VPS instance specified +// in the deployment object. +func (c *Client) Up(gitRemoteURL, buildType string, stream bool) (*http.Response, error) { + if buildType == "" { + buildType = c.buildType + } + + reqContent := &common.DaemonRequest{ + Stream: stream, + Project: c.project, + BuildType: buildType, + Secret: c.RemoteVPS.Daemon.Secret, + GitOptions: &common.GitOptions{ + RemoteURL: common.GetSSHRemoteURL(gitRemoteURL), + Branch: c.Branch, + }, + } + return c.post("/up", reqContent) +} + +// Down brings the project down on the remote VPS instance specified +// in the configuration object. +func (c *Client) Down() (*http.Response, error) { + return c.post("/down", nil) +} + +// Status lists the currently active containers on the remote VPS instance +func (c *Client) Status() (*http.Response, error) { + resp, err := c.get("/status", nil) + if err != nil && + (strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "refused")) { + return nil, fmt.Errorf("daemon on remote %s appears offline or inaccessible", c.Name) + } + return resp, err +} + +// Reset shuts down deployment and deletes the contents of the deployment's +// project directory +func (c *Client) Reset() (*http.Response, error) { + return c.post("/reset", nil) +} + +// Logs get logs of given container +func (c *Client) Logs(stream bool, container string) (*http.Response, error) { + reqContent := map[string]string{ + common.Stream: strconv.FormatBool(stream), + common.Container: container, + } + + return c.get("/logs", reqContent) +} + +// AddUser adds an authorized user for access to Inertia Web +func (c *Client) AddUser(username, password string, admin bool) (*http.Response, error) { + reqContent := &common.UserRequest{ + Username: username, + Password: password, + Admin: admin, + } + return c.post("/user/adduser", reqContent) +} + +// RemoveUser prevents a user from accessing Inertia Web +func (c *Client) RemoveUser(username string) (*http.Response, error) { + reqContent := &common.UserRequest{Username: username} + return c.post("/user/removeuser", reqContent) +} + +// ResetUsers resets all users on the remote. +func (c *Client) ResetUsers() (*http.Response, error) { + return c.post("/user/resetusers", nil) +} + +// ListUsers lists all users on the remote. +func (c *Client) ListUsers() (*http.Response, error) { + return c.get("/user/listusers", nil) +} + +// Sends a GET request. "queries" contains query string arguments. +func (c *Client) get(endpoint string, queries map[string]string) (*http.Response, error) { + // Assemble request + req, err := c.buildRequest("GET", endpoint, nil) + if err != nil { + return nil, err + } + + // Add query strings + if queries != nil { + q := req.URL.Query() + for k, v := range queries { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + } + + client := buildHTTPSClient() + return client.Do(req) +} + +func (c *Client) post(endpoint string, requestBody interface{}) (*http.Response, error) { + // Assemble payload + var payload io.Reader + if requestBody != nil { + body, err := json.Marshal(requestBody) + if err != nil { + return nil, err + } + payload = bytes.NewReader(body) + } else { + payload = nil + } + + // Assemble request + req, err := c.buildRequest("POST", endpoint, payload) + if err != nil { + return nil, err + } + + client := buildHTTPSClient() + return client.Do(req) +} + +func (c *Client) buildRequest(method string, endpoint string, payload io.Reader) (*http.Request, error) { + // Assemble URL + url, err := url.Parse("https://" + c.RemoteVPS.GetIPAndPort()) + if err != nil { + return nil, err + } + url.Path = path.Join(url.Path, endpoint) + urlString := url.String() + + // Assemble request + req, err := http.NewRequest(method, urlString, payload) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.Daemon.Token) + + return req, nil +} + +func buildHTTPSClient() *http.Client { + // Make HTTPS request + tr := &http.Transport{ + // Our certificates are self-signed, so will raise + // a warning - currently, we ask our client to ignore + // this warning. + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + return &http.Client{Transport: tr} +} diff --git a/client/deployment_test.go b/client/client_test.go similarity index 53% rename from client/deployment_test.go rename to client/client_test.go index 8cb4eacc..499b13d4 100644 --- a/client/deployment_test.go +++ b/client/client_test.go @@ -1,25 +1,26 @@ package client import ( + "crypto/tls" "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" + "os" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/ubclaunchpad/inertia/common" - git "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/config" - "gopkg.in/src-d/go-git.v4/storage/memory" ) var ( fakeAuth = "ubclaunchpad" ) -func getMockDeployment(ts *httptest.Server, s *memory.Storage) (*Deployment, error) { +func getMockClient(ts *httptest.Server) *Client { var ( url string port string @@ -40,28 +41,139 @@ func getMockDeployment(ts *httptest.Server, s *memory.Storage) (*Deployment, err Daemon: &DaemonConfig{ Port: port, Secret: "arjan", + Token: fakeAuth, }, } - mockRepo, err := git.Init(s, nil) - if err != nil { - return nil, err + + return &Client{ + RemoteVPS: mockRemote, + project: "test_project", + } +} + +func getIntegrationClient(mockRunner *mockSSHRunner) *Client { + remote := &RemoteVPS{ + IP: "127.0.0.1", + PEM: "../test/keys/id_rsa", + User: "root", + Daemon: &DaemonConfig{ + Port: "4303", + }, + } + travis := os.Getenv("TRAVIS") + if travis != "" { + remote.SSHPort = "69" + } else { + remote.SSHPort = "22" } - _, err = mockRepo.CreateRemote(&config.RemoteConfig{ - Name: "origin", - URLs: []string{"myremote"}, - }) - if err != nil { - return nil, err + if mockRunner != nil { + mockRunner.r = remote + return &Client{ + version: "test", + RemoteVPS: remote, + sshRunner: mockRunner, + } } + return &Client{ + version: "test", + RemoteVPS: remote, + sshRunner: NewSSHRunner(remote), + } +} + +func TestInstallDocker(t *testing.T) { + session := &mockSSHRunner{} + client := getIntegrationClient(session) + script, err := ioutil.ReadFile("bootstrap/docker.sh") + assert.Nil(t, err) + + // Make sure the right command is run. + client.installDocker(session) + assert.Equal(t, string(script), session.Calls[0]) +} + +func TestDaemonUp(t *testing.T) { + session := &mockSSHRunner{} + client := getIntegrationClient(session) + script, err := ioutil.ReadFile("bootstrap/daemon-up.sh") + assert.Nil(t, err) + actualCommand := fmt.Sprintf(string(script), "latest", "4303", "0.0.0.0") - return &Deployment{ - RemoteVPS: mockRemote, - Repository: mockRepo, - Auth: fakeAuth, - Project: "test_project", - }, nil + // Make sure the right command is run. + err = client.DaemonUp("latest", "0.0.0.0", "4303") + assert.Nil(t, err) + println(actualCommand) + assert.Equal(t, actualCommand, session.Calls[0]) +} + +func TestKeyGen(t *testing.T) { + session := &mockSSHRunner{} + remote := getIntegrationClient(session) + script, err := ioutil.ReadFile("bootstrap/token.sh") + assert.Nil(t, err) + tokenScript := fmt.Sprintf(string(script), "test") + + // Make sure the right command is run. + + // Make sure the right command is run. + _, err = remote.getDaemonAPIToken(session, "test") + assert.Nil(t, err) + assert.Equal(t, session.Calls[0], tokenScript) } +func TestBootstrap(t *testing.T) { + session := &mockSSHRunner{} + client := getIntegrationClient(session) + + dockerScript, err := ioutil.ReadFile("bootstrap/docker.sh") + assert.Nil(t, err) + + keyScript, err := ioutil.ReadFile("bootstrap/keygen.sh") + assert.Nil(t, err) + + script, err := ioutil.ReadFile("bootstrap/token.sh") + assert.Nil(t, err) + tokenScript := fmt.Sprintf(string(script), "test") + + script, err = ioutil.ReadFile("bootstrap/daemon-up.sh") + assert.Nil(t, err) + daemonScript := fmt.Sprintf(string(script), "test", "4303", "127.0.0.1") + + err = client.BootstrapRemote("ubclaunchpad/inertia") + assert.Nil(t, err) + + // Make sure all commands are formatted correctly + assert.Equal(t, string(dockerScript), session.Calls[0]) + assert.Equal(t, string(keyScript), session.Calls[1]) + assert.Equal(t, daemonScript, session.Calls[2]) + assert.Equal(t, tokenScript, session.Calls[3]) +} + +func TestBootstrapIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + cli := getIntegrationClient(nil) + err := cli.BootstrapRemote("") + assert.Nil(t, err) + + // Daemon setup takes a bit of time - do a crude wait + time.Sleep(3 * time.Second) + + // Check if daemon is online following bootstrap + host := "https://" + cli.GetIPAndPort() + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + resp, err := client.Get(host) + assert.Nil(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) + defer resp.Body.Close() + _, err = ioutil.ReadAll(resp.Body) + assert.Nil(t, err) +} func TestUp(t *testing.T) { testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) @@ -90,13 +202,8 @@ func TestUp(t *testing.T) { })) defer testServer.Close() - memory := memory.NewStorage() - defer func() { memory = nil }() - - d, err := getMockDeployment(testServer, memory) - assert.Nil(t, err) - - resp, err := d.Up("docker-compose", false) + d := getMockClient(testServer) + resp, err := d.Up("myremote.git", "docker-compose", false) assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) } @@ -117,12 +224,7 @@ func TestDown(t *testing.T) { })) defer testServer.Close() - memory := memory.NewStorage() - defer func() { memory = nil }() - - d, err := getMockDeployment(testServer, memory) - assert.Nil(t, err) - + d := getMockClient(testServer) resp, err := d.Down() assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -144,25 +246,15 @@ func TestStatus(t *testing.T) { })) defer testServer.Close() - memory := memory.NewStorage() - defer func() { memory = nil }() - - d, err := getMockDeployment(testServer, memory) - assert.Nil(t, err) - + d := getMockClient(testServer) resp, err := d.Status() assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) } func TestStatusFail(t *testing.T) { - memory := memory.NewStorage() - defer func() { memory = nil }() - - d, err := getMockDeployment(nil, memory) - assert.Nil(t, err) - - _, err = d.Status() + d := getMockClient(nil) + _, err := d.Status() assert.Contains(t, err.Error(), "appears offline") } @@ -182,12 +274,7 @@ func TestReset(t *testing.T) { })) defer testServer.Close() - memory := memory.NewStorage() - defer func() { memory = nil }() - - d, err := getMockDeployment(testServer, memory) - assert.Nil(t, err) - + d := getMockClient(testServer) resp, err := d.Reset() assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) @@ -215,12 +302,7 @@ func TestLogs(t *testing.T) { })) defer testServer.Close() - memory := memory.NewStorage() - defer func() { memory = nil }() - - d, err := getMockDeployment(testServer, memory) - assert.Nil(t, err) - + d := getMockClient(testServer) resp, err := d.Logs(true, "docker-compose") assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) diff --git a/client/config.go b/client/config.go index 577564b9..a0f5e6e7 100644 --- a/client/config.go +++ b/client/config.go @@ -5,43 +5,71 @@ import ( "io" "io/ioutil" "os" - "path/filepath" - "reflect" "github.com/BurntSushi/toml" - "github.com/ubclaunchpad/inertia/common" ) var ( // NoInertiaRemote is used to warn about missing inertia remote NoInertiaRemote = "No inertia remote" - configFileName = ".inertia.toml" ) // Config represents the current projects configuration. type Config struct { - Version string `toml:"inertia"` + Version string `toml:"version"` Project string `toml:"project-name"` BuildType string `toml:"build-type"` Remotes []*RemoteVPS `toml:"remote"` - Writer io.Writer `toml:"-"` } -// Write writes configuration to Inertia config file. -func (config *Config) Write() error { - if config.Writer == nil { - return nil +// NewConfig sets up Inertia configuration with given properties +func NewConfig(version, project, buildType string) *Config { + return &Config{ + Version: version, + Project: project, + BuildType: buildType, + Remotes: make([]*RemoteVPS, 0), } - path, err := GetConfigFilePath() - if err != nil { - return err +} + +// Write writes configuration to Inertia config file at path. Optionally +// takes io.Writers. +func (config *Config) Write(filePath string, writers ...io.Writer) error { + if len(writers) == 0 && filePath == "" { + return errors.New("nothing to write to") + } + + var writer io.Writer + + // If io.Writers are given, attach all writers + if len(writers) > 0 { + writer = io.MultiWriter(writers...) } - // Overwrite file if file exists - if _, err := os.Stat(path); !os.IsNotExist(err) { - ioutil.WriteFile(path, []byte(""), 0644) + + // If path is given, attach file writer + if filePath != "" { + w, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, os.ModePerm) + if err != nil { + return err + } + + // Overwrite file if file exists + if _, err := os.Stat(filePath); !os.IsNotExist(err) { + ioutil.WriteFile(filePath, []byte(""), 0644) + } else if err != nil { + return err + } + + // Set writer + if writer != nil { + writer = io.MultiWriter(writer, w) + } else { + writer = w + } } - // Write configuration to file - encoder := toml.NewEncoder(config.Writer) + + // Write configuration to writers + encoder := toml.NewEncoder(writer) return encoder.Encode(config) } @@ -71,129 +99,3 @@ func (config *Config) RemoveRemote(name string) bool { } return false } - -// InitializeInertiaProject creates the inertia config folder and -// returns an error if we're not in a git project. -func InitializeInertiaProject(version, buildType string) error { - cwd, err := os.Getwd() - if err != nil { - return err - } - err = common.CheckForGit(cwd) - if err != nil { - return err - } - - return createConfigFile(version, buildType) -} - -// createConfigFile returns an error if the config directory -// already exists (the project is already initialized). -func createConfigFile(version, buildType string) error { - configFilePath, err := GetConfigFilePath() - if err != nil { - return err - } - - // Check if Inertia is already set up. - s, fileErr := os.Stat(configFilePath) - if s != nil { - return errors.New("inertia already properly configured in this folder") - } - - cwd, err := os.Getwd() - if err != nil { - return err - } - - // Directory exists. Make sure configuration file exists. - if os.IsNotExist(fileErr) { - config := Config{ - Project: filepath.Base(cwd), - Version: version, - BuildType: buildType, - Remotes: make([]*RemoteVPS, 0), - } - - path, err := GetConfigFilePath() - if err != nil { - return err - } - f, err := os.Create(path) - if err != nil { - return err - } - writer, err := os.OpenFile(configFilePath, os.O_WRONLY, os.ModePerm) - if err != nil { - return err - } - config.Writer = writer - defer f.Close() - config.Write() - } - - return nil -} - -// GetProjectConfigFromDisk returns the current project's configuration. -// If an .inertia folder is not found, it returns an error. -func GetProjectConfigFromDisk() (*Config, error) { - configFilePath, err := GetConfigFilePath() - if err != nil { - return nil, err - } - - raw, err := ioutil.ReadFile(configFilePath) - - if err != nil { - if os.IsNotExist(err) { - return nil, errors.New("config file doesnt exist, try inertia init") - } - return nil, err - } - - var result Config - err = toml.Unmarshal(raw, &result) - if err != nil { - return nil, err - } - - // Add writer to object for writing/testing. - result.Writer, err = os.OpenFile(configFilePath, os.O_WRONLY, os.ModePerm) - if err != nil { - return nil, err - } - - return &result, err -} - -// GetConfigFilePath returns the absolute path of the config file. -func GetConfigFilePath() (string, error) { - path, err := os.Getwd() - if err != nil { - return "", err - } - return filepath.Join(path, configFileName), nil -} - -// SetProperty takes a struct pointer and searches for its "toml" tag with a search key -// and set property value with the tag -func SetProperty(name string, value string, structObject interface{}) bool { - val := reflect.ValueOf(structObject) - - if val.Kind() != reflect.Ptr { - return false - } - structVal := val.Elem() - for i := 0; i < structVal.NumField(); i++ { - valueField := structVal.Field(i) - typeField := structVal.Type().Field(i) - if typeField.Tag.Get("toml") == name { - if valueField.IsValid() && valueField.CanSet() && valueField.Kind() == reflect.String { - valueField.SetString(value) - return true - } - } - } - return false -} diff --git a/client/config_test.go b/client/config_test.go index 2578f23c..e4759e92 100644 --- a/client/config_test.go +++ b/client/config_test.go @@ -1,62 +1,78 @@ package client import ( + "bytes" + "io/ioutil" "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" ) -func TestConfigCreateAndWriteAndRead(t *testing.T) { - err := createConfigFile("", "") +func TestNewConfig(t *testing.T) { + cfg := NewConfig("test", "best-project", "docker-compose") + assert.Equal(t, cfg.Version, "test") +} + +func TestWriteFailed(t *testing.T) { + cfg := NewConfig("test", "best-project", "docker-compose") + err := cfg.Write("") + assert.NotNil(t, err) + assert.Contains(t, "nothing to write to", err.Error()) +} + +func TestWriteToPath(t *testing.T) { + configPath := "/test-config.toml" + cfg := NewConfig("test", "best-project", "docker-compose") + + cwd, err := os.Getwd() assert.Nil(t, err) - config, err := GetProjectConfigFromDisk() + absPath := filepath.Join(cwd, configPath) + defer os.RemoveAll(absPath) + + err = cfg.Write(absPath) assert.Nil(t, err) - config.AddRemote(&RemoteVPS{ - Name: "test", - IP: "1234", - User: "bobheadxi", - PEM: "/some/pem/file", - Daemon: &DaemonConfig{ - Port: "8080", - SSHPort: "22", - }, - }) - config.AddRemote(&RemoteVPS{ - Name: "test2", - IP: "12343", - User: "bobheadxi234", - PEM: "/some/pem/file234", - Daemon: &DaemonConfig{ - Port: "80801", - SSHPort: "222", - }, - }) - err = config.Write() + + writtenConfigContents, err := ioutil.ReadFile(absPath) assert.Nil(t, err) + assert.Contains(t, string(writtenConfigContents), "best-project") + assert.Contains(t, string(writtenConfigContents), "docker-compose") +} - readConfig, err := GetProjectConfigFromDisk() +func TestWriteToWritersAndFile(t *testing.T) { + configPath := "/test-config.toml" + cfg := NewConfig("test", "best-project", "docker-compose") + + cwd, err := os.Getwd() assert.Nil(t, err) - assert.Equal(t, config.Remotes[0], readConfig.Remotes[0]) - assert.Equal(t, config.Remotes[1], readConfig.Remotes[1]) + absPath := filepath.Join(cwd, configPath) + defer os.RemoveAll(absPath) + + buffer1 := bytes.NewBuffer(nil) + buffer2 := bytes.NewBuffer(nil) - path, err := GetConfigFilePath() + err = cfg.Write(absPath, buffer1, buffer2) assert.Nil(t, err) - println(path) - err = os.Remove(path) + + writtenConfigContents, err := ioutil.ReadFile(absPath) assert.Nil(t, err) + assert.Contains(t, string(writtenConfigContents), "best-project") + assert.Contains(t, string(writtenConfigContents), "docker-compose") + assert.Contains(t, buffer1.String(), "best-project") + assert.Contains(t, buffer2.String(), "best-project") } func TestConfigGetRemote(t *testing.T) { config := &Config{Remotes: make([]*RemoteVPS, 0)} testRemote := &RemoteVPS{ - Name: "test", - IP: "12343", - User: "bobheadxi", - PEM: "/some/pem/file", + Name: "test", + IP: "12343", + User: "bobheadxi", + PEM: "/some/pem/file", + SSHPort: "22", Daemon: &DaemonConfig{ - Port: "8080", - SSHPort: "22", + Port: "8080", }, } config.AddRemote(testRemote) @@ -68,27 +84,27 @@ func TestConfigGetRemote(t *testing.T) { assert.False(t, found) } -func TestConfigRemoteRemote(t *testing.T) { +func TestConfigRemoveRemote(t *testing.T) { config := &Config{Remotes: make([]*RemoteVPS, 0)} testRemote := &RemoteVPS{ - Name: "test", - IP: "12343", - User: "bobheadxi", - PEM: "/some/pem/file", + Name: "test", + IP: "12343", + User: "bobheadxi", + PEM: "/some/pem/file", + SSHPort: "22", Daemon: &DaemonConfig{ - Port: "8080", - SSHPort: "22", + Port: "8080", }, } config.AddRemote(testRemote) config.AddRemote(&RemoteVPS{ - Name: "test2", - IP: "12343", - User: "bobheadxi234", - PEM: "/some/pem/file234", + Name: "test2", + IP: "12343", + User: "bobheadxi234", + PEM: "/some/pem/file234", + SSHPort: "222", Daemon: &DaemonConfig{ - Port: "80801", - SSHPort: "222", + Port: "80801", }, }) removed := config.RemoveRemote("test2") @@ -100,30 +116,3 @@ func TestConfigRemoteRemote(t *testing.T) { assert.True(t, found) assert.Equal(t, testRemote, remote) } - -func TestSetProperty(t *testing.T) { - - testDaemonConfig := &DaemonConfig{ - Port: "8080", - SSHPort: "22", - } - - testRemote := &RemoteVPS{ - Name: "testName", - IP: "1234", - User: "testUser", - PEM: "/some/pem/file", - Daemon: testDaemonConfig, - } - a := SetProperty("name", "newTestName", testRemote) - assert.True(t, a) - assert.Equal(t, "newTestName", testRemote.Name) - - b := SetProperty("wrongtag", "otherTestName", testRemote) - assert.False(t, b) - assert.Equal(t, "newTestName", testRemote.Name) - - c := SetProperty("port", "8000", testDaemonConfig) - assert.True(t, c) - assert.Equal(t, "8000", testDaemonConfig.Port) -} diff --git a/client/deployment.go b/client/deployment.go deleted file mode 100644 index 0cf2fa09..00000000 --- a/client/deployment.go +++ /dev/null @@ -1,216 +0,0 @@ -package client - -import ( - "bytes" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "path" - "strconv" - "strings" - - "github.com/ubclaunchpad/inertia/common" - git "gopkg.in/src-d/go-git.v4" -) - -// Deployment manages a deployment -type Deployment struct { - *RemoteVPS - Repository *git.Repository - Auth string - Project string - BuildType string -} - -// GetDeployment returns the local deployment setup -func GetDeployment(name string) (*Deployment, error) { - config, err := GetProjectConfigFromDisk() - if err != nil { - return nil, err - } - - repo, err := common.GetLocalRepo() - if err != nil { - return nil, err - } - - remote, found := config.GetRemote(name) - if !found { - return nil, errors.New("Remote not found") - } - auth := remote.Daemon.Token - - return &Deployment{ - RemoteVPS: remote, - Repository: repo, - Auth: auth, - BuildType: config.BuildType, - Project: config.Project, - }, nil -} - -// Up brings the project up on the remote VPS instance specified -// in the deployment object. -func (d *Deployment) Up(buildType string, stream bool) (*http.Response, error) { - // TODO: Support other Git remotes. - origin, err := d.Repository.Remote("origin") - if err != nil { - return nil, err - } - - if buildType == "" { - buildType = d.BuildType - } - - reqContent := &common.DaemonRequest{ - Stream: stream, - Project: d.Project, - BuildType: buildType, - Secret: d.RemoteVPS.Daemon.Secret, - GitOptions: &common.GitOptions{ - RemoteURL: common.GetSSHRemoteURL(origin.Config().URLs[0]), - Branch: d.Branch, - }, - } - return d.post("/up", reqContent) -} - -// Down brings the project down on the remote VPS instance specified -// in the configuration object. -func (d *Deployment) Down() (*http.Response, error) { - return d.post("/down", nil) -} - -// Status lists the currently active containers on the remote VPS instance -func (d *Deployment) Status() (*http.Response, error) { - resp, err := d.get("/status", nil) - if err != nil && - (strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "refused")) { - return nil, fmt.Errorf("daemon on remote %s appears offline or inaccessible", d.Name) - } - return resp, err -} - -// Reset shuts down deployment and deletes the contents of the deployment's -// project directory -func (d *Deployment) Reset() (*http.Response, error) { - return d.post("/reset", nil) -} - -// Logs get logs of given container -func (d *Deployment) Logs(stream bool, container string) (*http.Response, error) { - reqContent := map[string]string{ - common.Stream: strconv.FormatBool(stream), - common.Container: container, - } - - return d.get("/logs", reqContent) -} - -// AddUser adds an authorized user for access to Inertia Web -func (d *Deployment) AddUser(username, password string, admin bool) (*http.Response, error) { - reqContent := &common.UserRequest{ - Username: username, - Password: password, - Admin: admin, - } - return d.post("/user/adduser", reqContent) -} - -// RemoveUser prevents a user from accessing Inertia Web -func (d *Deployment) RemoveUser(username string) (*http.Response, error) { - reqContent := &common.UserRequest{Username: username} - return d.post("/user/removeuser", reqContent) -} - -// ResetUsers resets all users on the remote. -func (d *Deployment) ResetUsers() (*http.Response, error) { - return d.post("/user/resetusers", nil) -} - -// ListUsers lists all users on the remote. -func (d *Deployment) ListUsers() (*http.Response, error) { - return d.get("/user/listusers", nil) -} - -// Sends a GET request. "queries" contains query string arguments. -func (d *Deployment) get(endpoint string, queries map[string]string) (*http.Response, error) { - // Assemble request - req, err := d.buildRequest("GET", endpoint, nil) - if err != nil { - return nil, err - } - - // Add query strings - if queries != nil { - q := req.URL.Query() - for k, v := range queries { - q.Add(k, v) - } - req.URL.RawQuery = q.Encode() - } - - client := buildHTTPSClient() - return client.Do(req) -} - -func (d *Deployment) post(endpoint string, requestBody interface{}) (*http.Response, error) { - // Assemble payload - var payload io.Reader - if requestBody != nil { - body, err := json.Marshal(requestBody) - if err != nil { - return nil, err - } - payload = bytes.NewReader(body) - } else { - payload = nil - } - - // Assemble request - req, err := d.buildRequest("POST", endpoint, payload) - if err != nil { - return nil, err - } - - client := buildHTTPSClient() - return client.Do(req) -} - -func (d *Deployment) buildRequest(method string, endpoint string, payload io.Reader) (*http.Request, error) { - // Assemble URL - url, err := url.Parse("https://" + d.RemoteVPS.GetIPAndPort()) - if err != nil { - return nil, err - } - url.Path = path.Join(url.Path, endpoint) - urlString := url.String() - - // Assemble request - req, err := http.NewRequest(method, urlString, payload) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+d.Auth) - - return req, nil -} - -func buildHTTPSClient() *http.Client { - // Make HTTPS request - tr := &http.Transport{ - // Our certificates are self-signed, so will raise - // a warning - currently, we ask our client to ignore - // this warning. - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - } - return &http.Client{Transport: tr} -} diff --git a/client/remote.go b/client/remote.go index 3d7e4f50..81672726 100644 --- a/client/remote.go +++ b/client/remote.go @@ -10,20 +10,20 @@ import ( // RemoteVPS contains parameters for the VPS type RemoteVPS struct { - Name string `toml:"name"` - IP string `toml:"IP"` - User string `toml:"user"` - PEM string `toml:"pemfile"` - Branch string `toml:"branch"` - Daemon *DaemonConfig `toml:"daemon"` + Name string `toml:"name"` + IP string `toml:"IP"` + User string `toml:"user"` + PEM string `toml:"pemfile"` + Branch string `toml:"branch"` + SSHPort string `toml:"ssh_port"` + Daemon *DaemonConfig `toml:"daemon"` } // DaemonConfig contains parameters for the Daemon type DaemonConfig struct { - Port string `toml:"port"` - SSHPort string `toml:"ssh_port"` - Token string `toml:"token"` - Secret string `toml:"secret"` + Port string `toml:"port"` + Token string `toml:"token"` + Secret string `toml:"secret"` } // GetHost creates the user@IP string. @@ -36,103 +36,6 @@ func (remote *RemoteVPS) GetIPAndPort() string { return remote.IP + ":" + remote.Daemon.Port } -// Bootstrap configures a remote vps for continuous deployment -// by installing docker, starting the daemon and building a -// public-private key-pair. It outputs configuration information -// for the user. -func (remote *RemoteVPS) Bootstrap(runner SSHSession, repoName string, config *Config) error { - println("Setting up remote \"" + remote.Name + "\" at " + remote.IP) - - println(">> Step 1/4: Installing docker...") - err := remote.installDocker(runner) - if err != nil { - return err - } - - println("\n>> Step 2/4: Building deploy key...") - if err != nil { - return err - } - pub, err := remote.keyGen(runner) - if err != nil { - return err - } - - // 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...") - if err != nil { - return err - } - err = remote.DaemonUp(runner, config.Version, remote.IP, remote.Daemon.Port) - if err != nil { - return err - } - - println("\n>> Step 4/4: Fetching daemon API token...") - token, err := remote.getDaemonAPIToken(runner, config.Version) - if err != nil { - return err - } - remote.Daemon.Token = token - err = config.Write() - if err != nil { - return err - } - - 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", remote.Name) - - println("=============================\n") - - // Output deploy key to user. - println(">> GitHub Deploy Key (add to https://www.github.com/" + repoName + "/settings/keys/new): ") - println(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://" + remote.IP + ":" + remote.Daemon.Port + "/webhook") - println("WebHook Secret: " + remote.Daemon.Secret) - println(`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") - - println(`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", remote.Name) - - return nil -} - -// DaemonUp brings the daemon up on the remote instance. -func (remote *RemoteVPS) DaemonUp(session SSHSession, daemonVersion, host, daemonPort string) error { - scriptBytes, err := Asset("client/bootstrap/daemon-up.sh") - if err != nil { - return err - } - - // Run inertia daemon. - daemonCmdStr := fmt.Sprintf(string(scriptBytes), daemonVersion, daemonPort, host) - return session.RunStream(daemonCmdStr, false) -} - -// DaemonDown brings the daemon down on the remote instance -func (remote *RemoteVPS) DaemonDown(session SSHSession) error { - scriptBytes, err := Asset("client/bootstrap/daemon-down.sh") - if err != nil { - return err - } - - _, stderr, err := session.Run(string(scriptBytes)) - if err != nil { - println(stderr.String()) - return err - } - - return nil -} - // installDocker installs docker on a remote vps. func (remote *RemoteVPS) installDocker(session SSHSession) error { installDockerSh, err := Asset("client/bootstrap/docker.sh") diff --git a/client/remote_test.go b/client/remote_test.go deleted file mode 100644 index 58dc88e8..00000000 --- a/client/remote_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package client - -import ( - "bytes" - "crypto/tls" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func getTestConfig(writer io.Writer) *Config { - config := &Config{ - Writer: writer, - Version: "test", - } - return config -} - -func getTestRemote() *RemoteVPS { - remote := &RemoteVPS{ - Name: "gcloud", - IP: "127.0.0.1", - PEM: "../test/keys/id_rsa", - User: "root", - Daemon: &DaemonConfig{ - Port: "4303", - }, - } - travis := os.Getenv("TRAVIS") - if travis != "" { - remote.Daemon.SSHPort = "69" - } else { - remote.Daemon.SSHPort = "22" - } - return remote -} - -func TestInstallDocker(t *testing.T) { - remote := getTestRemote() - script, err := ioutil.ReadFile("bootstrap/docker.sh") - assert.Nil(t, err) - - // Make sure the right command is run. - session := mockSSHRunner{r: remote} - remote.installDocker(&session) - assert.Equal(t, string(script), session.Calls[0]) -} - -func TestDaemonUp(t *testing.T) { - remote := getTestRemote() - script, err := ioutil.ReadFile("bootstrap/daemon-up.sh") - assert.Nil(t, err) - actualCommand := fmt.Sprintf(string(script), "latest", "4303", "0.0.0.0") - - // Make sure the right command is run. - session := mockSSHRunner{r: remote} - - // Make sure the right command is run. - err = remote.DaemonUp(&session, "latest", "0.0.0.0", "4303") - assert.Nil(t, err) - println(actualCommand) - assert.Equal(t, actualCommand, session.Calls[0]) -} - -func TestKeyGen(t *testing.T) { - remote := getTestRemote() - script, err := ioutil.ReadFile("bootstrap/token.sh") - assert.Nil(t, err) - tokenScript := fmt.Sprintf(string(script), "test") - - // Make sure the right command is run. - session := mockSSHRunner{r: remote} - - // Make sure the right command is run. - _, err = remote.getDaemonAPIToken(&session, "test") - assert.Nil(t, err) - assert.Equal(t, session.Calls[0], tokenScript) -} - -func TestBootstrap(t *testing.T) { - remote := getTestRemote() - dockerScript, err := ioutil.ReadFile("bootstrap/docker.sh") - assert.Nil(t, err) - - keyScript, err := ioutil.ReadFile("bootstrap/keygen.sh") - assert.Nil(t, err) - - script, err := ioutil.ReadFile("bootstrap/token.sh") - assert.Nil(t, err) - tokenScript := fmt.Sprintf(string(script), "test") - - script, err = ioutil.ReadFile("bootstrap/daemon-up.sh") - assert.Nil(t, err) - daemonScript := fmt.Sprintf(string(script), "test", "4303", "127.0.0.1") - - var writer bytes.Buffer - session := mockSSHRunner{r: remote} - err = remote.Bootstrap(&session, "ubclaunchpad/inertia", getTestConfig(&writer)) - assert.Nil(t, err) - - // Make sure all commands are formatted correctly - assert.Equal(t, string(dockerScript), session.Calls[0]) - assert.Equal(t, string(keyScript), session.Calls[1]) - assert.Equal(t, daemonScript, session.Calls[2]) - assert.Equal(t, tokenScript, session.Calls[3]) -} - -func TestBootstrapIntegration(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - remote := getTestRemote() - session := &SSHRunner{r: remote} - var writer bytes.Buffer - err := remote.Bootstrap(session, "ubclaunchpad/inertia", getTestConfig(&writer)) - assert.Nil(t, err) - - // Daemon setup takes a bit of time - do a crude wait - time.Sleep(3 * time.Second) - - // Check if daemon is online following bootstrap - host := "https://" + remote.GetIPAndPort() - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - client := &http.Client{Transport: tr} - resp, err := client.Get(host) - assert.Nil(t, err) - assert.Equal(t, resp.StatusCode, http.StatusOK) - defer resp.Body.Close() - _, err = ioutil.ReadAll(resp.Body) - assert.Nil(t, err) -} diff --git a/client/ssh.go b/client/ssh.go index 7708711e..f4c21584 100644 --- a/client/ssh.go +++ b/client/ssh.go @@ -19,17 +19,28 @@ type SSHSession interface { // SSHRunner runs commands over SSH and captures results. type SSHRunner struct { - r *RemoteVPS + pem string + user string + ip string + sshPort string } // NewSSHRunner returns a new SSHRunner func NewSSHRunner(r *RemoteVPS) *SSHRunner { - return &SSHRunner{r: r} + if r != nil { + return &SSHRunner{ + pem: r.PEM, + user: r.User, + ip: r.IP, + sshPort: r.SSHPort, + } + } + return &SSHRunner{} } // Run runs a command remotely. func (runner *SSHRunner) Run(cmd string) (*bytes.Buffer, *bytes.Buffer, error) { - session, err := getSSHSession(runner.r.PEM, runner.r.IP, runner.r.Daemon.SSHPort, runner.r.User) + session, err := getSSHSession(runner.pem, runner.ip, runner.sshPort, runner.user) if err != nil { return nil, nil, err } @@ -47,7 +58,7 @@ func (runner *SSHRunner) Run(cmd string) (*bytes.Buffer, *bytes.Buffer, error) { // RunStream remotely executes given command, streaming its output // and opening up an optionally interactive session func (runner *SSHRunner) RunStream(cmd string, interactive bool) error { - session, err := getSSHSession(runner.r.PEM, runner.r.IP, runner.r.Daemon.SSHPort, runner.r.User) + session, err := getSSHSession(runner.pem, runner.ip, runner.sshPort, runner.user) if err != nil { return err } @@ -65,7 +76,7 @@ func (runner *SSHRunner) RunStream(cmd string, interactive bool) error { // RunSession sets up a SSH shell to the remote func (runner *SSHRunner) RunSession() error { - session, err := getSSHSession(runner.r.PEM, runner.r.IP, runner.r.Daemon.SSHPort, runner.r.User) + session, err := getSSHSession(runner.pem, runner.ip, runner.sshPort, runner.user) if err != nil { return err } diff --git a/client/ssh_test.go b/client/ssh_test.go index 4bfb4500..876ec137 100644 --- a/client/ssh_test.go +++ b/client/ssh_test.go @@ -2,9 +2,6 @@ package client import ( "bytes" - "testing" - - "github.com/stretchr/testify/assert" ) // mockSSHRunner is a mocked out implementation of SSHSession @@ -26,23 +23,3 @@ func (runner *mockSSHRunner) RunStream(cmd string, interactive bool) error { func (runner *mockSSHRunner) RunSession() error { return nil } - -func TestRun(t *testing.T) { - remote := getTestRemote() - session := mockSSHRunner{r: remote} - cmd := "ls -lsa" - - _, _, err := session.Run(cmd) - assert.Nil(t, err) - assert.Equal(t, cmd, session.Calls[0]) -} - -func TestRunInteractive(t *testing.T) { - remote := getTestRemote() - session := mockSSHRunner{r: remote} - cmd := "ls -lsa" - - err := session.RunStream(cmd, true) - assert.Nil(t, err) - assert.Equal(t, cmd, session.Calls[0]) -} diff --git a/daemon/inertia/project/build_test.go b/daemon/inertia/project/build_test.go index e3e23f75..12648f37 100644 --- a/daemon/inertia/project/build_test.go +++ b/daemon/inertia/project/build_test.go @@ -98,7 +98,7 @@ func TestDockerComposeIntegration(t *testing.T) { // try again if project no up (workaround for Travis) if !foundP { - time.Sleep(10 * time.Second) + time.Sleep(20 * time.Second) containers, err = cli.ContainerList( context.Background(), types.ContainerListOptions{}, @@ -140,7 +140,7 @@ func TestDockerComposeIntegration(t *testing.T) { // try again if project no up (workaround for Travis) if !foundP { - time.Sleep(10 * time.Second) + time.Sleep(20 * time.Second) containers, err = cli.ContainerList( context.Background(), types.ContainerListOptions{}, diff --git a/daemon/web/common/encodeURL.js b/daemon/web/common/encodeURL.js index e958033d..0d62eaf0 100644 --- a/daemon/web/common/encodeURL.js +++ b/daemon/web/common/encodeURL.js @@ -13,7 +13,7 @@ function encodeURL(params) { const result = []; const keys = Object.keys(params); - for (let k of keys) { + for (const k of keys) { const v = params[k]; if (typeof v !== 'string') { @@ -23,7 +23,7 @@ function encodeURL(params) { result.push(encodeURIComponent(k) + '=' + encodeURIComponent(v)); } - return result.join('&') + return result.join('&'); } -module.exports = encodeURL; \ No newline at end of file +module.exports = encodeURL; diff --git a/deploy.go b/deploy.go index 5b1581aa..a836502f 100644 --- a/deploy.go +++ b/deploy.go @@ -24,12 +24,13 @@ var deploymentUpCmd = &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 := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) } - stream, err := cmd.Flags().GetBool("stream") + // Get flags + stream, err := cmd.Flags().GetBool("stream") if err != nil { log.Fatal(err) } @@ -37,7 +38,17 @@ var deploymentUpCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - resp, err := deployment.Up(buildType, stream) + + repo, err := common.GetLocalRepo() + if err != nil { + log.Fatal(err) + } + // TODO: support other remotes + origin, err := repo.Remote("origin") + if err != nil { + log.Fatal(err) + } + resp, err := deployment.Up(origin.Config().URLs[0], buildType, stream) if err != nil { log.Fatal(err) } @@ -80,7 +91,7 @@ var deploymentDownCmd = &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 := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) } @@ -117,7 +128,7 @@ var deploymentStatusCmd = &cobra.Command{ running 'inertia [REMOTE] up'`, Run: func(cmd *cobra.Command, args []string) { remoteName := strings.Split(cmd.Parent().Use, " ")[0] - deployment, err := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) } @@ -164,7 +175,7 @@ var deploymentLogsCmd = &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 := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) } @@ -219,7 +230,7 @@ var deploymentSSHCmd = &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 := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) } @@ -244,14 +255,12 @@ for updates to this repository's remote master branch.`, remoteName := strings.Split(cmd.Parent().Use, " ")[0] // Bootstrap needs to write to configuration. - config, err := client.GetProjectConfigFromDisk() + config, path, err := getProjectConfigFromDisk() if err != nil { log.Fatal(err) } - remote, found := config.GetRemote(remoteName) + cli, found := client.NewClient(remoteName, config) if found { - session := client.NewSSHRunner(remote) - repo, err := common.GetLocalRepo() if err != nil { log.Fatal(err) @@ -265,10 +274,11 @@ for updates to this repository's remote master branch.`, log.Println(err) } - err = remote.Bootstrap(session, repoName, config) + 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?")) } @@ -285,7 +295,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 := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) } @@ -312,7 +322,7 @@ running 'inertia [REMOTE] init'`, } func init() { - config, err := client.GetProjectConfigFromDisk() + config, _, err := getProjectConfigFromDisk() if err != nil { return } diff --git a/format.go b/format.go index bcf46a02..7246e05d 100644 --- a/format.go +++ b/format.go @@ -1,6 +1,9 @@ package main import ( + "fmt" + + "github.com/ubclaunchpad/inertia/client" "github.com/ubclaunchpad/inertia/common" ) @@ -42,3 +45,13 @@ func formatStatus(s *common.DeploymentStatus) string { statusString += activeContainers return statusString } + +func formatRemoteDetails(remote *client.RemoteVPS) string { + remoteString := fmt.Sprintf("Remote %s: \n", remote.Name) + remoteString += fmt.Sprintf(" - Deployed Branch: %s\n", remote.Branch) + remoteString += fmt.Sprintf(" - IP Address: %s\n", remote.IP) + remoteString += fmt.Sprintf(" - VPS User: %s\n", remote.User) + remoteString += fmt.Sprintf(" - PEM File Location: %s\n", remote.PEM) + remoteString += fmt.Sprintf("Run 'inertia %s status' for more details.\n", remote.Name) + return remoteString +} diff --git a/format_test.go b/format_test.go index 871927da..42acbffa 100644 --- a/format_test.go +++ b/format_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/ubclaunchpad/inertia/client" "github.com/ubclaunchpad/inertia/common" ) @@ -45,3 +46,17 @@ func TestFormatStatusNoDeployment(t *testing.T) { assert.Contains(t, output, "inertia daemon 9000") assert.Contains(t, output, msgNoDeployment) } + +func TestFormatRemoteDetails(t *testing.T) { + client := &client.RemoteVPS{ + Name: "bob", + Branch: "great", + User: "tree", + PEM: "/wow/amaze", + } + output := formatRemoteDetails(client) + assert.Contains(t, output, "bob") + assert.Contains(t, output, "great") + assert.Contains(t, output, "tree") + assert.Contains(t, output, "/wow/amaze") +} diff --git a/init.go b/init.go index 4f1c8af7..8865a2fc 100644 --- a/init.go +++ b/init.go @@ -7,7 +7,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/ubclaunchpad/inertia/client" "github.com/ubclaunchpad/inertia/common" ) @@ -44,7 +43,7 @@ to succeed.`, } // Hello world config file! - err = client.InitializeInertiaProject(version, buildType) + err = initializeInertiaProject(version, buildType) if err != nil { log.Fatal(err) } @@ -73,7 +72,7 @@ var resetCmd = &cobra.Command{ if response != "y" { log.Fatal("aborting") } - path, err := client.GetConfigFilePath() + path, err := getConfigFilePath() if err != nil { log.Fatal(err) } @@ -81,6 +80,7 @@ var resetCmd = &cobra.Command{ println("Inertia configuration removed.") }, } + var setConfigCmd = &cobra.Command{ Use: "set [PROPERTY] [VALUE]", Short: "Set configuration property of the project", @@ -88,17 +88,17 @@ var setConfigCmd = &cobra.Command{ Args: cobra.MinimumNArgs(2), Run: func(cmd *cobra.Command, args []string) { // Ensure project initialized. - config, err := client.GetProjectConfigFromDisk() + config, path, err := getProjectConfigFromDisk() if err != nil { log.Fatal(err) } - success := client.SetProperty(args[0], args[1], config) + success := setProperty(args[0], args[1], config) if success { + config.Write(path) println("Configuration setting '" + args[0] + "' has been updated..") } else { println("Configuration setting '" + args[0] + "' not found.") } - }, } diff --git a/input.go b/input.go index 531abd2a..853b1b6c 100644 --- a/input.go +++ b/input.go @@ -18,8 +18,11 @@ var ( ) // addRemoteWalkthough is the command line walkthrough that asks -// users for RemoteVPS details -func addRemoteWalkthrough(in io.Reader, name, port, sshPort, currBranch string, config *client.Config) error { +// users for RemoteVPS details. It is up to the caller to save config. +func addRemoteWalkthrough( + in io.Reader, config *client.Config, + name, port, sshPort, currBranch string, +) error { homeEnvVar := os.Getenv("HOME") sshDir := filepath.Join(homeEnvVar, ".ssh") defaultSSHLoc := filepath.Join(sshDir, "id_rsa") @@ -69,16 +72,16 @@ func addRemoteWalkthrough(in io.Reader, name, port, sshPort, currBranch string, fmt.Println("of the -ssh flag to set a custom SSH port.") config.AddRemote(&client.RemoteVPS{ - Name: name, - IP: address, - User: user, - PEM: pemLoc, - Branch: branch, + Name: name, + IP: address, + User: user, + PEM: pemLoc, + Branch: branch, + SSHPort: sshPort, Daemon: &client.DaemonConfig{ - Port: port, - SSHPort: sshPort, - Secret: secret, + Port: port, + Secret: secret, }, }) - return config.Write() + return nil } diff --git a/input_test.go b/input_test.go index ec91ea63..fea09ba6 100644 --- a/input_test.go +++ b/input_test.go @@ -11,7 +11,7 @@ import ( ) func TestRemoteAddWalkthrough(t *testing.T) { - config := &client.Config{Remotes: make([]*client.RemoteVPS, 0)} + config := client.NewConfig("", "", "") in, err := ioutil.TempFile("", "") assert.Nil(t, err) defer in.Close() @@ -24,7 +24,7 @@ func TestRemoteAddWalkthrough(t *testing.T) { _, err = in.Seek(0, io.SeekStart) assert.Nil(t, err) - err = addRemoteWalkthrough(in, "inertia-rocks", "8080", "22", "dev", config) + err = addRemoteWalkthrough(in, config, "inertia-rocks", "8080", "22", "dev") r, found := config.GetRemote("inertia-rocks") assert.True(t, found) assert.Equal(t, "pemfile", r.PEM) @@ -34,7 +34,7 @@ func TestRemoteAddWalkthrough(t *testing.T) { } func TestRemoteAddWalkthroughFailure(t *testing.T) { - config := &client.Config{Remotes: make([]*client.RemoteVPS, 0)} + config := client.NewConfig("", "", "") in, err := ioutil.TempFile("", "") assert.Nil(t, err) defer in.Close() @@ -45,13 +45,13 @@ func TestRemoteAddWalkthroughFailure(t *testing.T) { _, err = in.Seek(0, io.SeekStart) assert.Nil(t, err) - err = addRemoteWalkthrough(in, "inertia-rocks", "8080", "22", "dev", config) + err = addRemoteWalkthrough(in, config, "inertia-rocks", "8080", "22", "dev") assert.Equal(t, errInvalidUser, err) in.WriteAt([]byte("pemfile\nuser\n\n"), 0) _, err = in.Seek(0, io.SeekStart) assert.Nil(t, err) - err = addRemoteWalkthrough(in, "inertia-rocks", "8080", "22", "dev", config) + err = addRemoteWalkthrough(in, config, "inertia-rocks", "8080", "22", "dev") assert.Equal(t, errInvalidAddress, err) } diff --git a/remote.go b/remote.go index 168a5825..4732fa1e 100644 --- a/remote.go +++ b/remote.go @@ -7,7 +7,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/ubclaunchpad/inertia/client" "github.com/ubclaunchpad/inertia/common" ) @@ -36,7 +35,7 @@ file. Specify a VPS name.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { // Ensure project initialized. - config, err := client.GetProjectConfigFromDisk() + config, path, err := getProjectConfigFromDisk() if err != nil { log.Fatal(err) } @@ -59,7 +58,11 @@ file. Specify a VPS name.`, } branch := head.Name().Short() - err = addRemoteWalkthrough(os.Stdin, args[0], port, sshPort, branch, config) + err = addRemoteWalkthrough(os.Stdin, config, args[0], port, sshPort, branch) + if err != nil { + log.Fatal(err) + } + err = config.Write(path) if err != nil { log.Fatal(err) } @@ -76,14 +79,14 @@ var listCmd = &cobra.Command{ Long: `Lists all currently configured remotes.`, Run: func(cmd *cobra.Command, args []string) { verbose, _ := cmd.Flags().GetBool("verbose") - config, err := client.GetProjectConfigFromDisk() + config, _, err := getProjectConfigFromDisk() if err != nil { log.Fatal(err) } for _, remote := range config.Remotes { if verbose { - printRemoteDetails(remote) + fmt.Println(formatRemoteDetails(remote)) } else { fmt.Println(remote.Name) } @@ -97,7 +100,7 @@ var removeCmd = &cobra.Command{ Long: `Remove a remote from Inertia's configuration file.`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - config, err := client.GetProjectConfigFromDisk() + config, path, err := getProjectConfigFromDisk() if err != nil { log.Fatal(err) } @@ -105,7 +108,7 @@ var removeCmd = &cobra.Command{ _, found := config.GetRemote(args[0]) if found { config.RemoveRemote(args[0]) - err = config.Write() + err = config.Write(path) if err != nil { log.Fatal("Failed to remove remote: " + err.Error()) } @@ -123,14 +126,14 @@ var showCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { // Ensure project initialized. - config, err := client.GetProjectConfigFromDisk() + config, _, err := getProjectConfigFromDisk() if err != nil { log.Fatal(err) } remote, found := config.GetRemote(args[0]) if found { - printRemoteDetails(remote) + fmt.Println(formatRemoteDetails(remote)) } else { println("No remote '" + args[0] + "' currently set up.") } @@ -144,17 +147,18 @@ var setCmd = &cobra.Command{ Args: cobra.MinimumNArgs(3), Run: func(cmd *cobra.Command, args []string) { // Ensure project initialized. - config, err := client.GetProjectConfigFromDisk() + config, path, err := getProjectConfigFromDisk() if err != nil { log.Fatal(err) } remote, found := config.GetRemote(args[0]) if found { - success := client.SetProperty(args[1], args[2], remote) + success := setProperty(args[1], args[2], remote) if success { + config.Write(path) println("Remote '" + args[0] + "' has been updated.") - printRemoteDetails(remote) + println(formatRemoteDetails(remote)) } else { // invalid input println("Remote setting '" + args[1] + "' not found.") @@ -165,15 +169,6 @@ var setCmd = &cobra.Command{ }, } -func printRemoteDetails(remote *client.RemoteVPS) { - fmt.Printf("Remote %s: \n", remote.Name) - fmt.Printf(" - Deployed Branch: %s\n", remote.Branch) - fmt.Printf(" - IP Address: %s\n", remote.IP) - fmt.Printf(" - VPS User: %s\n", remote.User) - fmt.Printf(" - PEM File Location: %s\n", remote.PEM) - fmt.Printf("Run 'inertia %s status' for more details.\n", remote.Name) -} - func init() { rootCmd.AddCommand(remoteCmd) remoteCmd.AddCommand(addCmd) diff --git a/storage.go b/storage.go new file mode 100644 index 00000000..f3599a10 --- /dev/null +++ b/storage.go @@ -0,0 +1,134 @@ +package main + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" + "reflect" + + "github.com/BurntSushi/toml" + "github.com/ubclaunchpad/inertia/client" + "github.com/ubclaunchpad/inertia/common" +) + +const configFileName = ".inertia.toml" + +// setProperty takes a struct pointer and searches for its "toml" tag with a search key +// and set property value with the tag +func setProperty(name string, value string, structObject interface{}) bool { + val := reflect.ValueOf(structObject) + + if val.Kind() != reflect.Ptr { + return false + } + structVal := val.Elem() + for i := 0; i < structVal.NumField(); i++ { + valueField := structVal.Field(i) + typeField := structVal.Type().Field(i) + if typeField.Tag.Get("toml") == name { + if valueField.IsValid() && valueField.CanSet() && valueField.Kind() == reflect.String { + valueField.SetString(value) + return true + } + } + } + return false +} + +// createConfigFile returns an error if the config directory +// already exists (the project is already initialized). +func createConfigFile(version, buildType string) error { + configFilePath, err := getConfigFilePath() + if err != nil { + return err + } + + // Check if Inertia is already set up. + s, fileErr := os.Stat(configFilePath) + if s != nil { + return errors.New("inertia already properly configured in this folder") + } + + // If file does not exist, create new configuration file. + if os.IsNotExist(fileErr) { + cwd, err := os.Getwd() + if err != nil { + return err + } + config := client.NewConfig(version, filepath.Base(cwd), buildType) + + f, err := os.Create(configFilePath) + if err != nil { + return err + } + defer f.Close() + config.Write(configFilePath) + } + + return nil +} + +// InitializeInertiaProject creates the inertia config folder and +// returns an error if we're not in a git project. +func initializeInertiaProject(version, buildType string) error { + cwd, err := os.Getwd() + if err != nil { + return err + } + err = common.CheckForGit(cwd) + if err != nil { + return err + } + + return createConfigFile(version, buildType) +} + +// getProjectConfigFromDisk returns the current project's configuration. +// If an .inertia folder is not found, it returns an error. +func getProjectConfigFromDisk() (*client.Config, string, error) { + configFilePath, err := getConfigFilePath() + if err != nil { + return nil, "", err + } + + raw, err := ioutil.ReadFile(configFilePath) + if err != nil { + if os.IsNotExist(err) { + return nil, configFilePath, errors.New("config file doesnt exist, try inertia init") + } + return nil, configFilePath, err + } + + var cfg client.Config + err = toml.Unmarshal(raw, &cfg) + if err != nil { + return nil, configFilePath, err + } + + return &cfg, configFilePath, err +} + +// getConfigFilePath returns the absolute path of the config file. +func getConfigFilePath() (string, error) { + path, err := os.Getwd() + if err != nil { + return "", err + } + return filepath.Join(path, configFileName), nil +} + +// getClient returns a local deployment setup +func getClient(name string) (*client.Client, error) { + config, _, err := getProjectConfigFromDisk() + if err != nil { + return nil, err + } + + client, found := client.NewClient(name, config) + if !found { + return nil, errors.New("Remote not found") + } + + return client, nil +} diff --git a/storage_test.go b/storage_test.go new file mode 100644 index 00000000..db960a95 --- /dev/null +++ b/storage_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/ubclaunchpad/inertia/client" +) + +func TestSetProperty(t *testing.T) { + testDaemonConfig := &client.DaemonConfig{ + Port: "8080", + Token: "abcdefg", + } + + testRemote := &client.RemoteVPS{ + Name: "testName", + IP: "1234", + User: "testUser", + PEM: "/some/pem/file", + Daemon: testDaemonConfig, + } + a := setProperty("name", "newTestName", testRemote) + assert.True(t, a) + assert.Equal(t, "newTestName", testRemote.Name) + + b := setProperty("wrongtag", "otherTestName", testRemote) + assert.False(t, b) + assert.Equal(t, "newTestName", testRemote.Name) + + c := setProperty("port", "8000", testDaemonConfig) + assert.True(t, c) + assert.Equal(t, "8000", testDaemonConfig.Port) +} + +func TestConfigCreateAndWriteAndRead(t *testing.T) { + err := createConfigFile("test", "dockerfile") + assert.Nil(t, err) + config, configPath, err := getProjectConfigFromDisk() + assert.Nil(t, err) + config.AddRemote(&client.RemoteVPS{ + Name: "test", + IP: "1234", + User: "bobheadxi", + PEM: "/some/pem/file", + SSHPort: "22", + Daemon: &client.DaemonConfig{ + Port: "8080", + }, + }) + config.AddRemote(&client.RemoteVPS{ + Name: "test2", + IP: "12343", + User: "bobheadxi234", + PEM: "/some/pem/file234", + SSHPort: "222", + Daemon: &client.DaemonConfig{ + Port: "80801", + }, + }) + err = config.Write(configPath) + assert.Nil(t, err) + + readConfig, _, err := getProjectConfigFromDisk() + assert.Nil(t, err) + assert.Equal(t, config.Remotes[0], readConfig.Remotes[0]) + assert.Equal(t, config.Remotes[1], readConfig.Remotes[1]) + + err = os.Remove(configPath) + assert.Nil(t, err) +} diff --git a/test/build/docker-compose/docker-compose.yml b/test/build/docker-compose/docker-compose.yml index 9fe678e8..9eea3608 100644 --- a/test/build/docker-compose/docker-compose.yml +++ b/test/build/docker-compose/docker-compose.yml @@ -5,4 +5,6 @@ services: image: python:alpine command: ["python", "-m", "http.server", "80"] ports: - - "80:80" + - "80:80" + - "443:443" + - "9001:9001" diff --git a/deploy_users.go b/users.go similarity index 94% rename from deploy_users.go rename to users.go index 934ad2de..e40956a7 100644 --- a/deploy_users.go +++ b/users.go @@ -9,7 +9,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/ubclaunchpad/inertia/client" "golang.org/x/crypto/ssh/terminal" ) @@ -31,7 +30,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 := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) } @@ -80,7 +79,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 := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) } @@ -117,7 +116,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 := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) } @@ -151,7 +150,7 @@ var deploymentUsersListCmd = &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 := client.GetDeployment(remoteName) + deployment, err := getClient(remoteName) if err != nil { log.Fatal(err) }