diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d246670f..50cdd7d9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,5 +1,4 @@ name: build - on: [push] jobs: @@ -9,7 +8,7 @@ jobs: - name: Checkout uses: actions/checkout@v1 - name: Build - run: ./ci/build.sh + run: ./ci/steps/build.sh - name: Upload uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml new file mode 100644 index 00000000..a8045e43 --- /dev/null +++ b/.github/workflows/integration.yaml @@ -0,0 +1,26 @@ +name: integration +on: + push: + schedule: + - cron: '*/180 * * * *' + +jobs: + integration: + runs-on: ubuntu-latest + env: + CODER_URL: ${{ secrets.CODER_URL }} + CODER_EMAIL: ${{ secrets.CODER_EMAIL }} + CODER_PASSWORD: ${{ secrets.CODER_PASSWORD }} + steps: + - uses: actions/checkout@v1 + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - uses: actions/setup-go@v2 + with: + go-version: '^1.14' + - name: go test + run: go test -v ./ci/integration/... diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..36397b7d --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,46 @@ +name: test +on: [push] + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: fmt + uses: ./ci/image + with: + args: ./ci/steps/fmt.sh + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: lint + uses: ./ci/image + with: + args: ./ci/steps/lint.sh + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: test + uses: ./ci/image + with: + args: go test ./internal/... ./cmd/... diff --git a/.gitignore b/.gitignore index 5fd924f8..c3d7f6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea ci/bin cmd/coder/coder +ci/integration/bin +ci/integration/env.sh \ No newline at end of file diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile new file mode 100644 index 00000000..cb06bf81 --- /dev/null +++ b/ci/image/Dockerfile @@ -0,0 +1,8 @@ +FROM golang:1 + +ENV GOFLAGS="-mod=readonly" +ENV CI=true + +RUN go get golang.org/x/tools/cmd/goimports +RUN go get golang.org/x/lint/golint +RUN go get github.com/mattn/goveralls \ No newline at end of file diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go new file mode 100644 index 00000000..e14edd6e --- /dev/null +++ b/ci/integration/integration_test.go @@ -0,0 +1,120 @@ +package integration + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "cdr.dev/coder-cli/ci/tcli" + "cdr.dev/slog/sloggers/slogtest/assert" +) + +func build(path string) error { + cmd := exec.Command( + "sh", "-c", + fmt.Sprintf("cd ../../ && go build -o %s ./cmd/coder", path), + ) + cmd.Env = append(os.Environ(), "GOOS=linux", "CGO_ENABLED=0") + + _, err := cmd.CombinedOutput() + if err != nil { + return err + } + return nil +} + +var binpath string + +func init() { + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + + binpath = filepath.Join(cwd, "bin", "coder") + err = build(binpath) + if err != nil { + panic(err) + } +} + +// write session tokens to the given container runner +func headlessLogin(ctx context.Context, t *testing.T, runner *tcli.ContainerRunner) { + creds := login(ctx, t) + cmd := exec.CommandContext(ctx, "sh", "-c", "mkdir -p ~/.config/coder && cat > ~/.config/coder/session") + + // !IMPORTANT: be careful that this does not appear in logs + cmd.Stdin = strings.NewReader(creds.token) + runner.RunCmd(cmd).Assert(t, + tcli.Success(), + ) + runner.Run(ctx, fmt.Sprintf("echo -ne %s > ~/.config/coder/url", creds.url)).Assert(t, + tcli.Success(), + ) +} + +func TestCoderCLI(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() + + c, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ + Image: "codercom/enterprise-dev", + Name: "coder-cli-tests", + BindMounts: map[string]string{ + binpath: "/bin/coder", + }, + }) + assert.Success(t, "new run container", err) + defer c.Close() + + c.Run(ctx, "which coder").Assert(t, + tcli.Success(), + tcli.StdoutMatches("/usr/sbin/coder"), + tcli.StderrEmpty(), + ) + + c.Run(ctx, "coder version").Assert(t, + tcli.StderrEmpty(), + tcli.Success(), + tcli.StdoutMatches("linux"), + ) + + c.Run(ctx, "coder help").Assert(t, + tcli.Success(), + tcli.StderrMatches("Commands:"), + tcli.StderrMatches("Usage: coder"), + tcli.StdoutEmpty(), + ) + + headlessLogin(ctx, t, c) + + c.Run(ctx, "coder envs").Assert(t, + tcli.Success(), + ) + + c.Run(ctx, "coder urls").Assert(t, + tcli.Error(), + ) + + c.Run(ctx, "coder sync").Assert(t, + tcli.Error(), + ) + + c.Run(ctx, "coder sh").Assert(t, + tcli.Error(), + ) + + c.Run(ctx, "coder logout").Assert(t, + tcli.Success(), + ) + + c.Run(ctx, "coder envs").Assert(t, + tcli.Error(), + ) +} diff --git a/ci/integration/login_test.go b/ci/integration/login_test.go new file mode 100644 index 00000000..e0334f00 --- /dev/null +++ b/ci/integration/login_test.go @@ -0,0 +1,75 @@ +package integration + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "testing" + + "cdr.dev/slog/sloggers/slogtest/assert" +) + +type credentials struct { + url, token string +} + +func login(ctx context.Context, t *testing.T) credentials { + var ( + email = requireEnv(t, "CODER_EMAIL") + password = requireEnv(t, "CODER_PASSWORD") + rawURL = requireEnv(t, "CODER_URL") + ) + sessionToken := getSessionToken(ctx, t, email, password, rawURL) + + return credentials{ + url: rawURL, + token: sessionToken, + } +} + +func requireEnv(t *testing.T, key string) string { + value := os.Getenv(key) + assert.True(t, fmt.Sprintf("%q is nonempty", key), value != "") + return value +} + +type loginBuiltInAuthReq struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type loginBuiltInAuthResp struct { + SessionToken string `json:"session_token"` +} + +func getSessionToken(ctx context.Context, t *testing.T, email, password, rawURL string) string { + reqbody := loginBuiltInAuthReq{ + Email: email, + Password: password, + } + body, err := json.Marshal(reqbody) + assert.Success(t, "marshal login req body", err) + + u, err := url.Parse(rawURL) + assert.Success(t, "parse raw url", err) + u.Path = "/auth/basic/login" + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body)) + assert.Success(t, "new request", err) + + resp, err := http.DefaultClient.Do(req) + assert.Success(t, "do request", err) + assert.Equal(t, "request status 201", http.StatusCreated, resp.StatusCode) + + var tokenResp loginBuiltInAuthResp + err = json.NewDecoder(resp.Body).Decode(&tokenResp) + assert.Success(t, "decode response", err) + + defer resp.Body.Close() + + return tokenResp.SessionToken +} diff --git a/ci/build.sh b/ci/steps/build.sh similarity index 94% rename from ci/build.sh rename to ci/steps/build.sh index 8a6f540c..8411ccde 100755 --- a/ci/build.sh +++ b/ci/steps/build.sh @@ -14,14 +14,14 @@ mkdir -p bin build(){ tmpdir=$(mktemp -d) - go build -ldflags "-s -w -X main.version=${tag}" -o "$tmpdir/coder" ../cmd/coder + go build -ldflags "-s -w -X main.version=${tag}" -o "$tmpdir/coder" ../../cmd/coder pushd "$tmpdir" tarname="coder-cli-$GOOS-$GOARCH.tar.gz" tar -czf "$tarname" coder popd - cp "$tmpdir/$tarname" bin + cp "$tmpdir/$tarname" ../bin rm -rf "$tmpdir" } diff --git a/ci/steps/fmt.sh b/ci/steps/fmt.sh new file mode 100755 index 00000000..bb4b0d2c --- /dev/null +++ b/ci/steps/fmt.sh @@ -0,0 +1,16 @@ +#!/bin/bash +echo "Formatting..." + +go mod tidy +gofmt -w -s . +goimports -w "-local=$$(go list -m)" . + +if [ "$CI" != "" ]; then + if [[ $(git ls-files --other --modified --exclude-standard) != "" ]]; then + echo "Files need generation or are formatted incorrectly:" + git -c color.ui=always status | grep --color=no '\e\[31m' + echo "Please run the following locally:" + echo " ./ci/steps/fmt.sh" + exit 1 + fi +fi \ No newline at end of file diff --git a/ci/steps/lint.sh b/ci/steps/lint.sh new file mode 100755 index 00000000..51da081d --- /dev/null +++ b/ci/steps/lint.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +echo "Linting..." + +go vet ./... +golint -set_exit_status ./... \ No newline at end of file diff --git a/ci/tcli/doc.go b/ci/tcli/doc.go new file mode 100644 index 00000000..561dc480 --- /dev/null +++ b/ci/tcli/doc.go @@ -0,0 +1,4 @@ +// Package tcli provides a framework for CLI integration testing. +// Execute commands on the raw host or inside a docker container. +// Define custom Assertion types to extend test functionality. +package tcli diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go new file mode 100644 index 00000000..101cc926 --- /dev/null +++ b/ci/tcli/tcli.go @@ -0,0 +1,327 @@ +package tcli + +import ( + "bytes" + "context" + "fmt" + "io" + "os/exec" + "regexp" + "strings" + "testing" + "time" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "cdr.dev/slog/sloggers/slogtest/assert" + "golang.org/x/xerrors" +) + +var ( + _ runnable = &ContainerRunner{} + _ runnable = &HostRunner{} +) + +type runnable interface { + Run(ctx context.Context, command string) *Assertable + RunCmd(cmd *exec.Cmd) *Assertable + io.Closer +} + +// ContainerConfig describes the ContainerRunner configuration schema for initializing a testing environment +type ContainerConfig struct { + Name string + Image string + BindMounts map[string]string +} + +func mountArgs(m map[string]string) (args []string) { + for src, dest := range m { + args = append(args, "--mount", fmt.Sprintf("type=bind,source=%s,target=%s", src, dest)) + } + return args +} + +func preflightChecks() error { + _, err := exec.LookPath("docker") + if err != nil { + return xerrors.Errorf(`"docker" not found in $PATH`) + } + return nil +} + +// ContainerRunner specifies a runtime container for performing command tests +type ContainerRunner struct { + name string + ctx context.Context +} + +// NewContainerRunner starts a new docker container for executing command tests +func NewContainerRunner(ctx context.Context, config *ContainerConfig) (*ContainerRunner, error) { + if err := preflightChecks(); err != nil { + return nil, err + } + + args := []string{ + "run", + "--name", config.Name, + "-it", "-d", + } + args = append(args, mountArgs(config.BindMounts)...) + args = append(args, config.Image) + + cmd := exec.CommandContext(ctx, "docker", args...) + + out, err := cmd.CombinedOutput() + if err != nil { + return nil, xerrors.Errorf( + "failed to start testing container %q, (%s): %w", + config.Name, string(out), err) + } + + return &ContainerRunner{ + name: config.Name, + ctx: ctx, + }, nil +} + +// Close kills and removes the command execution testing container +func (r *ContainerRunner) Close() error { + cmd := exec.CommandContext(r.ctx, + "sh", "-c", strings.Join([]string{ + "docker", "kill", r.name, "&&", + "docker", "rm", r.name, + }, " ")) + + out, err := cmd.CombinedOutput() + if err != nil { + return xerrors.Errorf( + "failed to stop testing container %q, (%s): %w", + r.name, string(out), err) + } + return nil +} + +// Run executes the given command in the runtime container with reasonable defaults. +// "command" is executed in a shell as an argument to "sh -c". +func (r *ContainerRunner) Run(ctx context.Context, command string) *Assertable { + cmd := exec.CommandContext(ctx, + "docker", "exec", "-i", r.name, + "sh", "-c", command, + ) + + return &Assertable{ + cmd: cmd, + tname: command, + } +} + +// RunCmd lifts the given *exec.Cmd into the runtime container +func (r *ContainerRunner) RunCmd(cmd *exec.Cmd) *Assertable { + path, _ := exec.LookPath("docker") + cmd.Path = path + command := strings.Join(cmd.Args, " ") + cmd.Args = append([]string{"docker", "exec", "-i", r.name}, cmd.Args...) + + return &Assertable{ + cmd: cmd, + tname: command, + } +} + +// HostRunner executes command tests on the host, outside of a container +type HostRunner struct{} + +// Run executes the given command on the host. +// "command" is executed in a shell as an argument to "sh -c". +func (r *HostRunner) Run(ctx context.Context, command string) *Assertable { + cmd := exec.CommandContext(ctx, "sh", "-c", command) + + return &Assertable{ + cmd: cmd, + tname: command, + } +} + +// RunCmd executes the given *exec.Cmd on the host +func (r *HostRunner) RunCmd(cmd *exec.Cmd) *Assertable { + return &Assertable{ + cmd: cmd, + tname: strings.Join(cmd.Args, " "), + } +} + +// Close is a noop for HostRunner +func (r *HostRunner) Close() error { + return nil +} + +// Assertable describes an initialized command ready to be run and asserted against +type Assertable struct { + cmd *exec.Cmd + tname string +} + +// Assert runs the Assertable and +func (a Assertable) Assert(t *testing.T, option ...Assertion) { + slog.Helper() + var ( + stdout bytes.Buffer + stderr bytes.Buffer + result CommandResult + ) + + a.cmd.Stdout = &stdout + a.cmd.Stderr = &stderr + + start := time.Now() + err := a.cmd.Run() + result.Duration = time.Since(start) + + if exitErr, ok := err.(*exec.ExitError); ok { + result.ExitCode = exitErr.ExitCode() + } else if err != nil { + slogtest.Fatal(t, "command failed to run", slog.Error(err), slog.F("command", a.cmd)) + } else { + result.ExitCode = 0 + } + + result.Stdout = stdout.Bytes() + result.Stderr = stderr.Bytes() + + slogtest.Info(t, "command output", + slog.F("command", a.cmd), + slog.F("stdout", string(result.Stdout)), + slog.F("stderr", string(result.Stderr)), + slog.F("exit_code", result.ExitCode), + slog.F("duration", result.Duration), + ) + + for _, assertion := range option { + assertion(t, &result) + } +} + +// Assertion specifies an assertion on the given CommandResult. +// Pass custom Assertion functions to cover special cases. +type Assertion func(t *testing.T, r *CommandResult) + +// CommandResult contains the aggregated result of a command execution +type CommandResult struct { + Stdout, Stderr []byte + ExitCode int + Duration time.Duration +} + +// Success asserts that the command exited with an exit code of 0 +func Success() Assertion { + slog.Helper() + return ExitCodeIs(0) +} + +// Error asserts that the command exited with a nonzero exit code +func Error() Assertion { + return func(t *testing.T, r *CommandResult) { + slog.Helper() + assert.True(t, "exit code is nonzero", r.ExitCode != 0) + } +} + +// ExitCodeIs asserts that the command exited with the given code +func ExitCodeIs(code int) Assertion { + return func(t *testing.T, r *CommandResult) { + slog.Helper() + assert.Equal(t, "exit code is as expected", code, r.ExitCode) + } +} + +// StdoutEmpty asserts that the command did not write any data to Stdout +func StdoutEmpty() Assertion { + return func(t *testing.T, r *CommandResult) { + slog.Helper() + empty(t, "stdout", r.Stdout) + } +} + +// GetResult offers an escape hatch from tcli +// The pointer passed as "result" will be assigned to the command's *CommandResult +func GetResult(result **CommandResult) Assertion { + return func(t *testing.T, r *CommandResult) { + slog.Helper() + *result = r + } +} + +// StderrEmpty asserts that the command did not write any data to Stderr +func StderrEmpty() Assertion { + return func(t *testing.T, r *CommandResult) { + slog.Helper() + empty(t, "stderr", r.Stderr) + } +} + +// StdoutMatches asserts that Stdout contains a substring which matches the given regexp +func StdoutMatches(pattern string) Assertion { + return func(t *testing.T, r *CommandResult) { + slog.Helper() + matches(t, "stdout", pattern, r.Stdout) + } +} + +// StderrMatches asserts that Stderr contains a substring which matches the given regexp +func StderrMatches(pattern string) Assertion { + return func(t *testing.T, r *CommandResult) { + slog.Helper() + matches(t, "stderr", pattern, r.Stderr) + } +} + +func matches(t *testing.T, name, pattern string, target []byte) { + slog.Helper() + fields := []slog.Field{ + slog.F("pattern", pattern), + slog.F("target", string(target)), + slog.F("sink", name), + } + + ok, err := regexp.Match(pattern, target) + if err != nil { + slogtest.Fatal(t, "failed to attempt regexp match", append(fields, slog.Error(err))...) + } + if !ok { + slogtest.Fatal(t, "expected to find pattern, no match found", fields...) + } +} + +func empty(t *testing.T, name string, a []byte) { + slog.Helper() + if len(a) > 0 { + slogtest.Fatal(t, "expected "+name+" to be empty", slog.F("got", string(a))) + } +} + +// DurationLessThan asserts that the command completed in less than the given duration +func DurationLessThan(dur time.Duration) Assertion { + return func(t *testing.T, r *CommandResult) { + slog.Helper() + if r.Duration > dur { + slogtest.Fatal(t, "duration longer than expected", + slog.F("expected_less_than", dur.String), + slog.F("actual", r.Duration.String()), + ) + } + } +} + +// DurationGreaterThan asserts that the command completed in greater than the given duration +func DurationGreaterThan(dur time.Duration) Assertion { + return func(t *testing.T, r *CommandResult) { + slog.Helper() + if r.Duration < dur { + slogtest.Fatal(t, "duration shorter than expected", + slog.F("expected_greater_than", dur.String), + slog.F("actual", r.Duration.String()), + ) + } + } +} diff --git a/ci/tcli/tcli_test.go b/ci/tcli/tcli_test.go new file mode 100644 index 00000000..97bd1b6e --- /dev/null +++ b/ci/tcli/tcli_test.go @@ -0,0 +1,69 @@ +package tcli_test + +import ( + "context" + "os" + "os/exec" + "strings" + "testing" + "time" + + "cdr.dev/coder-cli/ci/tcli" + "cdr.dev/slog/sloggers/slogtest/assert" +) + +func TestTCli(t *testing.T) { + t.Parallel() + ctx := context.Background() + + container, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ + Image: "ubuntu:latest", + Name: "test-container", + }) + assert.Success(t, "new run container", err) + defer container.Close() + + container.Run(ctx, "echo testing").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("esting"), + ) + + container.Run(ctx, "sleep 1.5 && echo 1>&2 stderr-message").Assert(t, + tcli.Success(), + tcli.StdoutEmpty(), + tcli.StderrMatches("message"), + tcli.DurationGreaterThan(time.Second), + ) + + cmd := exec.CommandContext(ctx, "cat") + cmd.Stdin = strings.NewReader("testing") + + container.RunCmd(cmd).Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("testing"), + ) +} +func TestHostRunner(t *testing.T) { + t.Parallel() + var ( + c tcli.HostRunner + ctx = context.Background() + ) + + c.Run(ctx, "echo testing").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("testing"), + ) + + wd, err := os.Getwd() + assert.Success(t, "get working dir", err) + + c.Run(ctx, "pwd").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches(wd), + ) +} diff --git a/cmd/coder/sync.go b/cmd/coder/sync.go index e0d62708..601fddd7 100644 --- a/cmd/coder/sync.go +++ b/cmd/coder/sync.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "fmt" "log" "os" "os/exec" @@ -34,7 +33,7 @@ func (cmd *syncCmd) RegisterFlags(fl *pflag.FlagSet) { } // version returns local rsync protocol version as a string. -func (_ *syncCmd) version() string { +func rsyncVersion() string { cmd := exec.Command("rsync", "--version") out, err := cmd.CombinedOutput() if err != nil { @@ -93,13 +92,13 @@ func (cmd *syncCmd) Run(fl *pflag.FlagSet) { Client: entClient, } - localVersion := cmd.version() + localVersion := rsyncVersion() remoteVersion, rsyncErr := s.Version() if rsyncErr != nil { flog.Info("Unable to determine remote rsync version. Proceeding cautiously.") } else if localVersion != remoteVersion { - flog.Fatal(fmt.Sprintf("rsync protocol mismatch. %s.", localVersion, rsyncErr)) + flog.Fatal("rsync protocol mismatch: local = %v, remote = %v", localVersion, rsyncErr) } for err == nil || err == sync.ErrRestartSync { diff --git a/go.mod b/go.mod index 7d8feec2..4f496165 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module cdr.dev/coder-cli go 1.14 require ( + cdr.dev/slog v1.3.0 cdr.dev/wsep v0.0.0-20200728013649-82316a09813f github.com/fatih/color v1.9.0 // indirect github.com/gorilla/websocket v1.4.1 diff --git a/internal/activity/pusher.go b/internal/activity/pusher.go index fb57068d..c2102fc1 100644 --- a/internal/activity/pusher.go +++ b/internal/activity/pusher.go @@ -4,8 +4,9 @@ import ( "time" "cdr.dev/coder-cli/internal/entclient" - "go.coder.com/flog" "golang.org/x/time/rate" + + "go.coder.com/flog" ) const pushInterval = time.Minute @@ -20,6 +21,7 @@ type Pusher struct { rate *rate.Limiter } +// NewPusher instantiates a new instance of Pusher func NewPusher(c *entclient.Client, envID, source string) *Pusher { return &Pusher{ envID: envID, @@ -29,6 +31,7 @@ func NewPusher(c *entclient.Client, envID, source string) *Pusher { } } +// Push pushes activity, abiding by a rate limit func (p *Pusher) Push() { if !p.rate.Allow() { return diff --git a/internal/activity/writer.go b/internal/activity/writer.go index 1e5c4f66..a10c4341 100644 --- a/internal/activity/writer.go +++ b/internal/activity/writer.go @@ -7,11 +7,13 @@ type activityWriter struct { wr io.Writer } +// Write writes to the underlying writer and tracks activity func (w *activityWriter) Write(p []byte) (n int, err error) { w.p.Push() return w.wr.Write(p) } +// Writer wraps the given writer such that all writes trigger an activity push func (p *Pusher) Writer(wr io.Writer) io.Writer { return &activityWriter{p: p, wr: wr} } diff --git a/internal/config/file.go b/internal/config/file.go index de254fc7..bb83e746 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -1,20 +1,25 @@ package config +// File provides convenience methods for interacting with *os.File type File string +// Delete deletes the file func (f File) Delete() error { return rm(string(f)) } +// Write writes the string to the file func (f File) Write(s string) error { return write(string(f), 0600, []byte(s)) } +// Read reads the file to a string func (f File) Read() (string, error) { byt, err := read(string(f)) return string(byt), err } +// Coder CLI configuration files var ( Session File = "session" URL File = "url" diff --git a/internal/entclient/activity.go b/internal/entclient/activity.go index 92c0afed..362d7da0 100644 --- a/internal/entclient/activity.go +++ b/internal/entclient/activity.go @@ -4,6 +4,7 @@ import ( "net/http" ) +// PushActivity pushes CLI activity to Coder func (c Client) PushActivity(source string, envID string) error { res, err := c.request("POST", "/api/metrics/usage/push", map[string]string{ "source": source, diff --git a/internal/entclient/client.go b/internal/entclient/client.go index e0609d13..a306c707 100644 --- a/internal/entclient/client.go +++ b/internal/entclient/client.go @@ -6,6 +6,7 @@ import ( "net/url" ) +// Client wraps the Coder HTTP API type Client struct { BaseURL *url.URL Token string diff --git a/internal/entclient/devurl.go b/internal/entclient/devurl.go index 57925372..9260dcce 100644 --- a/internal/entclient/devurl.go +++ b/internal/entclient/devurl.go @@ -5,11 +5,12 @@ import ( "net/http" ) +// DelDevURL deletes the specified devurl func (c Client) DelDevURL(envID, urlID string) error { reqString := "/api/environments/%s/devurls/%s" - reqUrl := fmt.Sprintf(reqString, envID, urlID) + reqURL := fmt.Sprintf(reqString, envID, urlID) - res, err := c.request("DELETE", reqUrl, map[string]string{ + res, err := c.request("DELETE", reqURL, map[string]string{ "environment_id": envID, "url_id": urlID, }) @@ -24,11 +25,12 @@ func (c Client) DelDevURL(envID, urlID string) error { return nil } +// UpsertDevURL upserts the specified devurl for the authenticated user func (c Client) UpsertDevURL(envID, port, access string) error { reqString := "/api/environments/%s/devurls" - reqUrl := fmt.Sprintf(reqString, envID) + reqURL := fmt.Sprintf(reqString, envID) - res, err := c.request("POST", reqUrl, map[string]string{ + res, err := c.request("POST", reqURL, map[string]string{ "environment_id": envID, "port": port, "access": access, diff --git a/internal/entclient/env.go b/internal/entclient/env.go index 45a7aa0e..11a806c5 100644 --- a/internal/entclient/env.go +++ b/internal/entclient/env.go @@ -7,11 +7,13 @@ import ( "nhooyr.io/websocket" ) +// Environment describes a Coder environment type Environment struct { Name string `json:"name"` ID string `json:"id"` } +// Envs gets the list of environments owned by the authenticated user func (c Client) Envs(user *User, org Org) ([]Environment, error) { var envs []Environment err := c.requestBody( @@ -22,6 +24,8 @@ func (c Client) Envs(user *User, org Org) ([]Environment, error) { return envs, err } +// DialWsep dials an environments command execution interface +// See github.com/cdr/wsep for details func (c Client) DialWsep(ctx context.Context, env Environment) (*websocket.Conn, error) { u := c.copyURL() if c.BaseURL.Scheme == "https" { diff --git a/internal/entclient/me.go b/internal/entclient/me.go index 7c7c66f0..69c3bde5 100644 --- a/internal/entclient/me.go +++ b/internal/entclient/me.go @@ -1,11 +1,13 @@ package entclient +// User describes a Coder user account type User struct { ID string `json:"id"` Email string `json:"email"` Username string `json:"username"` } +// Me gets the details of the authenticated user func (c Client) Me() (*User, error) { var u User err := c.requestBody("GET", "/api/users/me", nil, &u) @@ -15,11 +17,13 @@ func (c Client) Me() (*User, error) { return &u, nil } +// SSHKey describes an SSH keypair type SSHKey struct { PublicKey string `json:"public_key"` PrivateKey string `json:"private_key"` } +// SSHKey gets the current SSH kepair of the authenticated user func (c Client) SSHKey() (*SSHKey, error) { var key SSHKey err := c.requestBody("GET", "/api/users/me/sshkey", nil, &key) diff --git a/internal/entclient/org.go b/internal/entclient/org.go index 24a8307b..6ac9cdc5 100644 --- a/internal/entclient/org.go +++ b/internal/entclient/org.go @@ -1,11 +1,13 @@ package entclient +// Org describes an Organization in Coder type Org struct { ID string `json:"id"` Name string `json:"name"` Members []User `json:"members"` } +// Orgs gets all Organizations func (c Client) Orgs() ([]Org, error) { var os []Org err := c.requestBody("GET", "/api/orgs", nil, &os) diff --git a/internal/loginsrv/server.go b/internal/loginsrv/server.go index b062977a..2f8774c9 100644 --- a/internal/loginsrv/server.go +++ b/internal/loginsrv/server.go @@ -6,6 +6,7 @@ import ( "sync" ) +// Server waits for the login callback to send session token type Server struct { TokenCond *sync.Cond Token string diff --git a/internal/sync/sync.go b/internal/sync/sync.go index b00cbeb1..b58382a1 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -210,6 +210,7 @@ func (s Sync) work(ev timedEvent) { } } +// ErrRestartSync describes a known error case that can be solved by re-starting the command var ErrRestartSync = errors.New("the sync exited because it was overloaded, restart it") // workEventGroup converges a group of events to prevent duplicate work. diff --git a/internal/xterminal/terminal.go b/internal/xterminal/terminal.go index d2838725..ce3c9e3f 100644 --- a/internal/xterminal/terminal.go +++ b/internal/xterminal/terminal.go @@ -41,6 +41,7 @@ func ColorEnabled(fd uintptr) (bool, error) { return terminal.IsTerminal(int(fd)), nil } +// ResizeEvent describes the new terminal dimensions following a resize type ResizeEvent struct { Height, Width uint16 }
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: