From 54241652450e339f627be78869fa0d3f58a54943 Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 20:22:37 -0800 Subject: [PATCH 01/13] daemon: tweak logs return message, fix doc --- daemon/inertiad/daemon/logs.go | 2 +- docs_src/api/swagger.yml | 30 ++++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/daemon/inertiad/daemon/logs.go b/daemon/inertiad/daemon/logs.go index fd3f1ebf..1dad00fe 100644 --- a/daemon/inertiad/daemon/logs.go +++ b/daemon/inertiad/daemon/logs.go @@ -105,7 +105,7 @@ func (s *Server) logHandler(w http.ResponseWriter, r *http.Request) { } else { buf := new(bytes.Buffer) buf.ReadFrom(logs) - render.Render(w, r, res.MsgOK("configured environment variables retrieved", + render.Render(w, r, res.MsgOK("logs retrieved", "logs", strings.Split(buf.String(), "\n"))) } } diff --git a/docs_src/api/swagger.yml b/docs_src/api/swagger.yml index 8cc66331..4fc93708 100644 --- a/docs_src/api/swagger.yml +++ b/docs_src/api/swagger.yml @@ -279,16 +279,30 @@ paths: example: 500 responses: 200: - description: Log contents retrieved + description: Success! content: - text/plain: - type: string - example: | - No deployment detected - Setting up project... - Cloning branch dev from git@github.com:example/example.git... + application/json: + schema: + allOf: + - $ref: '#/components/schemas/OKResponse' + - type: object + required: [ data ] + properties: + data: + type: object + required: [ token ] + properties: + token: + type: array + items: + type: string + description: Array of log entries + example: + - No deployment detected + - Setting up project... + - Cloning branch dev from git@github.com:example/example.git... 4XX,5XX: - $ref: '#/components/responses/Error' + $ref: '#/components/responses/Error' # auth From 7409ddcec3e7a2bd361dfdff435f8c351b0e9ca6 Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 20:23:10 -0800 Subject: [PATCH 02/13] deps: update to include gorilla/websocket@1.4.0 --- Gopkg.lock | 74 ++++++++++++++++++++++-------------------------------- Gopkg.toml | 2 +- 2 files changed, 31 insertions(+), 45 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 549b1d7b..d1d8ceea 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -22,11 +22,11 @@ name = "github.com/Microsoft/go-winio" packages = ["."] pruneopts = "NUT" - revision = "97e4973ce50b2ff5f09635a57e2b88a037aae829" - version = "v0.4.11" + revision = "1a8911d1ed007260465c3bfbbc785ac6915a0bb8" + version = "v0.4.12" [[projects]] - digest = "1:bd656681cbe059e319c341e1f7990d77ebe3afda6aaa2b62df423a8aeac66abf" + digest = "1:5587b23bb43ab016a2999fbd41f89d960190064b0b49d8146e1e0f21942cec62" name = "github.com/aws/aws-sdk-go" packages = [ "aws", @@ -62,8 +62,8 @@ "service/sts", ] pruneopts = "NUT" - revision = "81f3829f5a9d041041bdf56e55926691309d7699" - version = "v1.16.26" + revision = "3248e106c35aafb9b982f5d086f0150faf24ee4e" + version = "v1.17.6" [[projects]] digest = "1:90f59f03e8a0c973faff6d6122a4000efaee118907b5c695cc7615d2d8240538" @@ -114,7 +114,7 @@ [[projects]] branch = "master" - digest = "1:6e13d10a7940ca65fc8e54be8b0e564aa05b98333970b4a94eaa2fe52b9e0bb7" + digest = "1:a8450024ae144691ff6449a6b673673a823864974032649eeac27b2ff18e2af4" name = "github.com/docker/docker" packages = [ "api", @@ -137,7 +137,7 @@ "errdefs", ] pruneopts = "NUT" - revision = "50e63adf30d33fc1547527a4097c796cbe4b770f" + revision = "501cb131a7b79682de2cdb83d86e20975f38702d" [[projects]] digest = "1:2a47f7eb1a2c30428d1ee6808cb66d4deb17e68a3e55d696f03c8068552ba5e8" @@ -175,15 +175,15 @@ version = "v1.12.0" [[projects]] - digest = "1:9883e4bc6a4b3e0c2266ff322ccfcdf5566d11c889160fe798589a4633edc871" + digest = "1:e314993806c7b43f0d7c0abaedde500aa4de3010407a62a282e30748bac8df0b" name = "github.com/go-chi/chi" packages = [ ".", "middleware", ] pruneopts = "NUT" - revision = "1a6bb108ccf279c8dac6f9bad857d773b4f9b421" - version = "v4.0.1" + revision = "da24bba8dcd4021cafac38724bf10dccc97c3e36" + version = "v4.0.2" [[projects]] digest = "1:c831043316efe18a5b81f75568c0de847645e127cac35082c68de5b4b417d18f" @@ -202,12 +202,12 @@ version = "v1.0.1" [[projects]] - digest = "1:8ba832f19021187b30cf505ea0335c1b779b378e61c2468790082f7262925c4d" + digest = "1:bfbf4d40fc3eb7168f906a6cec5e92fb0946fc8beaf7df39328b52bc96facedc" name = "github.com/gogo/protobuf" packages = ["proto"] pruneopts = "NUT" - revision = "4cbf7e384e768b4e01799441fdf2a706a5635ae7" - version = "v1.2.0" + revision = "ba06b47c162d49f2af050fb4c75bcbc86a159d5c" + version = "v1.2.1" [[projects]] digest = "1:4a0c072e44da763409da72d41492373a034baf2e6d849c76d239b4abdfbb6c49" @@ -249,20 +249,20 @@ version = "0.5" [[projects]] - digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061" + digest = "1:9785a54031460a402fab4e4bbb3124c8dd9e9f7b1982109fef605cb91632d480" name = "github.com/mattn/go-colorable" packages = ["."] pruneopts = "NUT" - revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" - version = "v0.0.9" + revision = "3a70a971f94a22f2fa562ffcc7a0eb45f5daf045" + version = "v0.1.1" [[projects]] - digest = "1:bffa444ca07c69c599ae5876bc18b25bfd5fa85b297ca10a25594d284a7e9c5d" + digest = "1:f4109742a922eca7b7a6a1f97f93cc5834e21a5f13a1e0c1cead73815ab75548" name = "github.com/mattn/go-isatty" packages = ["."] pruneopts = "NUT" - revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" - version = "v0.0.4" + revision = "369ecd8cea9851e459abb67eb171853e3986591e" + version = "v0.0.6" [[projects]] digest = "1:f9f72e583aaacf1d1ac5d6121abd4afd3c690baa9e14e1d009df26bf831ba347" @@ -400,8 +400,8 @@ name = "github.com/xanzy/ssh-agent" packages = ["."] pruneopts = "NUT" - revision = "640f0ab560aeb89d523bb6ac322b1244d5c3796c" - version = "v0.2.0" + revision = "6a3e2ff9e7c564f36873c2e36413f634534f1c44" + version = "v0.2.1" [[projects]] digest = "1:ae8eea1a24ae43a46c2e96631b6303fcc4210ca0ac9d643e4da965029d1b511d" @@ -413,7 +413,7 @@ [[projects]] branch = "master" - digest = "1:534e6c6c171feb3a326cb08d02a690f3ce2ce946f72b37597e3f27de1f53f069" + digest = "1:8a74d3f3824e72a6a4e34a3538b146843d7431344217773c87e80e1deee9f075" name = "golang.org/x/crypto" packages = [ "bcrypt", @@ -438,7 +438,7 @@ "ssh/terminal", ] pruneopts = "NUT" - revision = "b8fe1690c61389d7d2a8074a507d1d40c5d30448" + revision = "7f87c0fbb88b590338857bcb720678c2583d4dea" [[projects]] branch = "master" @@ -452,33 +452,18 @@ "webdav/internal/xml", ] pruneopts = "NUT" - revision = "d26f9f9a57f3fab6a695bec0d84433c2c50f8bbf" + revision = "312bce6e941fc7e152b630a8643ea239afc12ddd" [[projects]] branch = "master" - digest = "1:c8b0ddc18c9e831e9d2d222f2555715ce6d447d1305b450f0c58622fb91cc078" + digest = "1:5a319b3daa32a7134d8797d73c7ad887282e57bdd8791043ec540e5c17360f5e" name = "golang.org/x/sys" packages = [ "unix", "windows", ] pruneopts = "NUT" - revision = "afcc84fd7533758f95a6e93ae710aa945a0b7e73" - -[[projects]] - digest = "1:8029e9743749d4be5bc9f7d42ea1659471767860f0cdc34d37c3111bd308a295" - name = "golang.org/x/text" - packages = [ - "internal/gen", - "internal/triegen", - "internal/ucd", - "transform", - "unicode/cldr", - "unicode/norm", - ] - pruneopts = "NUT" - revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" - version = "v0.3.0" + revision = "775f8194d0f9e65c46913c7be783d3d95a29333c" [[projects]] digest = "1:1cf1388ec8c73b7ecc711d9f279ab631ea0a6964d1ccc32809a6be90c33fa2a0" @@ -495,12 +480,13 @@ version = "v4.3.0" [[projects]] - digest = "1:121a091d3097060d20b8fa3f3da6d153f4e19922a70a458466d8b74380760ff7" + digest = "1:bcee186946d33b2e3befed808ee6ffe73387345807187e51790e3e148b5344c2" name = "gopkg.in/src-d/go-git.v4" packages = [ ".", "config", "internal/revision", + "internal/url", "plumbing", "plumbing/cache", "plumbing/filemode", @@ -540,8 +526,8 @@ "utils/merkletrie/noder", ] pruneopts = "NUT" - revision = "a1f6ef44dfed1253ef7f3bc049f66b15f8fc2ab2" - version = "v4.9.1" + revision = "db6c41c156481962abf9a55a324858674c25ab08" + version = "v4.10.0" [[projects]] digest = "1:b233ad4ec87ac916e7bf5e678e98a2cb9e8b52f6de6ad3e11834fc7a71b8e3bf" diff --git a/Gopkg.toml b/Gopkg.toml index 3b19cc1a..98f504a5 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -29,7 +29,7 @@ [[constraint]] name = "github.com/gorilla/websocket" - version = "1.2.0" + version = "1.4.0" [[constraint]] name = "github.com/aws/aws-sdk-go" From 57eb84c30312780708b723b2a6b52b05e96782f5 Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 20:23:55 -0800 Subject: [PATCH 03/13] client: split out UserClient, rework function returns, add debugging --- client/bootstrap/bootstrap_test.go | 11 +- client/client.go | 356 ++++++++++++++------ client/client_test.go | 500 +++++++++++------------------ client/sshc_test.go | 16 +- client/users.go | 160 +++++++++ client/users_test.go | 199 ++++++++++++ cmd/core/utils/input/input.go | 13 + cmd/remotes/env.go | 54 ++-- cmd/remotes/remotes.go | 211 +++--------- cmd/remotes/user.go | 117 ++----- cmd/remotes/user_totp.go | 41 +-- 11 files changed, 938 insertions(+), 740 deletions(-) create mode 100644 client/users.go create mode 100644 client/users_test.go diff --git a/client/bootstrap/bootstrap_test.go b/client/bootstrap/bootstrap_test.go index 2b03585d..dc2a9162 100644 --- a/client/bootstrap/bootstrap_test.go +++ b/client/bootstrap/bootstrap_test.go @@ -3,7 +3,7 @@ package bootstrap import ( - "net/http" + "context" "os" "testing" "time" @@ -28,8 +28,9 @@ func newIntegrationClient() *client.Client { }, } return client.NewClient(remote, client.Options{ - SSH: runner.SSHOptions{}, - Out: os.Stdout, + SSH: runner.SSHOptions{}, + Out: os.Stdout, + Debug: true, }) } @@ -45,7 +46,7 @@ func TestBootstrap_Integration(t *testing.T) { time.Sleep(3 * time.Second) // Check if daemon is online following bootstrap - resp, err := c.Status() + status, err := c.Status(context.Background()) assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "test", status.InertiaVersion) } diff --git a/client/client.go b/client/client.go index bde1fe53..cda5eece 100644 --- a/client/client.go +++ b/client/client.go @@ -1,7 +1,9 @@ package client import ( + "bufio" "bytes" + "context" "encoding/json" "errors" "fmt" @@ -17,22 +19,24 @@ import ( "github.com/ubclaunchpad/inertia/api" "github.com/ubclaunchpad/inertia/cfg" "github.com/ubclaunchpad/inertia/client/runner" + "github.com/ubclaunchpad/inertia/cmd/core/utils/output" "github.com/ubclaunchpad/inertia/common" ) // Client manages a deployment type Client struct { - out io.Writer + out io.Writer + ssh runner.SSHSession + debug bool Remote *cfg.Remote - - ssh runner.SSHSession } // Options denotes configuration options for a Client type Options struct { - SSH runner.SSHOptions - Out io.Writer + SSH runner.SSHOptions + Out io.Writer + Debug bool } // NewClient sets up a client to communicate to the daemon at the given remote @@ -52,6 +56,12 @@ func NewClient(remote *cfg.Remote, opts Options) *Client { } } +// WithWriter sets the given io.Writer as the client's default output +func (c *Client) WithWriter(out io.Writer) { c.out = out } + +// WithDebug sets the client's debug mode +func (c *Client) WithDebug(debug bool) { c.debug = true } + // GetSSHClient instantiates an SSH client for Inertia-related commands func (c *Client) GetSSHClient() (*SSHClient, error) { if c.ssh == nil { @@ -63,94 +73,209 @@ func (c *Client) GetSSHClient() (*SSHClient, error) { }, nil } +// GetUserClient instantiates an API client for Inertia user management commands +func (c *Client) GetUserClient() *UserClient { + return NewUserClient(c) +} + +// UpRequest declares parameters for project deployment +type UpRequest struct { + Project string + URL string + Profile cfg.Profile +} + // Up brings the project up on the remote VPS instance specified // in the deployment object. -func (c *Client) Up(project, url string, profile cfg.Profile, stream bool) (*http.Response, error) { - return c.post("/up", &api.UpRequest{ - Stream: stream, - Project: project, +func (c *Client) Up(ctx context.Context, req UpRequest) error { + resp, err := c.post(ctx, "/up", &api.UpRequest{ + Stream: false, + Project: req.Project, WebHookSecret: c.Remote.Daemon.WebHookSecret, - - BuildType: string(profile.Build.Type), - BuildFilePath: profile.Build.BuildFilePath, + BuildType: string(req.Profile.Build.Type), + BuildFilePath: req.Profile.Build.BuildFilePath, GitOptions: api.GitOptions{ - RemoteURL: common.GetSSHRemoteURL(url), - Branch: profile.Branch, + RemoteURL: common.GetSSHRemoteURL(req.URL), + Branch: req.Profile.Branch, }, }) + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } + base, err := c.unmarshal(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response: %s", err.Error()) + } + return base.Error() } -// LogIn gets an access token for the user with the given credentials. Use "" -// for totp if none is required. -func (c *Client) LogIn(user, password, totp string) (*http.Response, error) { - return c.post("/user/login", &api.UserRequest{ - Username: user, - Password: password, - Totp: totp, +// UpWithOutput blocks and streams 'up' output to the client's io.Writer +func (c *Client) UpWithOutput(ctx context.Context, req UpRequest) error { + resp, err := c.post(ctx, "/up", &api.UpRequest{ + Stream: true, + Project: req.Project, + WebHookSecret: c.Remote.Daemon.WebHookSecret, + BuildType: string(req.Profile.Build.Type), + BuildFilePath: req.Profile.Build.BuildFilePath, + GitOptions: api.GitOptions{ + RemoteURL: common.GetSSHRemoteURL(req.URL), + Branch: req.Profile.Branch, + }, }) + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } + defer resp.Body.Close() + + var reader = bufio.NewReader(resp.Body) + for { + select { + case <-ctx.Done(): + return nil + default: + line, err := reader.ReadBytes('\n') + if err != nil { + return fmt.Errorf("error occured while reading output: %s", err.Error()) + } + fmt.Fprint(c.out, string(line)) + } + } } // Token generates token on this remote. -func (c *Client) Token() (*http.Response, error) { - return c.get("/token", nil) +func (c *Client) Token(ctx context.Context) (token string, err error) { + resp, err := c.get(ctx, "/token", nil) + if err != nil { + return "", fmt.Errorf("failed to make request: %s", err.Error()) + } + + base, err := c.unmarshal(resp.Body, api.KV{Key: "token", Value: &token}) + resp.Body.Close() + if err != nil { + return "", fmt.Errorf("failed to read response: %s", err.Error()) + } + + return token, base.Error() } // Prune clears Docker ReadFiles on this remote. -func (c *Client) Prune() (*http.Response, error) { - return c.post("/prune", nil) +func (c *Client) Prune(ctx context.Context) error { + resp, err := c.post(ctx, "/prune", nil) + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } + + base, err := c.unmarshal(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response: %s", err.Error()) + } + + return base.Error() } // 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) +func (c *Client) Down(ctx context.Context) error { + resp, err := c.post(ctx, "/down", nil) + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } + + base, err := c.unmarshal(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response: %s", err.Error()) + } + + return base.Error() } // Status lists the currently active containers on the remote VPS instance -func (c *Client) Status() (*http.Response, error) { - resp, err := c.get("/status", nil) +func (c *Client) Status(ctx context.Context) (*api.DeploymentStatus, error) { + resp, err := c.get(ctx, "/status", nil) if err != nil && (strings.Contains(err.Error(), "EOF") || strings.Contains(err.Error(), "refused")) { return nil, errors.New("daemon on remote appears offline or inaccessible") + } else if err != nil { + return nil, fmt.Errorf("failed to make request: %s", err.Error()) } - return resp, err + + var status = &api.DeploymentStatus{} + base, err := c.unmarshal(resp.Body, api.KV{ + Key: "status", Value: status, + }) + if err != nil { + return nil, fmt.Errorf("failed to read response: %s", err.Error()) + } + + return status, base.Error() } // 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) +func (c *Client) Reset(ctx context.Context) error { + resp, err := c.post(ctx, "/reset", nil) + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } + + base, err := c.unmarshal(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response: %s", err.Error()) + } + + return base.Error() +} + +// LogsRequest denotes parameters for log querying +type LogsRequest struct { + Container string + Entries int } // Logs get logs of given container -func (c *Client) Logs(container string, entries int) (*http.Response, error) { - reqContent := map[string]string{api.Container: container} - if entries > 0 { - reqContent[api.Entries] = strconv.Itoa(entries) +func (c *Client) Logs(ctx context.Context, req LogsRequest) ([]string, error) { + reqContent := map[string]string{api.Container: req.Container} + if req.Entries > 0 { + reqContent[api.Entries] = strconv.Itoa(req.Entries) } - return c.get("/logs", reqContent) + resp, err := c.get(ctx, "/logs", reqContent) + if err != nil { + return nil, err + } + var logs = make([]string, 0) + b, err := c.unmarshal(resp.Body, api.KV{Key: "logs", Value: &logs}) + resp.Body.Close() + if err != nil { + output.Fatal(err) + } + return logs, b.Error() } -// LogsWebSocket opens a websocket connection to given container's logs -func (c *Client) LogsWebSocket(container string, entries int) (SocketReader, error) { +// LogsWithOutput opens a websocket connection to given container's logs and +// streams it to the given io.Writer +func (c *Client) LogsWithOutput(ctx context.Context, req LogsRequest) error { addr, err := c.Remote.DaemonAddr() if err != nil { - return nil, err + return err } host, err := url.Parse(addr) if err != nil { - return nil, err + return fmt.Errorf("invalid daemon address: %s", err.Error()) } // Set up request - url := &url.URL{Scheme: "wss", Host: host.Host, Path: "/logs"} - params := map[string]string{ - api.Container: container, + var url = &url.URL{Scheme: "wss", Host: host.Host, Path: "/logs"} + var params = map[string]string{ + api.Container: req.Container, api.Stream: "true", } - if entries > 0 { - params[api.Entries] = strconv.Itoa(entries) + if req.Entries > 0 { + params[api.Entries] = strconv.Itoa(req.Entries) } encodeQuery(url, params) @@ -158,71 +283,83 @@ func (c *Client) LogsWebSocket(container string, entries int) (SocketReader, err var header = http.Header{} header.Set("Authorization", "Bearer "+c.Remote.Daemon.Token) - // Attempt websocket connection + // set up websocket connection socket, resp, err := buildWebSocketDialer(c.Remote.Daemon.VerifySSL). - Dial(url.String(), header) + DialContext(ctx, url.String(), header) if err == websocket.ErrBadHandshake { - return nil, fmt.Errorf("websocket handshake failed with status %d", resp.StatusCode) + return fmt.Errorf("websocket handshake failed with status %d", resp.StatusCode) } if err != nil { - return nil, fmt.Errorf("failed to connect to daemon at %s: %s", url.Host, err.Error()) + return fmt.Errorf("failed to connect to daemon at %s: %s", url.Host, err.Error()) + } + defer socket.Close() + + // read from socket until error + var errC = make(chan error, 1) + go func() { + for { + _, line, err := socket.ReadMessage() + if err != nil { + errC <- fmt.Errorf("error occured while reading from socket: %s", err.Error()) + } + fmt.Fprint(c.out, string(line)) + } + }() + + // block until done + for { + select { + case <-ctx.Done(): + return nil + case err := <-errC: + return err + } } - return socket, nil } // UpdateEnv updates environment variable -func (c *Client) UpdateEnv(name, value string, encrypt, remove bool) (*http.Response, error) { - return c.post("/env", api.EnvRequest{ +func (c *Client) UpdateEnv(ctx context.Context, name, value string, encrypt, remove bool) error { + resp, err := c.post(ctx, "/env", api.EnvRequest{ Name: name, Value: value, Encrypt: encrypt, Remove: remove, }) -} - -// ListEnv lists environment variables currently set on remote -func (c *Client) ListEnv() (*http.Response, error) { - return c.get("/env", nil) -} - -// AddUser adds an authorized user for access to Inertia Web -func (c *Client) AddUser(username, password string, admin bool) (*http.Response, error) { - return c.post("/user/add", &api.UserRequest{ - Username: username, - Password: password, - Admin: admin, - }) -} + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } -// RemoveUser prevents a user from accessing Inertia Web -func (c *Client) RemoveUser(username string) (*http.Response, error) { - return c.post("/user/remove", &api.UserRequest{Username: username}) -} + base, err := c.unmarshal(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response: %s", err.Error()) + } -// ResetUsers resets all users on the remote. -func (c *Client) ResetUsers() (*http.Response, error) { - return c.post("/user/reset", nil) + return base.Error() } -// ListUsers lists all users on the remote. -func (c *Client) ListUsers() (*http.Response, error) { - return c.get("/user/list", nil) -} +// ListEnv lists environment variables currently set on remote +func (c *Client) ListEnv(ctx context.Context) ([]string, error) { + resp, err := c.get(ctx, "/env", nil) + if err != nil { + return nil, fmt.Errorf("failed to make request: %s", err.Error()) + } -// EnableTotp enables Totp for a given user -func (c *Client) EnableTotp(username, password string) (*http.Response, error) { - return c.post("/user/totp/enable", &api.UserRequest{ - Username: username, - Password: password, - }) -} + var variables = make([]string, 0) + base, err := c.unmarshal(resp.Body, api.KV{Key: "variables", Value: &variables}) + resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("failed to read response: %s", err.Error()) + } -// DisableTotp disables Totp for a given user -func (c *Client) DisableTotp() (*http.Response, error) { - return c.post("/user/totp/disable", nil) + return variables, base.Error() } // Sends a GET request. "queries" contains query string arguments. -func (c *Client) get(endpoint string, queries map[string]string) (*http.Response, error) { +func (c *Client) get( + ctx context.Context, + endpoint string, + queries map[string]string, +) (*http.Response, error) { // Assemble request - req, err := c.buildRequest("GET", endpoint, nil) + req, err := c.buildRequest(ctx, "GET", endpoint, nil) if err != nil { return nil, err } @@ -235,7 +372,11 @@ func (c *Client) get(endpoint string, queries map[string]string) (*http.Response return buildHTTPSClient(c.Remote.Daemon.VerifySSL).Do(req) } -func (c *Client) post(endpoint string, requestBody interface{}) (*http.Response, error) { +func (c *Client) post( + ctx context.Context, + endpoint string, + requestBody interface{}, +) (*http.Response, error) { // Assemble payload var payload io.Reader if requestBody != nil { @@ -249,7 +390,7 @@ func (c *Client) post(endpoint string, requestBody interface{}) (*http.Response, } // Assemble request - req, err := c.buildRequest("POST", endpoint, payload) + req, err := c.buildRequest(ctx, "POST", endpoint, payload) if err != nil { return nil, err } @@ -257,7 +398,12 @@ func (c *Client) post(endpoint string, requestBody interface{}) (*http.Response, return buildHTTPSClient(c.Remote.Daemon.VerifySSL).Do(req) } -func (c *Client) buildRequest(method string, endpoint string, payload io.Reader) (*http.Request, error) { +func (c *Client) buildRequest( + ctx context.Context, + method string, + endpoint string, + payload io.Reader, +) (*http.Request, error) { // Assemble URL addr, err := c.Remote.DaemonAddr() if err != nil { @@ -265,7 +411,7 @@ func (c *Client) buildRequest(method string, endpoint string, payload io.Reader) } url, err := url.Parse(addr) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid url configuration: %s", err.Error()) } url.Path = path.Join(url.Path, endpoint) @@ -276,6 +422,26 @@ func (c *Client) buildRequest(method string, endpoint string, payload io.Reader) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.Remote.Daemon.Token) + req.WithContext(ctx) + c.debugf("request constructed: %s %s (authorized: %v, with payload: %v, verified: %v)", + method, req.URL.String(), c.Remote.Daemon.Token != "", payload != nil, c.Remote.Daemon.VerifySSL) return req, nil } + +// unmarshal wraps c.unmarshal and logs responses +func (c *Client) unmarshal(r io.Reader, kvs ...api.KV) (*api.BaseResponse, error) { + b, err := api.Unmarshal(r, kvs...) + if b != nil { + bytes, _ := json.MarshalIndent(b, "", " ") + c.debugf("response received: %s (%s)", b.Message, string(bytes)) + } + return b, err +} + +// debugf logs to the client's output if debug is enabled +func (c *Client) debugf(format string, args ...interface{}) { + if c.debug { + fmt.Fprintf(c.out, "DEBUG: "+format+"\n", args...) + } +} diff --git a/client/client_test.go b/client/client_test.go index 5a878d86..91372ca4 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,6 +1,8 @@ package client import ( + "bytes" + "context" "encoding/json" "io/ioutil" "net/http" @@ -10,19 +12,24 @@ import ( "testing" "time" + "github.com/go-chi/render" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" - "github.com/ubclaunchpad/inertia/api" "github.com/ubclaunchpad/inertia/cfg" "github.com/ubclaunchpad/inertia/client/runner/mocks" + "github.com/ubclaunchpad/inertia/daemon/inertiad/res" ) var ( fakeAuth = "ubclaunchpad" ) -func newMockClient(ts *httptest.Server) *Client { +type testWriter struct{ t *testing.T } + +func (l *testWriter) Write(b []byte) (bytes int, e error) { l.t.Log(string(b)); return } + +func newMockClient(t *testing.T, ts *httptest.Server) *Client { var ( url string port string @@ -37,6 +44,8 @@ func newMockClient(ts *httptest.Server) *Client { } return &Client{ + debug: true, + out: &testWriter{t}, Remote: &cfg.Remote{ IP: url, SSH: &cfg.SSH{ @@ -49,12 +58,15 @@ func newMockClient(ts *httptest.Server) *Client { Token: fakeAuth, }, }, - out: os.Stdout, } } -func newMockSSHClient(m *mocks.FakeSSHSession) *Client { +func newMockSSHClient(t *testing.T, m *mocks.FakeSSHSession) *Client { return &Client{ + ssh: m, + out: &testWriter{t}, + debug: true, + Remote: &cfg.Remote{ Version: "test", IP: "127.0.0.1", @@ -67,22 +79,26 @@ func newMockSSHClient(m *mocks.FakeSSHSession) *Client { Port: "4303", }, }, - out: os.Stdout, - ssh: m, } } -func TestUp(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) +func TestNewClient(t *testing.T) { + var c = NewClient(&cfg.Remote{}, Options{}) + assert.NotNil(t, c) + c.WithDebug(false) + c.WithWriter(os.Stdout) +} + +func TestClient_Up(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request method - assert.Equal(t, "POST", req.Method) + assert.Equal(t, "POST", r.Method) // Check request body - body, err := ioutil.ReadAll(req.Body) + body, err := ioutil.ReadAll(r.Body) assert.NoError(t, err) - defer req.Body.Close() + defer r.Body.Close() var upReq api.UpRequest err = json.Unmarshal(body, &upReq) assert.NoError(t, err) @@ -92,420 +108,264 @@ func TestUp(t *testing.T) { assert.Equal(t, "docker-compose", upReq.BuildType) // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/up", endpoint) + assert.Equal(t, "/up", r.URL.Path) // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + render.Render(w, r, res.MsgOK("uwu")) })) defer testServer.Close() - var d = newMockClient(testServer) + var d = newMockClient(t, testServer) assert.False(t, d.Remote.Daemon.VerifySSL) - resp, err := d.Up("test_project", "myremote.git", cfg.Profile{ + assert.NoError(t, d.Up(context.Background(), UpRequest{"test_project", "myremote.git", cfg.Profile{ Build: &cfg.Build{ Type: cfg.DockerCompose, }, - }, false) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + }})) } -func TestPrune(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) +func TestClient_Prune(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request method - assert.Equal(t, "POST", req.Method) + assert.Equal(t, "POST", r.Method) // Check correct endpoint called - endpoint := req.URL.Path + endpoint := r.URL.Path assert.Equal(t, "/prune", endpoint) // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + render.Render(w, r, res.MsgOK("uwu")) })) defer testServer.Close() - d := newMockClient(testServer) - resp, err := d.Prune() - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + var d = newMockClient(t, testServer) + assert.NoError(t, d.Prune(context.Background())) } -func TestDown(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) +func TestClient_Down(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request method - assert.Equal(t, "POST", req.Method) + assert.Equal(t, "POST", r.Method) // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/down", endpoint) + assert.Equal(t, "/down", r.URL.Path) // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) - })) - defer testServer.Close() - - d := newMockClient(testServer) - resp, err := d.Down() - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestStatus(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - - // Check request method - assert.Equal(t, "GET", req.Method) + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) - // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/status", endpoint) - - // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) + render.Render(w, r, res.MsgOK("uwu")) })) defer testServer.Close() - d := newMockClient(testServer) - resp, err := d.Status() - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + var d = newMockClient(t, testServer) + assert.NoError(t, d.Down(context.Background())) } -func TestStatusFail(t *testing.T) { - d := newMockClient(nil) - _, err := d.Status() - assert.Contains(t, err.Error(), "appears offline") -} +func TestClient_Status(t *testing.T) { + t.Run("daemon online", func(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -func TestReset(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) + // Check request method + assert.Equal(t, "GET", r.Method) - // Check request method - assert.Equal(t, "POST", req.Method) + // Check correct endpoint called + assert.Equal(t, "/status", r.URL.Path) - // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/reset", endpoint) + // Check auth + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) - // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) - })) - defer testServer.Close() + // return something + render.Render(w, r, res.MsgOK("status retrieved", + "status", api.DeploymentStatus{Branch: "amazing_test"})) + })) + defer testServer.Close() - d := newMockClient(testServer) - resp, err := d.Reset() - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + var d = newMockClient(t, testServer) + status, err := d.Status(context.Background()) + assert.NoError(t, err) + assert.Equal(t, "amazing_test", status.Branch) + }) + + t.Run("daemon offline", func(t *testing.T) { + var d = newMockClient(t, nil) + _, err := d.Status(context.Background()) + assert.Contains(t, err.Error(), "appears offline") + }) } -func TestLogs(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) +func TestClient_Reset(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request method - assert.Equal(t, "GET", req.Method) + assert.Equal(t, "POST", r.Method) // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/logs", endpoint) - - // Check body - defer req.Body.Close() - q := req.URL.Query() - assert.Equal(t, "docker-compose", q.Get(api.Container)) - assert.Equal(t, "10", q.Get(api.Entries)) + assert.Equal(t, "/reset", r.URL.Path) // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + + render.Render(w, r, res.MsgOK("uwu")) })) defer testServer.Close() - d := newMockClient(testServer) - resp, err := d.Logs("docker-compose", 10) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + var d = newMockClient(t, testServer) + assert.NoError(t, d.Reset(context.Background())) } -func TestLogsWebsocket(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { +func TestClient_Logs(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check request method - assert.Equal(t, "GET", req.Method) + assert.Equal(t, "GET", r.Method) // Check correct endpoint called - endpoint := req.URL.Path + endpoint := r.URL.Path assert.Equal(t, "/logs", endpoint) // Check body - defer req.Body.Close() - q := req.URL.Query() + q := r.URL.Query() assert.Equal(t, "docker-compose", q.Get(api.Container)) - assert.Equal(t, "true", q.Get(api.Stream)) assert.Equal(t, "10", q.Get(api.Entries)) // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) - - socketUpgrader := websocket.Upgrader{} - socket, err := socketUpgrader.Upgrade(rw, req, nil) - assert.NoError(t, err) - - err = socket.WriteMessage(websocket.TextMessage, []byte("hello world")) - assert.NoError(t, err) - })) - defer testServer.Close() - - d := newMockClient(testServer) - resp, err := d.LogsWebSocket("docker-compose", 10) - assert.NoError(t, err) - - time.Sleep(1 * time.Second) - _, m, err := resp.ReadMessage() - assert.NoError(t, err) - assert.Equal(t, []byte("hello world"), m) -} - -func TestLogsWebsocketNoDaemon(t *testing.T) { - testServer := httptest.NewTLSServer(nil) - // close the server to test error - testServer.Close() - - d := newMockClient(testServer) - _, err := d.LogsWebSocket("docker-compose", 10) - assert.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "connect: connection refused") || strings.Contains(err.Error(), "connectex: No connection could be made")) -} - -func TestUpdateEnv(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - - // Check request method - assert.Equal(t, "POST", req.Method) - - // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/env", endpoint) - - // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) - })) - defer testServer.Close() - - d := newMockClient(testServer) - resp, err := d.UpdateEnv("", "", false, false) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestListEnv(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) - // Check request method - assert.Equal(t, "GET", req.Method) - - // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/env", endpoint) - - // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) + // return response + render.Render(w, r, res.MsgOK("logs retrieved", + "logs", []string{"hello", "world"})) })) defer testServer.Close() - d := newMockClient(testServer) - resp, err := d.ListEnv() + var d = newMockClient(t, testServer) + logs, err := d.Logs(context.Background(), LogsRequest{"docker-compose", 10}) assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, []string{"hello", "world"}, logs) } -func TestAddUser(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - - // Check request method - assert.Equal(t, "POST", req.Method) - - // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/user/add", endpoint) - - // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) - })) - defer testServer.Close() - - d := newMockClient(testServer) - resp, err := d.AddUser("", "", false) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) +func TestClient_LogsWithOutput(t *testing.T) { + t.Run("daemon online", func(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // Check request method + assert.Equal(t, "GET", req.Method) + + // Check correct endpoint called + endpoint := req.URL.Path + assert.Equal(t, "/logs", endpoint) + + // Check body + defer req.Body.Close() + var q = req.URL.Query() + assert.Equal(t, "docker-compose", q.Get(api.Container)) + assert.Equal(t, "true", q.Get(api.Stream)) + assert.Equal(t, "10", q.Get(api.Entries)) + + // Check auth + assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) + + // upgrade to websocket and write back + var socketUpgrader = websocket.Upgrader{} + socket, err := socketUpgrader.Upgrade(rw, req, nil) + assert.NoError(t, err) + assert.NoError(t, socket.WriteMessage( + websocket.TextMessage, []byte("hello world"))) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer) + var buf = &bytes.Buffer{} + d.out = buf + ctx, cancel := context.WithCancel(context.Background()) + go func() { + time.Sleep(1 * time.Second) + assert.Equal(t, "hello world", buf.String()) + t.Log("message received!") + cancel() + }() + assert.NoError(t, d.LogsWithOutput(ctx, LogsRequest{"docker-compose", 10})) + }) + + t.Run("daemon offline", func(t *testing.T) { + testServer := httptest.NewTLSServer(nil) + // close the server to test error + testServer.Close() + + var d = newMockClient(t, testServer) + var err = d.LogsWithOutput(context.Background(), LogsRequest{"docker-compose", 10}) + assert.Error(t, err) + assert.True(t, + strings.Contains(err.Error(), "connect: connection refused") || + strings.Contains(err.Error(), "connectex: No connection could be made")) + }) } -func TestRemoveUser(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) +func TestClient_UpdateEnv(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request method - assert.Equal(t, "POST", req.Method) + assert.Equal(t, "POST", r.Method) // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/user/remove", endpoint) + assert.Equal(t, "/env", r.URL.Path) // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) - })) - defer testServer.Close() - - d := newMockClient(testServer) - resp, err := d.RemoveUser("") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestResetUser(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - - // Check request method - assert.Equal(t, "POST", req.Method) + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) - // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/user/reset", endpoint) - - // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) + render.Render(w, r, res.MsgOK("uwu")) })) defer testServer.Close() - d := newMockClient(testServer) - resp, err := d.ResetUsers() - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + d := newMockClient(t, testServer) + assert.NoError(t, d.UpdateEnv(context.Background(), "", "", false, false)) } -func TestListUsers(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) +func TestClient_ListEnv(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request method - assert.Equal(t, "GET", req.Method) + assert.Equal(t, "GET", r.Method) // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/user/list", endpoint) + endpoint := r.URL.Path + assert.Equal(t, "/env", endpoint) // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + render.Render(w, r, res.Msg("configured environment variables retrieved", http.StatusOK, + "variables", []string{"hello", "world"})) })) defer testServer.Close() - d := newMockClient(testServer) - resp, err := d.ListUsers() + var d = newMockClient(t, testServer) + envs, err := d.ListEnv(context.Background()) assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, []string{"hello", "world"}, envs) } -func TestToken(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) +func TestClient_Token(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request method - assert.Equal(t, http.MethodGet, req.Method) + assert.Equal(t, http.MethodGet, r.Method) // Check correct endpoint called - endpoint := req.URL.Path + endpoint := r.URL.Path assert.Equal(t, "/token", endpoint) // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) - })) - defer testServer.Close() - - d := newMockClient(testServer) - resp, err := d.Token() - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestLogIn(t *testing.T) { - username := "testguy" - password := "SomeKindo23asdfpassword" - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - - // Check request method - assert.Equal(t, http.MethodPost, req.Method) - - // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/user/login", endpoint) - - // Check auth - defer req.Body.Close() - body, err := ioutil.ReadAll(req.Body) - assert.Equal(t, nil, err) - var userReq api.UserRequest - assert.Equal(t, nil, json.Unmarshal(body, &userReq)) - assert.Equal(t, userReq.Username, username) - assert.Equal(t, userReq.Password, password) - })) - defer testServer.Close() - - d := newMockClient(testServer) - resp, err := d.LogIn(username, password, "") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestEnableTotp(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - - // Check request method - assert.Equal(t, "POST", req.Method) - - // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/user/totp/enable", endpoint) - - // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) - })) - defer testServer.Close() - - d := newMockClient(testServer) - resp, err := d.EnableTotp("", "") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestDisableTotp(t *testing.T) { - testServer := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(http.StatusOK) - - // Check request method - assert.Equal(t, "POST", req.Method) - - // Check correct endpoint called - endpoint := req.URL.Path - assert.Equal(t, "/user/totp/disable", endpoint) + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) - // Check auth - assert.Equal(t, "Bearer "+fakeAuth, req.Header.Get("Authorization")) + render.Render(w, r, res.MsgOK("token generated", + "token", "hello-world")) })) defer testServer.Close() - d := newMockClient(testServer) - resp, err := d.DisableTotp() + var d = newMockClient(t, testServer) + token, err := d.Token(context.Background()) assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "hello-world", token) } diff --git a/client/sshc_test.go b/client/sshc_test.go index fdaf829a..04a7db1c 100644 --- a/client/sshc_test.go +++ b/client/sshc_test.go @@ -9,9 +9,9 @@ import ( "github.com/ubclaunchpad/inertia/client/runner/mocks" ) -func TestInstallDocker(t *testing.T) { +func TestSSHClient_InstallDocker(t *testing.T) { var session = &mocks.FakeSSHSession{} - var client = newMockSSHClient(session) + var client = newMockSSHClient(t, session) // Get original script for comparison script, err := ioutil.ReadFile("scripts/docker.sh") @@ -28,9 +28,9 @@ func TestInstallDocker(t *testing.T) { assert.Equal(t, string(script), call) } -func TestDaemonUp(t *testing.T) { +func TestSSHClient_DaemonUp(t *testing.T) { var session = &mocks.FakeSSHSession{} - var client = newMockSSHClient(session) + var client = newMockSSHClient(t, session) // Get original script for comparison script, err := ioutil.ReadFile("scripts/daemon-up.sh") @@ -48,9 +48,9 @@ func TestDaemonUp(t *testing.T) { assert.Equal(t, actualCommand, call) } -func TestDaemonDown(t *testing.T) { +func TestSSHClient_DaemonDown(t *testing.T) { var session = &mocks.FakeSSHSession{} - var client = newMockSSHClient(session) + var client = newMockSSHClient(t, session) // Get original script for comparison script, err := ioutil.ReadFile("scripts/daemon-down.sh") @@ -65,9 +65,9 @@ func TestDaemonDown(t *testing.T) { assert.Equal(t, string(script), session.RunArgsForCall(0)) } -func TestKeyGen(t *testing.T) { +func TestSSHClient_KeyGen(t *testing.T) { var session = &mocks.FakeSSHSession{} - var client = newMockSSHClient(session) + var client = newMockSSHClient(t, session) // Get original script for comparison script, err := ioutil.ReadFile("scripts/token.sh") diff --git a/client/users.go b/client/users.go new file mode 100644 index 00000000..cf3ce1a9 --- /dev/null +++ b/client/users.go @@ -0,0 +1,160 @@ +package client + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/ubclaunchpad/inertia/api" +) + +var ( + // ErrNeedTotp is used to indicate that a 2FA-enabled user has not provided a TOTP + ErrNeedTotp = errors.New("TOTP is needed for user") +) + +// UserClient is used to access Inertia's /user APIs +type UserClient struct { + c *Client +} + +// NewUserClient instantiates a new client for user management functions +func NewUserClient(c *Client) *UserClient { + return &UserClient{c} +} + +// AuthenticateRequest denotes options for authenticating with the Inertia daemon +type AuthenticateRequest struct { + User string + Password string + TOTP string +} + +// Authenticate gets an access token for the user with the given credentials. Use "" +// for totp if none is required. +func (u *UserClient) Authenticate(ctx context.Context, req AuthenticateRequest) (token string, err error) { + resp, err := u.c.post(ctx, "/user/login", &api.UserRequest{ + Username: req.User, + Password: req.Password, + Totp: req.TOTP, + }) + if err != nil { + return "", fmt.Errorf("failed to make request: %s", err.Error()) + } + if resp.StatusCode == http.StatusExpectationFailed { + return "", ErrNeedTotp + } + + base, err := u.c.unmarshal(resp.Body, api.KV{Key: "token", Value: &token}) + resp.Body.Close() + if err != nil { + return "", fmt.Errorf("failed to read response: %s", err.Error()) + } + + return token, base.Error() +} + +// AddUser adds an authorized user for access to Inertia Web +func (u *UserClient) AddUser(ctx context.Context, username, password string, admin bool) error { + resp, err := u.c.post(ctx, "/user/add", &api.UserRequest{ + Username: username, + Password: password, + Admin: admin, + }) + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } + + base, err := u.c.unmarshal(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response: %s", err.Error()) + } + + return base.Error() +} + +// RemoveUser prevents a user from accessing Inertia Web +func (u *UserClient) RemoveUser(ctx context.Context, username string) error { + resp, err := u.c.post(ctx, "/user/remove", &api.UserRequest{Username: username}) + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } + + base, err := u.c.unmarshal(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response: %s", err.Error()) + } + + return base.Error() +} + +// ResetUsers resets all users on the remote. +func (u *UserClient) ResetUsers(ctx context.Context) error { + resp, err := u.c.post(ctx, "/user/reset", nil) + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } + + base, err := u.c.unmarshal(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response: %s", err.Error()) + } + + return base.Error() +} + +// ListUsers lists all users on the remote. +func (u *UserClient) ListUsers(ctx context.Context) ([]string, error) { + resp, err := u.c.get(ctx, "/user/list", nil) + if err != nil { + return nil, fmt.Errorf("failed to make request: %s", err.Error()) + } + + var users = make([]string, 0) + base, err := u.c.unmarshal(resp.Body, api.KV{Key: "users", Value: &users}) + resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("failed to read response: %s", err.Error()) + } + + return users, base.Error() +} + +// EnableTotp enables Totp for a given user +func (u *UserClient) EnableTotp(ctx context.Context, username, password string) (*api.TotpResponse, error) { + resp, err := u.c.post(ctx, "/user/totp/enable", &api.UserRequest{ + Username: username, + Password: password, + }) + if err != nil { + return nil, fmt.Errorf("failed to make request: %s", err.Error()) + } + + var totp api.TotpResponse + base, err := u.c.unmarshal(resp.Body, api.KV{Key: "totp", Value: &totp}) + if err != nil { + return nil, fmt.Errorf("failed to read response: %s", err.Error()) + } + + return &totp, base.Error() +} + +// DisableTotp disables Totp for a given user +func (u *UserClient) DisableTotp(ctx context.Context) error { + resp, err := u.c.post(ctx, "/user/totp/disable", nil) + if err != nil { + return fmt.Errorf("failed to make request: %s", err.Error()) + } + + base, err := u.c.unmarshal(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response: %s", err.Error()) + } + + return base.Error() +} diff --git a/client/users_test.go b/client/users_test.go new file mode 100644 index 00000000..cd06119d --- /dev/null +++ b/client/users_test.go @@ -0,0 +1,199 @@ +package client + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/render" + "github.com/stretchr/testify/assert" + "github.com/ubclaunchpad/inertia/api" + "github.com/ubclaunchpad/inertia/daemon/inertiad/res" +) + +func TestUserClient_AddUser(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Check request method + assert.Equal(t, "POST", r.Method) + + // Check correct endpoint called + assert.Equal(t, "/user/add", r.URL.Path) + + // Check auth + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + + render.Render(w, r, res.MsgOK("uwu")) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer).GetUserClient() + assert.NoError(t, d.AddUser(context.Background(), "", "", false)) +} + +func TestUserClient_RemoveUser(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Check request method + assert.Equal(t, "POST", r.Method) + + // Check correct endpoint called + assert.Equal(t, "/user/remove", r.URL.Path) + + // Check auth + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + + render.Render(w, r, res.MsgOK("uwu")) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer).GetUserClient() + assert.NoError(t, d.RemoveUser(context.Background(), "yaoharry")) +} + +func TestUserClient_ResetUser(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Check request method + assert.Equal(t, "POST", r.Method) + + // Check correct endpoint called + assert.Equal(t, "/user/reset", r.URL.Path) + + // Check auth + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + + render.Render(w, r, res.MsgOK("uwu")) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer).GetUserClient() + assert.NoError(t, d.ResetUsers(context.Background())) +} + +func TestUserClient_ListUsers(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Check request method + assert.Equal(t, "GET", r.Method) + + // Check correct endpoint called + assert.Equal(t, "/user/list", r.URL.Path) + + // Check auth + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + + render.Render(w, r, res.MsgOK("users retrieved", + "users", []string{"yaoharry"})) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer).GetUserClient() + users, err := d.ListUsers(context.Background()) + assert.NoError(t, err) + assert.Equal(t, []string{"yaoharry"}, users) +} + +func TestUserClient_Authenticate(t *testing.T) { + username := "testguy" + password := "SomeKindo23asdfpassword" + + t.Run("normal login", func(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Check request method + assert.Equal(t, http.MethodPost, r.Method) + + // Check correct endpoint called + endpoint := r.URL.Path + assert.Equal(t, "/user/login", endpoint) + + // Check auth + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + assert.Equal(t, nil, err) + var userReq api.UserRequest + assert.Equal(t, nil, json.Unmarshal(body, &userReq)) + assert.Equal(t, userReq.Username, username) + assert.Equal(t, userReq.Password, password) + + render.Render(w, r, res.MsgOK("session created", + "token", "uwu")) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer).GetUserClient() + token, err := d.Authenticate(context.Background(), AuthenticateRequest{ + User: username, + Password: password, + TOTP: "", + }) + assert.NoError(t, err) + assert.Equal(t, "uwu", token) + }) + + t.Run("requires TOTP", func(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + render.Render(w, r, res.Err("uwu", http.StatusPreconditionFailed)) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer).GetUserClient() + token, err := d.Authenticate(context.Background(), AuthenticateRequest{ + User: username, + Password: password, + TOTP: "", + }) + assert.Error(t, err) + assert.Equal(t, "", token) + }) +} + +func TestUserClient_EnableTotp(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Check request method + assert.Equal(t, "POST", r.Method) + + // Check correct endpoint called + endpoint := r.URL.Path + assert.Equal(t, "/user/totp/enable", endpoint) + + // Check auth + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + + render.Render(w, r, res.MsgOK("TOTP successfully enabled", + "totp", &api.TotpResponse{ + TotpSecret: "uwu", + })) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer).GetUserClient() + totp, err := d.EnableTotp(context.Background(), "", "") + assert.NoError(t, err) + assert.Equal(t, "uwu", totp.TotpSecret) +} + +func TestUserClient_DisableTotp(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Check request method + assert.Equal(t, "POST", r.Method) + + // Check correct endpoint called + assert.Equal(t, "/user/totp/disable", r.URL.Path) + + // Check auth + assert.Equal(t, "Bearer "+fakeAuth, r.Header.Get("Authorization")) + + render.Render(w, r, res.MsgOK("uwu")) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer).GetUserClient() + assert.NoError(t, d.DisableTotp(context.Background())) +} diff --git a/cmd/core/utils/input/input.go b/cmd/core/utils/input/input.go index 32e9a9fa..56b248cb 100644 --- a/cmd/core/utils/input/input.go +++ b/cmd/core/utils/input/input.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "os" + "os/signal" + "syscall" "github.com/ubclaunchpad/inertia/cfg" ) @@ -17,6 +19,17 @@ var ( errInvalidBuildFilePath = errors.New("invalid buildfile path") ) +// CatchSigterm listens in the background for some kind of interrupt and calls +// the given cancelFunc as necessary +func CatchSigterm(cancelFunc func()) { + var signals = make(chan os.Signal) + signal.Notify(signals, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + go func() { + <-signals + cancelFunc() + }() +} + // Prompt prints the given query and reads the response func Prompt(query string) (string, error) { println(query) diff --git a/cmd/remotes/env.go b/cmd/remotes/env.go index af260004..3cc9b662 100644 --- a/cmd/remotes/env.go +++ b/cmd/remotes/env.go @@ -1,12 +1,10 @@ package remotescmd import ( - "fmt" - "io/ioutil" + "context" "strings" "github.com/spf13/cobra" - "github.com/ubclaunchpad/inertia/api" "github.com/ubclaunchpad/inertia/cmd/core/utils/output" ) @@ -43,6 +41,9 @@ as follows: host.AddCommand(env.Command) } +// Context returns the root host command's context +func (root *EnvCmd) Context() context.Context { return root.host.ctx } + func (root *EnvCmd) attachSetCmd() { const flagEncrypt = "encrypt" var set = &cobra.Command{ @@ -53,16 +54,16 @@ variables are applied to all deployed containers.`, Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { var encrypt, _ = cmd.Flags().GetBool(flagEncrypt) - resp, err := root.host.client.UpdateEnv(args[0], args[1], encrypt, false) - if err != nil { + if err := root.host.client.UpdateEnv( + root.Context(), + args[0], + args[1], + encrypt, + false, + ); err != nil { output.Fatal(err) } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - output.Fatal(err) - } - fmt.Printf("(Status code %d) %s\n", resp.StatusCode, body) + println("env value successfully updated") }, } set.Flags().BoolP(flagEncrypt, "e", false, "encrypt variable when stored") @@ -77,17 +78,16 @@ func (root *EnvCmd) attachRemoveCmd() { and persistent environment storage.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - resp, err := root.host.client.UpdateEnv(args[0], "", false, true) - if err != nil { - output.Fatal(err) - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { + if err := root.host.client.UpdateEnv( + root.Context(), + args[0], + "", + false, + true, + ); err != nil { output.Fatal(err) } - - fmt.Printf("(Status code %d) %s\n", resp.StatusCode, body) + println("env value successfully removed") }, } root.AddCommand(remove) @@ -100,21 +100,15 @@ func (root *EnvCmd) attachListCmd() { Long: `Lists currently set and saved environment variables. The values of encrypted variables are not be decrypted.`, Run: func(cmd *cobra.Command, args []string) { - resp, err := root.host.client.ListEnv() - if err != nil { - output.Fatal(err) - } - defer resp.Body.Close() - var variables = make([]string, 0) - b, err := api.Unmarshal(resp.Body, api.KV{Key: "variables", Value: &variables}) + variables, err := root.host.client.ListEnv(root.Context()) if err != nil { output.Fatal(err) } + if len(variables) == 0 { - fmt.Printf("(Status code %d) no variables configured", resp.StatusCode) + println("no variables configured on remote") } else { - fmt.Printf("(Status code %d) %s: \n%s\n", - resp.StatusCode, b.Message, strings.Join(variables, "\n")) + println(strings.Join(variables, "\n")) } }, } diff --git a/cmd/remotes/remotes.go b/cmd/remotes/remotes.go index 9f0df206..931e863b 100644 --- a/cmd/remotes/remotes.go +++ b/cmd/remotes/remotes.go @@ -1,22 +1,20 @@ package remotescmd import ( - "bufio" + "context" "fmt" - "io/ioutil" - "net/http" "os" "path" "strings" "github.com/spf13/cobra" - "github.com/ubclaunchpad/inertia/api" "github.com/ubclaunchpad/inertia/cfg" "github.com/ubclaunchpad/inertia/client" "github.com/ubclaunchpad/inertia/client/bootstrap" "github.com/ubclaunchpad/inertia/client/runner" "github.com/ubclaunchpad/inertia/cmd/core" + "github.com/ubclaunchpad/inertia/cmd/core/utils/input" "github.com/ubclaunchpad/inertia/cmd/core/utils/output" "github.com/ubclaunchpad/inertia/local" ) @@ -59,6 +57,7 @@ type HostCmd struct { project *cfg.Project client *client.Client + ctx context.Context } // CmdOptions denotes options for individual host subcommands @@ -69,6 +68,7 @@ type CmdOptions struct { const ( flagShort = "short" + flagDebug = "debug" ) // AttachRemoteHostCmd attaches a subcommand for a configured remote host to the @@ -78,6 +78,8 @@ func AttachRemoteHostCmd( opts CmdOptions, hidden ...bool, ) { + ctx, cancel := context.WithCancel(context.Background()) + input.CatchSigterm(cancel) var host = &HostCmd{ project: opts.ProjectCfg, client: client.NewClient(opts.RemoteCfg, client.Options{ @@ -86,6 +88,7 @@ func AttachRemoteHostCmd( }, Out: os.Stdout, }), + ctx: ctx, } host.Command = &cobra.Command{ Use: opts.RemoteCfg.Name + " [command]", @@ -116,10 +119,14 @@ Run 'inertia [remote] init' to gather this information.`, fmt.Printf("[WARNING] Remote configuration version '%s' does not match your Inertia CLI version '%s'\n", host.getRemote().Version, inertia.Version) } + var debug, _ = cmd.Flags().GetBool(flagDebug) + host.client.WithDebug(debug) }, } host.PersistentFlags().BoolP(flagShort, "s", false, "don't stream output from command") + host.PersistentFlags().BoolP(flagDebug, "d", false, + "enable debug output from Inertia client") // attach children host.attachInitCmd() @@ -165,41 +172,22 @@ This requires an Inertia daemon to be active on your remote - do this by running fmt.Printf("deploying project '%s' using profile '%s'\n", root.project.Name, profileName) // Make up request - resp, err := root.client.Up( - root.project.Name, - root.project.URL, - *profile, - !short) - if err != nil { - output.Fatal(err) - } - defer resp.Body.Close() + var req = client.UpRequest{ + Project: root.project.Name, + URL: root.project.URL, + Profile: *profile} + var err error if short { - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - output.Fatal(err) - } - switch resp.StatusCode { - case http.StatusCreated: - fmt.Printf("(Status code %d) Project build started!\n", resp.StatusCode) - case http.StatusUnauthorized: - fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, body) - case http.StatusPreconditionFailed: - fmt.Printf("(Status code %d) Problem with deployment setup:\n%s\n", resp.StatusCode, body) - default: - fmt.Printf("(Status code %d) Unknown response from daemon:\n%s\n", - resp.StatusCode, body) - } + err = root.client.Up(root.ctx, req) } else { - reader := bufio.NewReader(resp.Body) - for { - line, err := reader.ReadBytes('\n') - if err != nil { - break - } - fmt.Print(string(line)) - } + err = root.client.UpWithOutput(root.ctx, req) + } + if err != nil { + output.Fatal(err) + } + if !short { + println("project deployment successfully started!") } }, } @@ -215,28 +203,10 @@ func (root *HostCmd) attachDownCmd() { Requires project to be online - do this by running 'inertia [remote] up`, Run: func(cmd *cobra.Command, args []string) { - resp, err := root.client.Down() - if err != nil { + if err := root.client.Down(root.ctx); err != nil { output.Fatal(err) } - - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - output.Fatal(err) - } - - switch resp.StatusCode { - case http.StatusOK: - fmt.Printf("(Status code %d) Project down\n", resp.StatusCode) - case http.StatusPreconditionFailed: - fmt.Printf("(Status code %d) No containers are currently active\n", resp.StatusCode) - case http.StatusUnauthorized: - fmt.Printf("(Status code %d) Bad auth: %s\n", resp.StatusCode, body) - default: - fmt.Printf("(Status code %d) Unknown response from daemon: %s\n", - resp.StatusCode, body) - } + println("project successfully shut down") }, } root.AddCommand(down) @@ -250,41 +220,18 @@ func (root *HostCmd) attachStatusCmd() { Requires the Inertia daemon to be active on your remote - do this by running 'inertia [remote] up'`, Run: func(cmd *cobra.Command, args []string) { - resp, err := root.client.Status() + status, err := root.client.Status(root.ctx) if err != nil { output.Fatal(err) } - defer resp.Body.Close() - switch resp.StatusCode { - case http.StatusOK: - host, err := root.getRemote().DaemonAddr() - if err != nil { - output.Fatal(err) - } - fmt.Printf("(Status code %d) Daemon at remote '%s' online at %s\n", - resp.StatusCode, root.remote, host) - var status = &api.DeploymentStatus{} - if _, err := api.Unmarshal(resp.Body, api.KV{ - Key: "status", Value: status, - }); err != nil { - output.Fatal(err) - } - println(output.FormatStatus(status)) - case http.StatusUnauthorized: - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - output.Fatal(err) - } - fmt.Printf("(Status code %d) Bad auth: %s\n", resp.StatusCode, body) - default: - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - output.Fatal(err) - } - fmt.Printf("(Status code %d) %s\n", - resp.StatusCode, body) + host, err := root.getRemote().DaemonAddr() + if err != nil { + output.Fatal(err) } + fmt.Printf("daemon on remote '%s' is online at %s\n", + root.remote, host) + println(output.FormatStatus(status)) }, } root.AddCommand(stat) @@ -310,46 +257,22 @@ Use 'inertia [remote] status' to see which containers are active.`, container = args[0] } + var req = client.LogsRequest{ + Container: container, + Entries: entries} + if short { // if short, just grab the last x log entries - resp, err := root.client.Logs(container, entries) - if err != nil { - output.Fatal(err) - } - defer resp.Body.Close() - - var logs []string - b, err := api.Unmarshal(resp.Body, api.KV{Key: "logs", Value: &logs}) + logs, err := root.client.Logs(root.ctx, req) if err != nil { output.Fatal(err) } - - switch resp.StatusCode { - case http.StatusOK: - fmt.Printf("(Status code %d) Logs: \n%s\n", resp.StatusCode, strings.Join(logs, "\n")) - case http.StatusUnauthorized: - fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, b.Message) - case http.StatusPreconditionFailed: - fmt.Printf("(Status code %d) Problem with deployment setup:\n%s\n", resp.StatusCode, b.Message) - default: - fmt.Printf("(Status code %d) Unknown response from daemon:\n%s\n", - resp.StatusCode, b.Message) - } + println(strings.Join(logs, "\n")) } else { // if not short, open a websocket to stream logs - socket, err := root.client.LogsWebSocket(container, entries) - if err != nil { + if err := root.client.LogsWithOutput(root.ctx, req); err != nil { output.Fatal(err) } - defer socket.Close() - - for { - _, line, err := socket.ReadMessage() - if err != nil { - output.Fatal(err) - } - fmt.Print(string(line)) - } } }, } @@ -363,16 +286,10 @@ func (root *HostCmd) attachPruneCmd() { Short: "Prune Docker assets and images on your remote", Long: `Prunes Docker assets and images from your remote to free up storage space.`, Run: func(cmd *cobra.Command, args []string) { - resp, err := root.client.Prune() - if err != nil { - output.Fatal(err) - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { + if err := root.client.Prune(root.ctx); err != nil { output.Fatal(err) } - fmt.Printf("(Status code %d) %s\n", resp.StatusCode, body) + fmt.Printf("docker assets have been pruned") }, } root.AddCommand(prune) @@ -485,25 +402,10 @@ func (root *HostCmd) newResetCmd() { On this remote, this kills all active containers and clears the project directory, allowing you to assign a different Inertia project to this remote.`, Run: func(cmd *cobra.Command, args []string) { - resp, err := root.client.Reset() - if err != nil { + if err := root.client.Reset(root.ctx); err != nil { output.Fatal(err) } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - output.Fatal(err) - } - - switch resp.StatusCode { - case http.StatusOK: - fmt.Printf("(Status code %d) %s\n", resp.StatusCode, body) - case http.StatusUnauthorized: - fmt.Printf("(Status code %d) Bad auth: %s\n", resp.StatusCode, body) - default: - fmt.Printf("(Status code %d) Unknown response from daemon: %s\n", - resp.StatusCode, body) - } + fmt.Printf("project on remote '%s' successfully reset\n", root.remote) }, } root.AddCommand(reset) @@ -519,8 +421,7 @@ directory (~/inertia) from your remote host.`, println("WARNING: This will stop down your project and remove the Inertia daemon.") println("This is irreversible. Continue? (y/n)") var response string - _, err := fmt.Scanln(&response) - if err != nil || response != "y" { + if _, err := fmt.Scanln(&response); err != nil || response != "y" { output.Fatal("aborting") } @@ -531,7 +432,7 @@ directory (~/inertia) from your remote host.`, // Daemon down println("Stopping project...") - if _, err = root.client.Down(); err != nil { + if err = root.client.Down(root.ctx); err != nil { output.Fatal(err) } println("Stopping daemon...") @@ -554,27 +455,11 @@ func (root *HostCmd) attachTokenCmd() { Short: "Generate tokens associated with permission levels for admin to share.", Long: `Generate tokens associated with permission levels for team leads to share`, Run: func(cmd *cobra.Command, args []string) { - resp, err := root.client.Token() + token, err := root.client.Token(root.ctx) if err != nil { output.Fatal(err) } - defer resp.Body.Close() - - var token string - b, err := api.Unmarshal(resp.Body, api.KV{Key: "token", Value: &token}) - if err != nil { - output.Fatal(err) - } - - switch resp.StatusCode { - case http.StatusOK: - fmt.Printf("New token: %s\n", token) - case http.StatusUnauthorized: - fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, b.Message) - default: - fmt.Printf("(Status code %d) Unknown response from daemon:\n%s\n", - resp.StatusCode, b.Message) - } + println(token) }, } root.AddCommand(token) diff --git a/cmd/remotes/user.go b/cmd/remotes/user.go index 447d24e7..a3126636 100644 --- a/cmd/remotes/user.go +++ b/cmd/remotes/user.go @@ -1,16 +1,15 @@ package remotescmd import ( + "context" "fmt" - "io/ioutil" - "net/http" "strings" "syscall" + "github.com/ubclaunchpad/inertia/client" "github.com/ubclaunchpad/inertia/local" "github.com/spf13/cobra" - "github.com/ubclaunchpad/inertia/api" "github.com/ubclaunchpad/inertia/cmd/core/utils/output" "golang.org/x/crypto/ssh/terminal" ) @@ -45,6 +44,12 @@ func AttachUserCmd(host *HostCmd) { host.AddCommand(user.Command) } +// context returns the root host command's context +func (root *UserCmd) context() context.Context { return root.host.ctx } + +// getUserClient returns the root host command's user client +func (root *UserCmd) getUserClient() *client.UserClient { return root.host.client.GetUserClient() } + func (root *UserCmd) attachAddCmd() { const flagAdmin = "admin" var add = &cobra.Command{ @@ -67,25 +72,9 @@ Use the --admin flag to create an admin user.`, fmt.Print("\n") var admin, _ = cmd.Flags().GetBool(flagAdmin) - resp, err := root.host.client.AddUser(args[0], password, admin) - if err != nil { - output.Fatal(err) - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { + if err := root.getUserClient().AddUser(root.context(), args[0], password, admin); err != nil { output.Fatal(err) } - - switch resp.StatusCode { - case http.StatusCreated: - fmt.Printf("(Status code %d) User added!\n", resp.StatusCode) - case http.StatusUnauthorized: - fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, body) - default: - fmt.Printf("(Status code %d) Unknown response from daemon:\n%s\n", - resp.StatusCode, body) - } }, } add.Flags().Bool(flagAdmin, false, "create a user with administrator permissions") @@ -102,24 +91,10 @@ This user will no longer be able to log in and view or configure the deployment remotely.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - resp, err := root.host.client.RemoveUser(args[0]) - if err != nil { - output.Fatal(err) - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { + if err := root.getUserClient().RemoveUser(root.context(), args[0]); err != nil { output.Fatal(err) } - switch resp.StatusCode { - case http.StatusOK: - fmt.Printf("(Status code %d) User removed.\n", resp.StatusCode) - case http.StatusUnauthorized: - fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, body) - default: - fmt.Printf("(Status code %d) Unknown response from daemon:\n%s\n", - resp.StatusCode, body) - } + println("user has been removed") }, } root.AddCommand(remove) @@ -141,12 +116,17 @@ func (root *UserCmd) attachLoginCmd() { } var totp, _ = cmd.Flags().GetString("totp") - resp, err := root.host.client.LogIn(username, string(pwBytes), totp) - if err != nil { + var req = client.AuthenticateRequest{ + User: username, + Password: string(pwBytes), + TOTP: totp, + } + token, err := root.getUserClient().Authenticate(root.context(), req) + if err != nil && err != client.ErrNeedTotp { output.Fatal(err) } - if resp.StatusCode == http.StatusExpectationFailed { + if err == client.ErrNeedTotp { // a TOTP is required fmt.Print("Authentication code (or backup code): ") totpBytes, err := terminal.ReadPassword(int(syscall.Stdin)) @@ -154,29 +134,19 @@ func (root *UserCmd) attachLoginCmd() { if err != nil { output.Fatal(err) } - resp, err = root.host.client.LogIn(username, string(pwBytes), string(totpBytes)) + req.TOTP = string(totpBytes) + token, err = root.getUserClient().Authenticate(root.context(), req) if err != nil { output.Fatal(err) } } - fmt.Printf("(Status code %d) ", resp.StatusCode) - if resp.StatusCode != http.StatusOK { - fmt.Println("Invalid credentials") - return - } - defer resp.Body.Close() - var token string - if api.Unmarshal(resp.Body, api.KV{Key: "token", Value: &token}); err != nil { - output.Fatal(err) - } - - root.host.client.Remote.Daemon.Token = string(token) + root.host.getRemote().Daemon.Token = token if err = local.SaveRemote(root.host.getRemote()); err != nil { output.Fatal(err) } - fmt.Println("You have been logged in successfully.") + fmt.Println("you have been logged in successfully, and a token has been saved") }, } login.Flags().String("totp", "", "auth code or backup code for 2FA") @@ -191,26 +161,10 @@ func (root *UserCmd) attachResetCmd() { will no longer be able to log in and view or configure the deployment remotely.`, Run: func(cmd *cobra.Command, args []string) { - resp, err := root.host.client.ResetUsers() - if err != nil { - output.Fatal(err) - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { + if err := root.getUserClient().ResetUsers(root.context()); err != nil { output.Fatal(err) } - - switch resp.StatusCode { - case http.StatusOK: - fmt.Printf("(Status code %d) All users removed.\n", resp.StatusCode) - case http.StatusUnauthorized: - fmt.Printf("(Status code %d) Bad auth:\n%s\n", resp.StatusCode, body) - default: - fmt.Printf("(Status code %d) Unknown response from daemon:\n%s\n", - resp.StatusCode, body) - } + println("all users removed") }, } root.AddCommand(reset) @@ -222,28 +176,11 @@ func (root *UserCmd) attachListCmd() { Short: "List all users registered on your remote.", Long: `Lists all users registered in Inertia's user database.`, Run: func(cmd *cobra.Command, args []string) { - resp, err := root.host.client.ListUsers() - if err != nil { - output.Fatal(err) - } - defer resp.Body.Close() - - var users = make([]string, 0) - b, err := api.Unmarshal(resp.Body, api.KV{Key: "users", Value: &users}) + users, err := root.getUserClient().ListUsers(root.context()) if err != nil { output.Fatal(err) } - - switch resp.StatusCode { - case http.StatusOK: - fmt.Printf("(Status code %d) %s:\n%s", resp.StatusCode, b.Message, - strings.Join(users, "\n")) - case http.StatusUnauthorized: - fmt.Printf("(Status code %d) Bad auth: %s\n", resp.StatusCode, b.Error()) - default: - fmt.Printf("(Status code %d) Unknown response from daemon: %s\n", - resp.StatusCode, b.Error()) - } + println(strings.Join(users, "\n")) }, } root.AddCommand(list) diff --git a/cmd/remotes/user_totp.go b/cmd/remotes/user_totp.go index 0dc5cd26..663a6abf 100644 --- a/cmd/remotes/user_totp.go +++ b/cmd/remotes/user_totp.go @@ -1,15 +1,15 @@ package remotescmd import ( + "context" "fmt" - "net/http" "syscall" qr "github.com/Baozisoftware/qrcode-terminal-go" "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" - "github.com/ubclaunchpad/inertia/api" + "github.com/ubclaunchpad/inertia/client" "github.com/ubclaunchpad/inertia/cmd/core/utils/output" ) @@ -39,6 +39,11 @@ func AttachTotpCmd(root *UserCmd) { root.AddCommand(totp.Command) } +// context returns the root host command's context +func (root *UserTotpCmd) context() context.Context { return root.host.ctx } + +func (root *UserTotpCmd) getUserClient() *client.UserClient { return root.host.client.GetUserClient() } + func (root *UserTotpCmd) attachEnableCmd() { var enable = &cobra.Command{ Use: "enable [user]", @@ -55,29 +60,17 @@ func (root *UserTotpCmd) attachEnableCmd() { } // Endpoint handles user authentication before enabling Totp - resp, err := root.host.client.EnableTotp(username, string(pwBytes)) - if err != nil { - output.Fatal(err) - } - if resp.StatusCode != http.StatusOK { - fmt.Printf("(Status code %d) Error Enabling Totp.", resp.StatusCode) - return - } - defer resp.Body.Close() - - var totpInfo api.TotpResponse - b, err := api.Unmarshal(resp.Body, api.KV{Key: "totp", Value: &totpInfo}) + totpInfo, err := root.getUserClient().EnableTotp(root.context(), username, string(pwBytes)) if err != nil { output.Fatal(err) } // Display QR code so users can easily add their keys to their // authenticator apps + println("2FA has been enabled!") + qr.New().Get(fmt.Sprintf("otpauth://totp/%s?secret=%s&issuer=Inertia", username, totpInfo.TotpSecret)).Print() - - fmt.Printf("\n\n(Status code %d) %s\n", - resp.StatusCode, b.Message) fmt.Print("Scan the QR code above to " + "add your Inertia account to your authenticator app.\n\n") fmt.Printf("Your secret key is: %s\n", totpInfo.TotpSecret) @@ -103,20 +96,10 @@ func (root *UserTotpCmd) attachDisableCmd() { Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { // Endpoint handles user authentication before disabling Totp - resp, err := root.host.client.DisableTotp() - if err != nil { + if err := root.getUserClient().DisableTotp(root.context()); err != nil { output.Fatal(err) } - - fmt.Printf("(Status code %d) ", resp.StatusCode) - if resp.StatusCode == http.StatusUnauthorized { - fmt.Println("Please try logging in again before " + - "disabling two-factor authentication.") - } else if resp.StatusCode != http.StatusOK { - fmt.Println("Error Disabling Totp.") - } else { - fmt.Println("Totp successfully disabled.") - } + println("2FA successfully disabled") }, } root.AddCommand(disable) From adf6d05328d96f3ab27a942c07374ea8d83dc5c3 Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 20:25:31 -0800 Subject: [PATCH 04/13] client: fix debug toggle --- client/client.go | 2 +- client/client_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/client.go b/client/client.go index cda5eece..c514fc41 100644 --- a/client/client.go +++ b/client/client.go @@ -60,7 +60,7 @@ func NewClient(remote *cfg.Remote, opts Options) *Client { func (c *Client) WithWriter(out io.Writer) { c.out = out } // WithDebug sets the client's debug mode -func (c *Client) WithDebug(debug bool) { c.debug = true } +func (c *Client) WithDebug(debug bool) { c.debug = debug } // GetSSHClient instantiates an SSH client for Inertia-related commands func (c *Client) GetSSHClient() (*SSHClient, error) { diff --git a/client/client_test.go b/client/client_test.go index 91372ca4..41537f40 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -86,7 +86,9 @@ func TestNewClient(t *testing.T) { var c = NewClient(&cfg.Remote{}, Options{}) assert.NotNil(t, c) c.WithDebug(false) + assert.False(t, c.debug) c.WithWriter(os.Stdout) + assert.Equal(t, os.Stdout, c.out) } func TestClient_Up(t *testing.T) { From c95d12a1a718940f8bb3c3d8e17af217f1eeab1c Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 20:36:57 -0800 Subject: [PATCH 05/13] client: improve websocket logs --- client/client.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/client.go b/client/client.go index c514fc41..c78fea5f 100644 --- a/client/client.go +++ b/client/client.go @@ -19,7 +19,6 @@ import ( "github.com/ubclaunchpad/inertia/api" "github.com/ubclaunchpad/inertia/cfg" "github.com/ubclaunchpad/inertia/client/runner" - "github.com/ubclaunchpad/inertia/cmd/core/utils/output" "github.com/ubclaunchpad/inertia/common" ) @@ -245,14 +244,16 @@ func (c *Client) Logs(ctx context.Context, req LogsRequest) ([]string, error) { resp, err := c.get(ctx, "/logs", reqContent) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to make request: %s", err.Error()) } + var logs = make([]string, 0) b, err := c.unmarshal(resp.Body, api.KV{Key: "logs", Value: &logs}) resp.Body.Close() if err != nil { - output.Fatal(err) + return nil, fmt.Errorf("failed to read response: %s", err.Error()) } + return logs, b.Error() } @@ -284,15 +285,18 @@ func (c *Client) LogsWithOutput(ctx context.Context, req LogsRequest) error { header.Set("Authorization", "Bearer "+c.Remote.Daemon.Token) // set up websocket connection + c.debugf("request constructed: %s (authorized: %v, verified: %v)", + url.String(), c.Remote.Daemon.Token != "", c.Remote.Daemon.VerifySSL) socket, resp, err := buildWebSocketDialer(c.Remote.Daemon.VerifySSL). DialContext(ctx, url.String(), header) if err == websocket.ErrBadHandshake { return fmt.Errorf("websocket handshake failed with status %d", resp.StatusCode) } if err != nil { - return fmt.Errorf("failed to connect to daemon at %s: %s", url.Host, err.Error()) + return fmt.Errorf("failed to connect to daemon: %s", err.Error()) } defer socket.Close() + c.debugf("websocket connection established") // read from socket until error var errC = make(chan error, 1) @@ -310,8 +314,10 @@ func (c *Client) LogsWithOutput(ctx context.Context, req LogsRequest) error { for { select { case <-ctx.Done(): + c.debugf("context cancelled, closing connection") return nil case err := <-errC: + c.debugf("error received: %s", err.Error()) return err } } From b71a13a44f8fc74a154b1ba437238af68cbf20dc Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 20:49:13 -0800 Subject: [PATCH 06/13] client: add debugging output to SSH client, improve SSH client returns --- client/bootstrap/bootstrap.go | 2 +- client/client.go | 2 ++ client/client_test.go | 22 --------------- client/sshc.go | 53 +++++++++++++++++++++++++++-------- client/sshc_test.go | 22 +++++++++++++++ client/users.go | 4 +-- 6 files changed, 68 insertions(+), 37 deletions(-) diff --git a/client/bootstrap/bootstrap.go b/client/bootstrap/bootstrap.go index 55f57fbe..be4203ee 100644 --- a/client/bootstrap/bootstrap.go +++ b/client/bootstrap/bootstrap.go @@ -53,7 +53,7 @@ Use 'inertia %s logs' to check on the daemon's setup progress. // Output deploy key to user fmt.Fprintf(out, ">> GitHub Deploy Key (add to https://www.github.com/%s/settings/keys/new):\n", repo) - fmt.Fprint(out, pub.String()+"\n") + fmt.Fprint(out, pub+"\n") // Output Webhook url to user var addr, _ = c.Remote.DaemonAddr() diff --git a/client/client.go b/client/client.go index c78fea5f..af1d288c 100644 --- a/client/client.go +++ b/client/client.go @@ -69,6 +69,8 @@ func (c *Client) GetSSHClient() (*SSHClient, error) { return &SSHClient{ ssh: c.ssh, remote: c.Remote, + debug: c.debug, + out: c.out, }, nil } diff --git a/client/client_test.go b/client/client_test.go index 41537f40..e39443a6 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/ubclaunchpad/inertia/api" "github.com/ubclaunchpad/inertia/cfg" - "github.com/ubclaunchpad/inertia/client/runner/mocks" "github.com/ubclaunchpad/inertia/daemon/inertiad/res" ) @@ -61,27 +60,6 @@ func newMockClient(t *testing.T, ts *httptest.Server) *Client { } } -func newMockSSHClient(t *testing.T, m *mocks.FakeSSHSession) *Client { - return &Client{ - ssh: m, - out: &testWriter{t}, - debug: true, - - Remote: &cfg.Remote{ - Version: "test", - IP: "127.0.0.1", - SSH: &cfg.SSH{ - IdentityFile: "../test/keys/id_rsa", - User: "root", - SSHPort: "69", - }, - Daemon: &cfg.Daemon{ - Port: "4303", - }, - }, - } -} - func TestNewClient(t *testing.T) { var c = NewClient(&cfg.Remote{}, Options{}) assert.NotNil(t, c) diff --git a/client/sshc.go b/client/sshc.go index 009fbb99..8a1857d7 100644 --- a/client/sshc.go +++ b/client/sshc.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "io" "strings" "github.com/ubclaunchpad/inertia/cfg" @@ -15,6 +16,9 @@ import ( type SSHClient struct { remote *cfg.Remote ssh runner.SSHSession + + out io.Writer + debug bool } // GetRunner returns the SSH client's underlying session @@ -38,9 +42,11 @@ func (s *SSHClient) DaemonDown() error { return err } - _, stderr, err := s.ssh.Run(string(scriptBytes)) + stdout, stderr, err := s.ssh.Run(string(scriptBytes)) + s.debugStdout("token.sh", stdout) + s.debugStderr("token.sh", stderr) if err != nil { - return fmt.Errorf("daemon shutdown failed: %s: %s", err.Error(), stderr.String()) + return fmt.Errorf("daemon shutdown failed: %s", err.Error()) } return nil @@ -64,23 +70,25 @@ func (s *SSHClient) InstallDocker() error { // GenerateKeys creates a public-private key-pair on the remote vps and returns // the public key. -func (s *SSHClient) GenerateKeys() (*bytes.Buffer, error) { +func (s *SSHClient) GenerateKeys() (string, error) { if s.ssh == nil { - return nil, errors.New("client not configured for SSH access") + return "", errors.New("client not configured for SSH access") } scriptBytes, err := internal.ReadFile("client/scripts/keygen.sh") if err != nil { - return nil, err + return "", err } // Create deploy key. - result, stderr, err := s.ssh.Run(string(scriptBytes)) + stdout, stderr, err := s.ssh.Run(string(scriptBytes)) + s.debugStdout("token.sh", stdout) + s.debugStderr("token.sh", stderr) if err != nil { - return nil, fmt.Errorf("key generation failed: %s: %s", err.Error(), stderr.String()) + return "", fmt.Errorf("key generation failed: %s", err.Error()) } - return result, nil + return stdout.String(), nil } // AssignAPIToken generates an API token and assigns it to client.Remote @@ -91,8 +99,10 @@ func (s *SSHClient) AssignAPIToken() error { } daemonCmdStr := fmt.Sprintf(string(scriptBytes), s.remote.Version) stdout, stderr, err := s.ssh.Run(daemonCmdStr) + s.debugStdout("token.sh", stdout) + s.debugStderr("token.sh", stderr) if err != nil { - return fmt.Errorf("api token generation failed: %s: %s", err.Error(), stderr.String()) + return fmt.Errorf("api token generation failed: %s", err.Error()) } // There may be a newline, remove it. @@ -107,10 +117,31 @@ func (s *SSHClient) UninstallInertia() error { return err } - _, stderr, err := s.ssh.Run(string(scriptBytes)) + stdout, stderr, err := s.ssh.Run(string(scriptBytes)) + s.debugStdout("inertia-down.sh", stdout) + s.debugStderr("inertia-down.sh", stderr) if err != nil { - return fmt.Errorf("Inertia down failed: %s: %s", err.Error(), stderr.String()) + return fmt.Errorf("inertia shutdown failed: %s", err.Error()) } return nil } + +// debugf logs to the client's output if debug is enabled +func (s *SSHClient) debugf(format string, args ...interface{}) { + if s.debug { + fmt.Fprintf(s.out, "DEBUG: "+format+"\n", args...) + } +} + +func (s *SSHClient) debugStderr(script string, out *bytes.Buffer) { + if out != nil && out.Len() > 0 { + s.debugf("%s stderr:\n>>>\n%s\n<<<", script, out.String()) + } +} + +func (s *SSHClient) debugStdout(script string, out *bytes.Buffer) { + if out != nil && out.Len() > 0 { + s.debugf("%s stdout:\n>>>\n%s\n<<<", script, out.String()) + } +} diff --git a/client/sshc_test.go b/client/sshc_test.go index 04a7db1c..f3700996 100644 --- a/client/sshc_test.go +++ b/client/sshc_test.go @@ -6,9 +6,31 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/ubclaunchpad/inertia/cfg" "github.com/ubclaunchpad/inertia/client/runner/mocks" ) +func newMockSSHClient(t *testing.T, m *mocks.FakeSSHSession) *Client { + return &Client{ + ssh: m, + out: &testWriter{t}, + debug: true, + + Remote: &cfg.Remote{ + Version: "test", + IP: "127.0.0.1", + SSH: &cfg.SSH{ + IdentityFile: "../test/keys/id_rsa", + User: "root", + SSHPort: "69", + }, + Daemon: &cfg.Daemon{ + Port: "4303", + }, + }, + } +} + func TestSSHClient_InstallDocker(t *testing.T) { var session = &mocks.FakeSSHSession{} var client = newMockSSHClient(t, session) diff --git a/client/users.go b/client/users.go index cf3ce1a9..eed48ffc 100644 --- a/client/users.go +++ b/client/users.go @@ -20,9 +20,7 @@ type UserClient struct { } // NewUserClient instantiates a new client for user management functions -func NewUserClient(c *Client) *UserClient { - return &UserClient{c} -} +func NewUserClient(c *Client) *UserClient { return &UserClient{c} } // AuthenticateRequest denotes options for authenticating with the Inertia daemon type AuthenticateRequest struct { From 3783d4d2702925bcf4aeb5d7cb1736d33a74369a Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 20:54:49 -0800 Subject: [PATCH 07/13] client: account for debug output in test --- client/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client_test.go b/client/client_test.go index e39443a6..6da6350f 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -262,7 +262,7 @@ func TestClient_LogsWithOutput(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(1 * time.Second) - assert.Equal(t, "hello world", buf.String()) + assert.Contains(t, buf.String(), "hello world") t.Log("message received!") cancel() }() From eb367664d50fd11b557110825aaba605fbd5f964 Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 21:32:07 -0800 Subject: [PATCH 08/13] client: make output thread-safe --- client/client.go | 7 ++++++- client/sshc.go | 10 +++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/client/client.go b/client/client.go index af1d288c..b1e5e09e 100644 --- a/client/client.go +++ b/client/client.go @@ -13,6 +13,7 @@ import ( "path" "strconv" "strings" + "sync" "github.com/gorilla/websocket" @@ -24,7 +25,9 @@ import ( // Client manages a deployment type Client struct { - out io.Writer + om sync.Mutex + out io.Writer + ssh runner.SSHSession debug bool @@ -450,6 +453,8 @@ func (c *Client) unmarshal(r io.Reader, kvs ...api.KV) (*api.BaseResponse, error // debugf logs to the client's output if debug is enabled func (c *Client) debugf(format string, args ...interface{}) { if c.debug { + c.om.Lock() fmt.Fprintf(c.out, "DEBUG: "+format+"\n", args...) + c.om.Unlock() } } diff --git a/client/sshc.go b/client/sshc.go index 8a1857d7..39acf72f 100644 --- a/client/sshc.go +++ b/client/sshc.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "strings" + "sync" "github.com/ubclaunchpad/inertia/cfg" internal "github.com/ubclaunchpad/inertia/client/internal" @@ -14,11 +15,12 @@ import ( // SSHClient implements Inertia's SSH commands type SSHClient struct { - remote *cfg.Remote - ssh runner.SSHSession - + om sync.Mutex out io.Writer debug bool + + remote *cfg.Remote + ssh runner.SSHSession } // GetRunner returns the SSH client's underlying session @@ -130,7 +132,9 @@ func (s *SSHClient) UninstallInertia() error { // debugf logs to the client's output if debug is enabled func (s *SSHClient) debugf(format string, args ...interface{}) { if s.debug { + s.om.Lock() fmt.Fprintf(s.out, "DEBUG: "+format+"\n", args...) + s.om.Unlock() } } From c9674e2cc3648c2a769dfa6f0d83df60783c6151 Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 22:03:12 -0800 Subject: [PATCH 09/13] client: fix race conditions and improve all streaming functions --- client/client.go | 30 +++++++++++++++++++++++------- client/client_test.go | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/client/client.go b/client/client.go index b1e5e09e..a19ccca2 100644 --- a/client/client.go +++ b/client/client.go @@ -132,17 +132,30 @@ func (c *Client) UpWithOutput(ctx context.Context, req UpRequest) error { } defer resp.Body.Close() - var reader = bufio.NewReader(resp.Body) + // read until error + var scan = bufio.NewScanner(resp.Body) + var errC = make(chan error, 1) + go func() { + for scan.Scan() { + c.om.Lock() + fmt.Fprintln(c.out, scan.Text()) + c.om.Unlock() + } + if err := scan.Err(); err != nil { + errC <- fmt.Errorf("error occured while reading output: %s", err.Error()) + return + } + }() + + // block until done for { select { case <-ctx.Done(): + c.debugf("context cancelled, closing connection") return nil - default: - line, err := reader.ReadBytes('\n') - if err != nil { - return fmt.Errorf("error occured while reading output: %s", err.Error()) - } - fmt.Fprint(c.out, string(line)) + case err := <-errC: + c.debugf("error received: %s", err.Error()) + return err } } } @@ -310,8 +323,11 @@ func (c *Client) LogsWithOutput(ctx context.Context, req LogsRequest) error { _, line, err := socket.ReadMessage() if err != nil { errC <- fmt.Errorf("error occured while reading from socket: %s", err.Error()) + return } + c.om.Lock() fmt.Fprint(c.out, string(line)) + c.om.Unlock() } }() diff --git a/client/client_test.go b/client/client_test.go index 6da6350f..7dadfec9 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -105,6 +106,35 @@ func TestClient_Up(t *testing.T) { }})) } +func TestClient_UpWithOutput(t *testing.T) { + testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "hello") + time.Sleep(10 * time.Millisecond) + fmt.Fprintln(w, "world") + time.Sleep(10 * time.Millisecond) + fmt.Fprintln(w, "chicken rice") + time.Sleep(10 * time.Millisecond) + })) + defer testServer.Close() + + var d = newMockClient(t, testServer) + var buf = &bytes.Buffer{} + d.out = buf + ctx, cancel := context.WithCancel(context.Background()) + go func() { + // wait before closing the connection to make sure messages arrive + time.Sleep(1 * time.Second) + cancel() + }() + assert.NoError(t, d.UpWithOutput(ctx, UpRequest{"test_project", "myremote.git", cfg.Profile{ + Build: &cfg.Build{ + Type: cfg.DockerCompose, + }, + }})) + assert.Contains(t, buf.String(), "hello\nworld") + assert.Contains(t, buf.String(), "chicken rice") +} + func TestClient_Prune(t *testing.T) { testServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -261,12 +291,12 @@ func TestClient_LogsWithOutput(t *testing.T) { d.out = buf ctx, cancel := context.WithCancel(context.Background()) go func() { + // wait before closing the connection to make sure message arrives time.Sleep(1 * time.Second) - assert.Contains(t, buf.String(), "hello world") - t.Log("message received!") cancel() }() assert.NoError(t, d.LogsWithOutput(ctx, LogsRequest{"docker-compose", 10})) + assert.Contains(t, buf.String(), "hello world") }) t.Run("daemon offline", func(t *testing.T) { From bd361266e6ef0a9dfebad5d877e5a0379c9e5e81 Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 22:18:27 -0800 Subject: [PATCH 10/13] client: fix debug script names --- client/sshc.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/sshc.go b/client/sshc.go index 39acf72f..1f090848 100644 --- a/client/sshc.go +++ b/client/sshc.go @@ -45,8 +45,8 @@ func (s *SSHClient) DaemonDown() error { } stdout, stderr, err := s.ssh.Run(string(scriptBytes)) - s.debugStdout("token.sh", stdout) - s.debugStderr("token.sh", stderr) + s.debugStdout("daemon-down.sh", stdout) + s.debugStderr("daemon-down.sh", stderr) if err != nil { return fmt.Errorf("daemon shutdown failed: %s", err.Error()) } @@ -84,8 +84,8 @@ func (s *SSHClient) GenerateKeys() (string, error) { // Create deploy key. stdout, stderr, err := s.ssh.Run(string(scriptBytes)) - s.debugStdout("token.sh", stdout) - s.debugStderr("token.sh", stderr) + s.debugStdout("keygen.sh", stdout) + s.debugStderr("keygen.sh", stderr) if err != nil { return "", fmt.Errorf("key generation failed: %s", err.Error()) } From b80b7c2d1129ed02f39663b8059fc61422d215bf Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Tue, 26 Feb 2019 22:23:48 -0800 Subject: [PATCH 11/13] docs: regenerate tip documentation --- docs/cli/inertia_${remote_name}_down.md | 2 +- docs/cli/inertia_${remote_name}_env.md | 2 +- docs/cli/inertia_${remote_name}_env_ls.md | 2 +- docs/cli/inertia_${remote_name}_env_rm.md | 2 +- docs/cli/inertia_${remote_name}_env_set.md | 2 +- docs/cli/inertia_${remote_name}_init.md | 2 +- docs/cli/inertia_${remote_name}_logs.md | 2 +- docs/cli/inertia_${remote_name}_send.md | 32 --------------- docs/cli/inertia_${remote_name}_status.md | 2 +- docs/cli/inertia_${remote_name}_up.md | 12 +++--- docs/cli/inertia_${remote_name}_user.md | 4 +- docs/cli/inertia_${remote_name}_user_add.md | 2 +- docs/cli/inertia_${remote_name}_user_login.md | 2 +- docs/cli/inertia_${remote_name}_user_ls.md | 2 +- docs/cli/inertia_${remote_name}_user_reset.md | 2 +- docs/cli/inertia_${remote_name}_user_rm.md | 2 +- docs/cli/inertia_${remote_name}_user_totp.md | 6 +-- ...nertia_${remote_name}_user_totp_disable.md | 4 +- ...inertia_${remote_name}_user_totp_enable.md | 4 +- docs/cli/inertia_init.md | 23 ++++++++--- docs/cli/inertia_project.md | 31 ++++++++++++++ docs/cli/inertia_project_profile.md | 28 +++++++++++++ docs/cli/inertia_project_profile_apply.md | 32 +++++++++++++++ docs/cli/inertia_project_profile_ls.md | 29 ++++++++++++++ docs/cli/inertia_project_profile_set.md | 40 +++++++++++++++++++ docs/cli/inertia_project_profile_show.md | 29 ++++++++++++++ docs/cli/inertia_project_reset.md | 29 ++++++++++++++ docs/cli/inertia_project_set.md | 28 +++++++++++++ docs/cli/inertia_remote.md | 1 + docs/cli/inertia_remote_add.md | 18 +++++++-- docs/cli/inertia_remote_rm.md | 6 +++ docs/cli/inertia_remote_show.md | 6 +++ docs/cli/inertia_remote_upgrade.md | 36 +++++++++++++++++ docs/tip/api/index.html | 16 +++----- docs/tip/api/swagger.yml | 30 ++++++++++---- docs/tip/cli/README.md | 4 +- docs/tip/cli/inertia_${remote_name}.md | 1 + docs/tip/cli/inertia_${remote_name}_down.md | 1 + docs/tip/cli/inertia_${remote_name}_env.md | 1 + docs/tip/cli/inertia_${remote_name}_env_ls.md | 1 + docs/tip/cli/inertia_${remote_name}_env_rm.md | 1 + .../tip/cli/inertia_${remote_name}_env_set.md | 1 + docs/tip/cli/inertia_${remote_name}_init.md | 1 + docs/tip/cli/inertia_${remote_name}_logs.md | 1 + docs/tip/cli/inertia_${remote_name}_prune.md | 1 + docs/tip/cli/inertia_${remote_name}_send.md | 1 + docs/tip/cli/inertia_${remote_name}_ssh.md | 1 + docs/tip/cli/inertia_${remote_name}_status.md | 1 + docs/tip/cli/inertia_${remote_name}_token.md | 1 + .../cli/inertia_${remote_name}_uninstall.md | 1 + docs/tip/cli/inertia_${remote_name}_up.md | 1 + .../tip/cli/inertia_${remote_name}_upgrade.md | 1 + docs/tip/cli/inertia_${remote_name}_user.md | 1 + .../cli/inertia_${remote_name}_user_add.md | 1 + .../cli/inertia_${remote_name}_user_login.md | 1 + .../tip/cli/inertia_${remote_name}_user_ls.md | 1 + .../cli/inertia_${remote_name}_user_reset.md | 1 + .../tip/cli/inertia_${remote_name}_user_rm.md | 1 + .../cli/inertia_${remote_name}_user_totp.md | 1 + ...nertia_${remote_name}_user_totp_disable.md | 1 + ...inertia_${remote_name}_user_totp_enable.md | 1 + docs/tip/cli/inertia_remote_upgrade.md | 2 +- 62 files changed, 411 insertions(+), 90 deletions(-) create mode 100644 docs/cli/inertia_project.md create mode 100644 docs/cli/inertia_project_profile.md create mode 100644 docs/cli/inertia_project_profile_apply.md create mode 100644 docs/cli/inertia_project_profile_ls.md create mode 100644 docs/cli/inertia_project_profile_set.md create mode 100644 docs/cli/inertia_project_profile_show.md create mode 100644 docs/cli/inertia_project_reset.md create mode 100644 docs/cli/inertia_project_set.md create mode 100644 docs/cli/inertia_remote_upgrade.md diff --git a/docs/cli/inertia_${remote_name}_down.md b/docs/cli/inertia_${remote_name}_down.md index bea1844d..f72320c2 100644 --- a/docs/cli/inertia_${remote_name}_down.md +++ b/docs/cli/inertia_${remote_name}_down.md @@ -22,8 +22,8 @@ inertia ${remote_name} down [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_env.md b/docs/cli/inertia_${remote_name}_env.md index 06457b37..b95194b6 100644 --- a/docs/cli/inertia_${remote_name}_env.md +++ b/docs/cli/inertia_${remote_name}_env.md @@ -23,8 +23,8 @@ as follows: ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_env_ls.md b/docs/cli/inertia_${remote_name}_env_ls.md index 71c317d5..f53819a1 100644 --- a/docs/cli/inertia_${remote_name}_env_ls.md +++ b/docs/cli/inertia_${remote_name}_env_ls.md @@ -21,8 +21,8 @@ inertia ${remote_name} env ls [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_env_rm.md b/docs/cli/inertia_${remote_name}_env_rm.md index 4054e021..5ee2954d 100644 --- a/docs/cli/inertia_${remote_name}_env_rm.md +++ b/docs/cli/inertia_${remote_name}_env_rm.md @@ -21,8 +21,8 @@ inertia ${remote_name} env rm [name] [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_env_set.md b/docs/cli/inertia_${remote_name}_env_set.md index 91d075ef..53d30abb 100644 --- a/docs/cli/inertia_${remote_name}_env_set.md +++ b/docs/cli/inertia_${remote_name}_env_set.md @@ -22,8 +22,8 @@ inertia ${remote_name} env set [name] [value] [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_init.md b/docs/cli/inertia_${remote_name}_init.md index 4754d97e..bb1d970f 100644 --- a/docs/cli/inertia_${remote_name}_init.md +++ b/docs/cli/inertia_${remote_name}_init.md @@ -29,8 +29,8 @@ inertia ${remote_name} init [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_logs.md b/docs/cli/inertia_${remote_name}_logs.md index ec1d1a6d..4ccf5fca 100644 --- a/docs/cli/inertia_${remote_name}_logs.md +++ b/docs/cli/inertia_${remote_name}_logs.md @@ -25,8 +25,8 @@ inertia ${remote_name} logs [container] [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_send.md b/docs/cli/inertia_${remote_name}_send.md index 45c510bd..e69de29b 100644 --- a/docs/cli/inertia_${remote_name}_send.md +++ b/docs/cli/inertia_${remote_name}_send.md @@ -1,32 +0,0 @@ -## inertia ${remote_name} send - -Send a file to your Inertia deployment - -### Synopsis - -Sends a file, such as a configuration or .env file, to your Inertia deployment. - -``` -inertia ${remote_name} send [filepath] [flags] -``` - -### Options - -``` - -d, --dest string path relative from project root to send file to - -h, --help help for send - -p, --perm string permissions settings to create file with (default "0655") -``` - -### Options inherited from parent commands - -``` - --config string specify relative path to Inertia configuration (default "inertia.toml") - -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate -``` - -### SEE ALSO - -* [inertia ${remote_name}](inertia_${remote_name}.md) - Configure deployment to ${remote_name} - diff --git a/docs/cli/inertia_${remote_name}_status.md b/docs/cli/inertia_${remote_name}_status.md index dda5be48..94bc4da6 100644 --- a/docs/cli/inertia_${remote_name}_status.md +++ b/docs/cli/inertia_${remote_name}_status.md @@ -22,8 +22,8 @@ inertia ${remote_name} status [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_up.md b/docs/cli/inertia_${remote_name}_up.md index 50642af1..17dd668a 100644 --- a/docs/cli/inertia_${remote_name}_up.md +++ b/docs/cli/inertia_${remote_name}_up.md @@ -4,9 +4,11 @@ Bring project online on remote ### Synopsis -Builds and deploy your project on your remote. +Builds and deploy your project on your remote using your project's +default profile, or a profile you have applied using 'inertia project profile apply'. -This requires an Inertia daemon to be active on your remote - do this by running 'inertia [remote] init' +This requires an Inertia daemon to be active on your remote - do this by running +'inertia [remote] init'. ``` inertia ${remote_name} up [flags] @@ -15,16 +17,16 @@ inertia ${remote_name} up [flags] ### Options ``` - -h, --help help for up - --type string override configured build method for your project + -h, --help help for up + -p, --profile string specify a profile to deploy ``` ### Options inherited from parent commands ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_user.md b/docs/cli/inertia_${remote_name}_user.md index 423357f8..aef2f372 100644 --- a/docs/cli/inertia_${remote_name}_user.md +++ b/docs/cli/inertia_${remote_name}_user.md @@ -16,8 +16,8 @@ Configure user access to the Inertia Web application. ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO @@ -28,5 +28,5 @@ Configure user access to the Inertia Web application. * [inertia ${remote_name} user ls](inertia_${remote_name}_user_ls.md) - List all users registered on your remote. * [inertia ${remote_name} user reset](inertia_${remote_name}_user_reset.md) - Reset user database on your remote * [inertia ${remote_name} user rm](inertia_${remote_name}_user_rm.md) - Remove a user -* [inertia ${remote_name} user totp](inertia_${remote_name}_user_totp.md) - Manage TOTP settings for a user +* [inertia ${remote_name} user totp](inertia_${remote_name}_user_totp.md) - Manage 2FA TOTP settings for users diff --git a/docs/cli/inertia_${remote_name}_user_add.md b/docs/cli/inertia_${remote_name}_user_add.md index e170157e..0280a252 100644 --- a/docs/cli/inertia_${remote_name}_user_add.md +++ b/docs/cli/inertia_${remote_name}_user_add.md @@ -26,8 +26,8 @@ inertia ${remote_name} user add [user] [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_user_login.md b/docs/cli/inertia_${remote_name}_user_login.md index 9e9bfb87..04b82ec8 100644 --- a/docs/cli/inertia_${remote_name}_user_login.md +++ b/docs/cli/inertia_${remote_name}_user_login.md @@ -21,8 +21,8 @@ inertia ${remote_name} user login [user] [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_user_ls.md b/docs/cli/inertia_${remote_name}_user_ls.md index 018ded56..411d2d12 100644 --- a/docs/cli/inertia_${remote_name}_user_ls.md +++ b/docs/cli/inertia_${remote_name}_user_ls.md @@ -20,8 +20,8 @@ inertia ${remote_name} user ls [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_user_reset.md b/docs/cli/inertia_${remote_name}_user_reset.md index 478e6189..9e8bc8f9 100644 --- a/docs/cli/inertia_${remote_name}_user_reset.md +++ b/docs/cli/inertia_${remote_name}_user_reset.md @@ -22,8 +22,8 @@ inertia ${remote_name} user reset [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_user_rm.md b/docs/cli/inertia_${remote_name}_user_rm.md index 655e4293..2dfac37d 100644 --- a/docs/cli/inertia_${remote_name}_user_rm.md +++ b/docs/cli/inertia_${remote_name}_user_rm.md @@ -23,8 +23,8 @@ inertia ${remote_name} user rm [user] [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_user_totp.md b/docs/cli/inertia_${remote_name}_user_totp.md index 51b56beb..d9695f98 100644 --- a/docs/cli/inertia_${remote_name}_user_totp.md +++ b/docs/cli/inertia_${remote_name}_user_totp.md @@ -1,10 +1,10 @@ ## inertia ${remote_name} user totp -Manage TOTP settings for a user +Manage 2FA TOTP settings for users ### Synopsis -Manage TOTP settings for a registered user on your Inertia daemon +Manage 2FA TOTP settings for registered users on your Inertia daemon ### Options @@ -16,8 +16,8 @@ Manage TOTP settings for a registered user on your Inertia daemon ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO diff --git a/docs/cli/inertia_${remote_name}_user_totp_disable.md b/docs/cli/inertia_${remote_name}_user_totp_disable.md index 1e58fd6e..e6f39b36 100644 --- a/docs/cli/inertia_${remote_name}_user_totp_disable.md +++ b/docs/cli/inertia_${remote_name}_user_totp_disable.md @@ -20,11 +20,11 @@ inertia ${remote_name} user totp disable [user] [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO -* [inertia ${remote_name} user totp](inertia_${remote_name}_user_totp.md) - Manage TOTP settings for a user +* [inertia ${remote_name} user totp](inertia_${remote_name}_user_totp.md) - Manage 2FA TOTP settings for users diff --git a/docs/cli/inertia_${remote_name}_user_totp_enable.md b/docs/cli/inertia_${remote_name}_user_totp_enable.md index 8b156b97..e0f9bcc4 100644 --- a/docs/cli/inertia_${remote_name}_user_totp_enable.md +++ b/docs/cli/inertia_${remote_name}_user_totp_enable.md @@ -20,11 +20,11 @@ inertia ${remote_name} user totp enable [user] [flags] ``` --config string specify relative path to Inertia configuration (default "inertia.toml") + -d, --debug enable debug output from Inertia client -s, --short don't stream output from command - --verify-ssl verify SSL communications - requires a signed SSL certificate ``` ### SEE ALSO -* [inertia ${remote_name} user totp](inertia_${remote_name}_user_totp.md) - Manage TOTP settings for a user +* [inertia ${remote_name} user totp](inertia_${remote_name}_user_totp.md) - Manage 2FA TOTP settings for users diff --git a/docs/cli/inertia_init.md b/docs/cli/inertia_init.md index 983cc7c2..0cbd93ef 100644 --- a/docs/cli/inertia_init.md +++ b/docs/cli/inertia_init.md @@ -4,19 +4,32 @@ Initialize an Inertia project in this repository ### Synopsis -Initializes an Inertia project in this GitHub repository. - There must be a local git repository in order for initialization - to succeed. +Initializes an Inertia project in this GitHub repository. You can +provide an argument as the name of your project, otherwise the name of your +current directory will be used. + +There must be a local git repository in order for initialization +to succeed, unless you use the '--global' flag to initialize only +the Inertia global configuration. + +See https://inertia.ubclaunchpad.com/#project-configuration for more details. ``` inertia init [flags] ``` +### Examples + +``` +inertia init my_awesome_project +``` + ### Options ``` - -h, --help help for init - --version string specify Inertia daemon version to use + --git.remote string git remote to use for continuous deployment (default "origin") + -g, --global just initialize global inertia configuration + -h, --help help for init ``` ### Options inherited from parent commands diff --git a/docs/cli/inertia_project.md b/docs/cli/inertia_project.md new file mode 100644 index 00000000..19efb926 --- /dev/null +++ b/docs/cli/inertia_project.md @@ -0,0 +1,31 @@ +## inertia project + +Update and configure Inertia project settings + +### Synopsis + +Update and configure Inertia settings pertaining to this project. + +To create a new project, use 'inertia init'. + +For configuring remote settings, use 'inertia remote'. + +### Options + +``` + -h, --help help for project +``` + +### Options inherited from parent commands + +``` + --config string specify relative path to Inertia configuration (default "inertia.toml") +``` + +### SEE ALSO + +* [inertia](inertia.md) - Effortless, self-hosted continuous deployment for small teams and projects +* [inertia project profile](inertia_project_profile.md) - Manage project profile configurations +* [inertia project reset](inertia_project_reset.md) - Remove project configuration +* [inertia project set](inertia_project_set.md) - Update a property of your Inertia project configuration + diff --git a/docs/cli/inertia_project_profile.md b/docs/cli/inertia_project_profile.md new file mode 100644 index 00000000..1f2cfd50 --- /dev/null +++ b/docs/cli/inertia_project_profile.md @@ -0,0 +1,28 @@ +## inertia project profile + +Manage project profile configurations + +### Synopsis + +Manage profile configurations for your project + +### Options + +``` + -h, --help help for profile +``` + +### Options inherited from parent commands + +``` + --config string specify relative path to Inertia configuration (default "inertia.toml") +``` + +### SEE ALSO + +* [inertia project](inertia_project.md) - Update and configure Inertia project settings +* [inertia project profile apply](inertia_project_profile_apply.md) - Apply a project configuration profile to a remote +* [inertia project profile ls](inertia_project_profile_ls.md) - List configured project profiles +* [inertia project profile set](inertia_project_profile_set.md) - Configure project profiles +* [inertia project profile show](inertia_project_profile_show.md) - Output profile configuration + diff --git a/docs/cli/inertia_project_profile_apply.md b/docs/cli/inertia_project_profile_apply.md new file mode 100644 index 00000000..a828fd73 --- /dev/null +++ b/docs/cli/inertia_project_profile_apply.md @@ -0,0 +1,32 @@ +## inertia project profile apply + +Apply a project configuration profile to a remote + +### Synopsis + +Applies a project configuration profile to an existing remote. The applied +profile will be used whenever you run 'inertia ${remote_name} up' on the target +remote. + +By default, the profile called 'default' will be used. + +``` +inertia project profile apply [profile] [remote] [flags] +``` + +### Options + +``` + -h, --help help for apply +``` + +### Options inherited from parent commands + +``` + --config string specify relative path to Inertia configuration (default "inertia.toml") +``` + +### SEE ALSO + +* [inertia project profile](inertia_project_profile.md) - Manage project profile configurations + diff --git a/docs/cli/inertia_project_profile_ls.md b/docs/cli/inertia_project_profile_ls.md new file mode 100644 index 00000000..55e08c2d --- /dev/null +++ b/docs/cli/inertia_project_profile_ls.md @@ -0,0 +1,29 @@ +## inertia project profile ls + +List configured project profiles + +### Synopsis + +List configured profiles for this project. To add new ones, use +'inertia project profile set'. + +``` +inertia project profile ls [flags] +``` + +### Options + +``` + -h, --help help for ls +``` + +### Options inherited from parent commands + +``` + --config string specify relative path to Inertia configuration (default "inertia.toml") +``` + +### SEE ALSO + +* [inertia project profile](inertia_project_profile.md) - Manage project profile configurations + diff --git a/docs/cli/inertia_project_profile_set.md b/docs/cli/inertia_project_profile_set.md new file mode 100644 index 00000000..51812ef3 --- /dev/null +++ b/docs/cli/inertia_project_profile_set.md @@ -0,0 +1,40 @@ +## inertia project profile set + +Configure project profiles + +### Synopsis + +Configures project profiles - if the given profile does not exist, +a new one is created, otherwise the existing one is overwritten. + +Provide profile values via the available flags. + +``` +inertia project profile set [profile] [flags] +``` + +### Examples + +``` +inertia project profile set my_profile --build.type dockerfile --build.file Dockerfile.dev +``` + +### Options + +``` + --branch string branch for profile (default: current branch) + --build.file string relative path to build config file (e.g. 'Dockerfile') + --build.type string build type for profile + -h, --help help for set +``` + +### Options inherited from parent commands + +``` + --config string specify relative path to Inertia configuration (default "inertia.toml") +``` + +### SEE ALSO + +* [inertia project profile](inertia_project_profile.md) - Manage project profile configurations + diff --git a/docs/cli/inertia_project_profile_show.md b/docs/cli/inertia_project_profile_show.md new file mode 100644 index 00000000..8932c592 --- /dev/null +++ b/docs/cli/inertia_project_profile_show.md @@ -0,0 +1,29 @@ +## inertia project profile show + +Output profile configuration + +### Synopsis + +Prints the requested profile configuration. To add new ones, use +'inertia project profile set'. + +``` +inertia project profile show [flags] +``` + +### Options + +``` + -h, --help help for show +``` + +### Options inherited from parent commands + +``` + --config string specify relative path to Inertia configuration (default "inertia.toml") +``` + +### SEE ALSO + +* [inertia project profile](inertia_project_profile.md) - Manage project profile configurations + diff --git a/docs/cli/inertia_project_reset.md b/docs/cli/inertia_project_reset.md new file mode 100644 index 00000000..ff0cf23e --- /dev/null +++ b/docs/cli/inertia_project_reset.md @@ -0,0 +1,29 @@ +## inertia project reset + +Remove project configuration + +### Synopsis + +Removes your project configuration by deleting the configuration file. + This is irreversible. + +``` +inertia project reset [flags] +``` + +### Options + +``` + -h, --help help for reset +``` + +### Options inherited from parent commands + +``` + --config string specify relative path to Inertia configuration (default "inertia.toml") +``` + +### SEE ALSO + +* [inertia project](inertia_project.md) - Update and configure Inertia project settings + diff --git a/docs/cli/inertia_project_set.md b/docs/cli/inertia_project_set.md new file mode 100644 index 00000000..8f13cfb7 --- /dev/null +++ b/docs/cli/inertia_project_set.md @@ -0,0 +1,28 @@ +## inertia project set + +Update a property of your Inertia project configuration + +### Synopsis + +Updates a property of your Inertia project configuration and save it to inertia.toml. + +``` +inertia project set [property] [value] [flags] +``` + +### Options + +``` + -h, --help help for set +``` + +### Options inherited from parent commands + +``` + --config string specify relative path to Inertia configuration (default "inertia.toml") +``` + +### SEE ALSO + +* [inertia project](inertia_project.md) - Update and configure Inertia project settings + diff --git a/docs/cli/inertia_remote.md b/docs/cli/inertia_remote.md index e74e06b3..b4f4d135 100644 --- a/docs/cli/inertia_remote.md +++ b/docs/cli/inertia_remote.md @@ -36,4 +36,5 @@ inertia gcloud status # check on status of Inertia daemon * [inertia remote rm](inertia_remote_rm.md) - Remove a configured remote * [inertia remote set](inertia_remote_set.md) - Update details about remote * [inertia remote show](inertia_remote_show.md) - Show details about a remote +* [inertia remote upgrade](inertia_remote_upgrade.md) - Upgrade your remote configuration version to match the CLI diff --git a/docs/cli/inertia_remote_add.md b/docs/cli/inertia_remote_add.md index 6f708f8f..e0a8c6ed 100644 --- a/docs/cli/inertia_remote_add.md +++ b/docs/cli/inertia_remote_add.md @@ -5,19 +5,29 @@ Add a reference to a remote VPS instance ### Synopsis Adds a reference to a remote VPS instance. Requires information about the VPS -including IP address, user and a PEM file. The provided name will be used in other +including IP address, user and a identity file. The provided name will be used in other Inertia commands. ``` inertia remote add [remote] [flags] ``` +### Examples + +``` +inertia remote add staging --daemon.gen-secret --ip 1.2.3.4 +``` + ### Options ``` - -h, --help help for add - -p, --port string remote daemon port (default "4303") - -s, --ssh.port string remote SSH port (default "22") + --daemon.gen-secret toggle webhook secret generation (default true) + --daemon.port string remote daemon port (default "4303") + -h, --help help for add + --ip string IP address of remote + --ssh.key string path to SSH key for remote + --ssh.port string remote SSH port (default "22") + --ssh.user string user to use when accessing remote over SSH ``` ### Options inherited from parent commands diff --git a/docs/cli/inertia_remote_rm.md b/docs/cli/inertia_remote_rm.md index 0def6020..2efd31f9 100644 --- a/docs/cli/inertia_remote_rm.md +++ b/docs/cli/inertia_remote_rm.md @@ -10,6 +10,12 @@ Remove a remote from Inertia's configuration file. inertia remote rm [remote] [flags] ``` +### Examples + +``` +inertia remote rm staging +``` + ### Options ``` diff --git a/docs/cli/inertia_remote_show.md b/docs/cli/inertia_remote_show.md index cb6cfe64..da222626 100644 --- a/docs/cli/inertia_remote_show.md +++ b/docs/cli/inertia_remote_show.md @@ -10,6 +10,12 @@ Shows details about the given remote. inertia remote show [remote] [flags] ``` +### Examples + +``` +inertia remote show staging +``` + ### Options ``` diff --git a/docs/cli/inertia_remote_upgrade.md b/docs/cli/inertia_remote_upgrade.md new file mode 100644 index 00000000..522af912 --- /dev/null +++ b/docs/cli/inertia_remote_upgrade.md @@ -0,0 +1,36 @@ +## inertia remote upgrade + +Upgrade your remote configuration version to match the CLI + +### Synopsis + +Upgrade your remote configuration version to match the CLI and save it to global settings. + +``` +inertia remote upgrade [flags] +``` + +### Examples + +``` +inertia remote upgrade dev staging +``` + +### Options + +``` + --all upgrade all remotes + -h, --help help for upgrade + --version string specify Inertia daemon version to set (default "v0.5.2-34-gbd36126") +``` + +### Options inherited from parent commands + +``` + --config string specify relative path to Inertia configuration (default "inertia.toml") +``` + +### SEE ALSO + +* [inertia remote](inertia_remote.md) - Configure the local settings for a remote host + diff --git a/docs/tip/api/index.html b/docs/tip/api/index.html index 837c65f1..057f81ad 100644 --- a/docs/tip/api/index.html +++ b/docs/tip/api/index.html @@ -341,12 +341,9 @@
Authorizations:
query Parameters
container
string
Example: "/docker-compose"

Name of container to fetch logs for - leave blank for Inertia daemon logs

stream
boolean
Example: true

Whether or not to upgrade connection to a websocket

entries
integer
Example: 500

Number of lines of logs to fetch (default 500)

-

Responses

200

Log contents retrieved

+

Responses

200

Success!

4XX,5XX

Something went wrong - refer to the error code and message for more details

-
get /logs
https://$DAEMON_ADDR:$DAEMON_PORT/logs

Response samples

text/plain
Copy
No deployment detected
-Setting up project...
-Cloning branch dev from git@github.com:example/example.git...
-

Monitoring

Daemon healthcheck

Returns OK if daemon is online and ready

+
get /logs
https://$DAEMON_ADDR:$DAEMON_PORT/logs

Response samples

application/json
Copy
Expand all Collapse all
{
  • "code": 0,
  • "message": "(example) successfully did something!",
  • "request_id": "example/2Mch7LMzhj-000023",
  • "data":
    {
    }
}

Monitoring

Daemon healthcheck

Returns OK if daemon is online and ready

Responses

200

Daemon is online

get /
https://$DAEMON_ADDR:$DAEMON_PORT/

Response samples

text/plain
Copy
Hello world!

Deployment status check

Check the status of your Inertia deployment

Authorizations:

Responses

200

Success!

@@ -355,12 +352,9 @@
Authorizations:
query Parameters
container
string
Example: "/docker-compose"

Name of container to fetch logs for - leave blank for Inertia daemon logs

stream
boolean
Example: true

Whether or not to upgrade connection to a websocket

entries
integer
Example: 500

Number of lines of logs to fetch (default 500)

-

Responses

200

Log contents retrieved

+

Responses

200

Success!

4XX,5XX

Something went wrong - refer to the error code and message for more details

-
get /logs
https://$DAEMON_ADDR:$DAEMON_PORT/logs

Response samples

text/plain
Copy
No deployment detected
-Setting up project...
-Cloning branch dev from git@github.com:example/example.git...
-

Authentication

Validate JWT

Validate your JWT

+
get /logs
https://$DAEMON_ADDR:$DAEMON_PORT/logs

Response samples

application/json
Copy
Expand all Collapse all
{
  • "code": 0,
  • "message": "(example) successfully did something!",
  • "request_id": "example/2Mch7LMzhj-000023",
  • "data":
    {
    }
}

Authentication

Validate JWT

Validate your JWT

Authorizations:

Responses

200

Success!

4XX,5XX

Something went wrong - refer to the error code and message for more details

get /user/validate
https://$DAEMON_ADDR:$DAEMON_PORT/user/validate

Response samples

application/json
Copy
Expand all Collapse all
{
  • "code": 0,
  • "message": "(example) successfully did something!",
  • "request_id": "example/2Mch7LMzhj-000023",
  • "data": { }
}

Log in as user

Authenticate as a user to Inertia daemon

@@ -386,7 +380,7 @@
4XX,5XX

Something went wrong - refer to the error code and message for more details

post /user/totp/disable
https://$DAEMON_ADDR:$DAEMON_PORT/user/totp/disable

Response samples

application/json
Copy
Expand all Collapse all
{
  • "code": 0,
  • "message": "(example) successfully did something!",
  • "request_id": "example/2Mch7LMzhj-000023",
  • "data": { }
}