diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 50cdd7d9..00000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: build -on: [push] - -jobs: - build: - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Build - run: ./ci/steps/build.sh - - name: Upload - uses: actions/upload-artifact@v2 - with: - name: coder-cli - path: ./ci/bin/coder-cli-* diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml deleted file mode 100644 index a8045e43..00000000 --- a/.github/workflows/integration.yaml +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 996a13a1..00000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,60 +0,0 @@ -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 -v -cover -covermode=count ./internal/... ./cmd/... - gendocs: - 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: generate-docs - uses: ./ci/image - with: - args: ./ci/steps/gendocs.sh diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c3d7f6a6..00000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.idea -ci/bin -cmd/coder/coder -ci/integration/bin -ci/integration/env.sh \ No newline at end of file diff --git a/README.md b/README.md index 95b321a4..4fbeb053 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,13 @@ -# coder-cli +# Coder v1 CLI -`coder` is a command line utility for Coder Enterprise. +[![GitHub Release](https://img.shields.io/github/v/release/cdr/coder-cli?color=6b9ded&include_prerelease=false)](https://github.com/cdr/coder-cli/releases) +[![Documentation](https://godoc.org/cdr.dev/coder-cli?status.svg)](https://pkg.go.dev/cdr.dev/coder-cli/coder-sdk) -To report bugs and request features, please [open an issue](https://github.com/cdr/coder-cli/issues/new). +This is the command line utility for [Coder v1](https://coder.com/docs/coder). If you are using +[Coder v2 / Coder OSS](https://coder.com/docs/coder-oss/latest), use +[these instructions](https://coder.com/docs/coder-oss/latest/install) to install the CLI. -## Usage +The Coder v1 CLI is now closed-source. You may download binary releases from this repo. -View the `coder-cli` documentation [here](./docs/coder.md). - -You can find additional Coder Enterprise usage documentation on [https://enterprise.coder.com](https://enterprise.coder.com/docs/getting-started). - -## Install Release - -Download the latest [release](https://github.com/cdr/coder-cli/releases): - -1. Click a release and download the tar file for your operating system (ex: coder-cli-linux-amd64.tar.gz) -2. Extract the `coder` binary from the tar file, ex: - -```bash -cd ~/go/bin -tar -xvf ~/Downloads/coder-cli-linux-amd64.tar.gz -``` +[Coder v2](https://coder.com/docs/coder-oss/latest) is open-source and the recommended +version for new Coder users. \ No newline at end of file diff --git a/ci/README.md b/ci/README.md deleted file mode 100644 index ae2370a1..00000000 --- a/ci/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# ci - -## integration tests - -### `tcli` - -Package `tcli` provides a framework for writing end-to-end CLI tests. -Each test group can have its own container for executing commands in a consistent -and isolated filesystem. - -### prerequisites - -Assign the following environment variables to run the integration tests -against an existing Enterprise deployment instance. - -```bash -export CODER_URL=... -export CODER_EMAIL=... -export CODER_PASSWORD=... -``` - -Then, simply run the test command from the project root - -```sh -go test -v ./ci/integration -``` diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile deleted file mode 100644 index cb06bf81..00000000 --- a/ci/image/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -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/devurls_test.go b/ci/integration/devurls_test.go deleted file mode 100644 index 06553107..00000000 --- a/ci/integration/devurls_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package integration - -import ( - "context" - "testing" - - "cdr.dev/coder-cli/ci/tcli" -) - -func TestDevURLCLI(t *testing.T) { - t.Parallel() - run(t, "coder-cli-devurl-tests", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { - c.Run(ctx, "which coder").Assert(t, - tcli.Success(), - tcli.StdoutMatches("/usr/sbin/coder"), - tcli.StderrEmpty(), - ) - - c.Run(ctx, "coder urls ls").Assert(t, - tcli.Error(), - ) - }) - - // The following cannot be enabled nor verified until either the - // integration testing dogfood target has environments created, or - // we implement the 'env create' command for coder-cli to create our - // own here. - - // If we were to create an env ourselves ... we could test devurls something like - - // // == Login - // headlessLogin(ctx, t, c) - - // // == urls ls should fail w/o supplying an envname - // c.Run(ctx, "coder urls ls").Assert(t, - // tcli.Error(), - // ) - - // // == env creation should succeed - // c.Run(ctx, "coder envs create env1 --from image1 --cores 1 --ram 2gb --disk 10gb --nogpu").Assert(t, - // tcli.Success()) - - // // == urls ls should succeed for a newly-created environment - // var durl entclient.DevURL - // c.Run(ctx, `coder urls ls -o json`).Assert(t, - // tcli.Success(), - // jsonUnmarshals(&durl), // though if a new env, durl should be empty - // ) - - // // == devurl creation w/default PRIVATE access - // c.Run(ctx, `coder urls create env1 3000`).Assert(t, - // tcli.Success()) - - // // == devurl create w/access == AUTHED - // c.Run(ctx, `coder urls create env1 3001 --access=AUTHED`).Assert(t, - // tcli.Success()) - - // // == devurl create with name - // c.Run(ctx, `coder urls create env1 3002 --access=PUBLIC --name=foobar`).Assert(t, - // tcli.Success()) - - // // == devurl ls should return well-formed entries incl. one with AUTHED access - // c.Run(ctx, `coder urls ls env1 -o json | jq -c '.[] | select( .access == "AUTHED")'`).Assert(t, - // tcli.Success(), - // jsonUnmarshals(&durl)) - - // // == devurl ls should return well-formed entries incl. one with name 'foobar' - // c.Run(ctx, `coder urls ls env1 -o json | jq -c '.[] | select( .name == "foobar")'`).Assert(t, - // tcli.Success(), - // jsonUnmarshals(&durl)) - - // // == devurl rm should function - // c.Run(ctx, `coder urls rm env1 3002`).Assert(t, - // tcli.Success()) - - // // == removed devurl should no longer be there - // c.Run(ctx, `coder urls ls env1 -o json | jq -c '.[] | select( .name == "foobar")'`).Assert(t, - // tcli.Error(), - // jsonUnmarshals(&durl)) - -} diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go deleted file mode 100644 index eccf68a2..00000000 --- a/ci/integration/integration_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package integration - -import ( - "context" - "math/rand" - "testing" - "time" - - "cdr.dev/coder-cli/ci/tcli" - "cdr.dev/slog/sloggers/slogtest/assert" -) - -func run(t *testing.T, container string, execute func(t *testing.T, ctx context.Context, runner *tcli.ContainerRunner)) { - t.Run(container, func(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: container, - BindMounts: map[string]string{ - binpath: "/bin/coder", - }, - }) - assert.Success(t, "new run container", err) - defer c.Close() - - execute(t, ctx, c) - }) -} - -func TestCoderCLI(t *testing.T) { - t.Parallel() - run(t, "test-coder-cli", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { - 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.StdoutMatches("Available Commands"), - ) - - headlessLogin(ctx, t, c) - - c.Run(ctx, "coder envs").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder envs ls").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder urls").Assert(t, - tcli.Success(), - ) - - 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 ls").Assert(t, - tcli.Error(), - ) - }) - -} - -var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) - -func randString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - b := make([]byte, length) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] - } - return string(b) -} diff --git a/ci/integration/login_test.go b/ci/integration/login_test.go deleted file mode 100644 index e0334f00..00000000 --- a/ci/integration/login_test.go +++ /dev/null @@ -1,75 +0,0 @@ -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/integration/secrets_test.go b/ci/integration/secrets_test.go deleted file mode 100644 index fadcc84e..00000000 --- a/ci/integration/secrets_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package integration - -import ( - "context" - "fmt" - "regexp" - "testing" - - "cdr.dev/coder-cli/ci/tcli" -) - -func TestSecrets(t *testing.T) { - t.Parallel() - run(t, "secrets-cli-tests", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { - headlessLogin(ctx, t, c) - - c.Run(ctx, "coder secrets ls").Assert(t, - tcli.Success(), - ) - - name, value := randString(8), randString(8) - - c.Run(ctx, "coder secrets create").Assert(t, - tcli.Error(), - ) - - // this tests the "Value:" prompt fallback - c.Run(ctx, fmt.Sprintf("echo %s | coder secrets create %s --from-prompt", value, name)).Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - ) - - c.Run(ctx, "coder secrets ls").Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches("Value"), - tcli.StdoutMatches(regexp.QuoteMeta(name)), - ) - - c.Run(ctx, "coder secrets view "+name).Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - tcli.StdoutMatches(regexp.QuoteMeta(value)), - ) - - c.Run(ctx, "coder secrets rm").Assert(t, - tcli.Error(), - ) - c.Run(ctx, "coder secrets rm "+name).Assert(t, - tcli.Success(), - ) - c.Run(ctx, "coder secrets view "+name).Assert(t, - tcli.Error(), - tcli.StdoutEmpty(), - ) - - name, value = randString(8), randString(8) - - c.Run(ctx, fmt.Sprintf("coder secrets create %s --from-literal %s", name, value)).Assert(t, - tcli.Success(), - tcli.StderrEmpty(), - ) - - c.Run(ctx, "coder secrets view "+name).Assert(t, - tcli.Success(), - tcli.StdoutMatches(regexp.QuoteMeta(value)), - ) - - name, value = randString(8), randString(8) - c.Run(ctx, fmt.Sprintf("echo %s > ~/secret.json", value)).Assert(t, - tcli.Success(), - ) - c.Run(ctx, fmt.Sprintf("coder secrets create %s --from-file ~/secret.json", name)).Assert(t, - tcli.Success(), - ) - c.Run(ctx, "coder secrets view "+name).Assert(t, - tcli.Success(), - tcli.StdoutMatches(regexp.QuoteMeta(value)), - ) - }) -} diff --git a/ci/integration/setup_test.go b/ci/integration/setup_test.go deleted file mode 100644 index 566f40e9..00000000 --- a/ci/integration/setup_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package integration - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "cdr.dev/coder-cli/ci/tcli" - "golang.org/x/xerrors" -) - -// binpath is populated during package initialization with a path to the coder binary -var binpath string - -// initialize integration tests by building the coder-cli binary -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) - } -} - -// build the coder-cli binary and move to the integration testing bin directory -func build(path string) error { - tar := "coder-cli-linux-amd64.tar.gz" - dir := filepath.Dir(path) - cmd := exec.Command( - "sh", "-c", - fmt.Sprintf( - "cd ../../ && mkdir -p %s && ./ci/steps/build.sh && cp ./ci/bin/%s %s/ && tar -xzf %s -C %s", - dir, tar, dir, filepath.Join(dir, tar), dir), - ) - - out, err := cmd.CombinedOutput() - if err != nil { - return xerrors.Errorf("build coder-cli (%v): %w", string(out), err) - } - return nil -} - -// 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(), - ) -} diff --git a/ci/integration/ssh_test.go b/ci/integration/ssh_test.go deleted file mode 100644 index 3882b719..00000000 --- a/ci/integration/ssh_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package integration - -import ( - "context" - "testing" - - "cdr.dev/coder-cli/ci/tcli" - "cdr.dev/coder-cli/coder-sdk" -) - -func TestSSH(t *testing.T) { - t.Parallel() - run(t, "ssh-coder-cli-tests", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { - headlessLogin(ctx, t, c) - - // TODO remove this once we can create an environment if there aren't any - var envs []coder.Environment - c.Run(ctx, "coder envs ls --output json").Assert(t, - tcli.Success(), - tcli.StdoutJSONUnmarshal(&envs), - ) - - assert := tcli.Success() - - // if we don't have any environments, "coder config-ssh" will fail - if len(envs) == 0 { - assert = tcli.Error() - } - c.Run(ctx, "coder config-ssh").Assert(t, - assert, - ) - }) -} diff --git a/ci/integration/users_test.go b/ci/integration/users_test.go deleted file mode 100644 index 2138aeed..00000000 --- a/ci/integration/users_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package integration - -import ( - "context" - "testing" - - "cdr.dev/coder-cli/ci/tcli" - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/slog/sloggers/slogtest/assert" -) - -func TestUsers(t *testing.T) { - t.Parallel() - run(t, "users-cli-tests", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { - c.Run(ctx, "which coder").Assert(t, - tcli.Success(), - tcli.StdoutMatches("/usr/sbin/coder"), - tcli.StderrEmpty(), - ) - - headlessLogin(ctx, t, c) - - var user coder.User - c.Run(ctx, `coder users ls --output json | jq -c '.[] | select( .username == "admin")'`).Assert(t, - tcli.Success(), - tcli.StdoutJSONUnmarshal(&user), - ) - assert.Equal(t, "user email is as expected", "admin", user.Email) - assert.Equal(t, "name is as expected", "admin", user.Name) - - c.Run(ctx, "coder users ls --output human | grep admin").Assert(t, - tcli.Success(), - tcli.StdoutMatches("admin"), - ) - - c.Run(ctx, "coder logout").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder users ls").Assert(t, - tcli.Error(), - ) - }) -} diff --git a/ci/steps/build.sh b/ci/steps/build.sh deleted file mode 100755 index cf7d968e..00000000 --- a/ci/steps/build.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# Make pushd and popd silent -pushd() { builtin pushd "$@" >/dev/null; } -popd() { builtin popd >/dev/null; } - -set -euo pipefail -cd "$(dirname "$0")" - -tag=$(git describe --tags) - -build() { - echo "Building coder-cli for $GOOS-$GOARCH..." - - tmpdir=$(mktemp -d) - go build -ldflags "-X main.version=${tag}" -o "$tmpdir/coder" ../../cmd/coder - - pushd "$tmpdir" - if [[ "$GOOS" == "windows" ]]; then - artifact="coder-cli-$GOOS-$GOARCH.zip" - mv coder coder.exe - zip "$artifact" coder.exe - else - artifact="coder-cli-$GOOS-$GOARCH.tar.gz" - tar -czf "$artifact" coder - fi - popd - - mkdir -p ../bin - cp "$tmpdir/$artifact" ../bin/$artifact - rm -rf "$tmpdir" -} - -# Darwin builds do not work from Linux, so only try to build them from Darwin. -# See: https://github.com/cdr/coder-cli/issues/20 -if [[ "$(uname)" == "Darwin" ]]; then - CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 build -else - echo "Warning: Darwin builds don't work on Linux." - echo "Please use an OSX machine to build Darwin tars." -fi - -GOOS=linux GOARCH=amd64 build -GOOS=windows GOARCH=386 build diff --git a/ci/steps/fmt.sh b/ci/steps/fmt.sh deleted file mode 100755 index c202dab9..00000000 --- a/ci/steps/fmt.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "Formatting..." - -go mod tidy -gofmt -w -s . -goimports -w "-local=$$(go list -m)" . - -if [[ ${CI-} && $(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 diff --git a/ci/steps/gendocs.sh b/ci/steps/gendocs.sh deleted file mode 100755 index 64a3776a..00000000 --- a/ci/steps/gendocs.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "Generating docs..." - -cd "$(dirname "$0")" -cd ../../ - -go run ./cmd/coder gen-docs ./docs - -# remove cobra footer from each file -for filename in ./docs/*.md; do - trimmed=$(head -n -1 "$filename") - echo "$trimmed" > $filename -done - - -if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then - echo "Documentation needs generation:" - git -c color.ui=always status | grep --color=no '\e\[31m' - echo "Please run the following locally:" - echo " ./ci/steps/gendocs.sh" - exit 1 -fi diff --git a/ci/steps/lint.sh b/ci/steps/lint.sh deleted file mode 100755 index c3f72614..00000000 --- a/ci/steps/lint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "Linting..." - -go vet ./... -golint -set_exit_status ./... diff --git a/ci/tcli/doc.go b/ci/tcli/doc.go deleted file mode 100644 index 561dc480..00000000 --- a/ci/tcli/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// 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 deleted file mode 100644 index 51ec6f55..00000000 --- a/ci/tcli/tcli.go +++ /dev/null @@ -1,350 +0,0 @@ -package tcli - -import ( - "bytes" - "context" - "encoding/json" - "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, - "--network", "host", - "-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( - "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( - "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 - ) - if a.cmd == nil { - slogtest.Fatal(t, "test failed to initialize: no command specified") - } - - 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, "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()), - ) - } - } -} - -// StdoutJSONUnmarshal attempts to unmarshal stdout into the given target -func StdoutJSONUnmarshal(target interface{}) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - err := json.Unmarshal(r.Stdout, target) - assert.Success(t, "stdout json unmarshals", err) - } -} - -// StderrJSONUnmarshal attempts to unmarshal stderr into the given target -func StderrJSONUnmarshal(target interface{}) Assertion { - return func(t *testing.T, r *CommandResult) { - slog.Helper() - err := json.Unmarshal(r.Stdout, target) - assert.Success(t, "stderr json unmarshals", err) - } -} diff --git a/ci/tcli/tcli_test.go b/ci/tcli/tcli_test.go deleted file mode 100644 index 97bd1b6e..00000000 --- a/ci/tcli/tcli_test.go +++ /dev/null @@ -1,69 +0,0 @@ -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/main.go b/cmd/coder/main.go deleted file mode 100644 index 5f2c0c9c..00000000 --- a/cmd/coder/main.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "net/http" - _ "net/http/pprof" - "os" - "runtime" - - "cdr.dev/coder-cli/internal/cmd" - "cdr.dev/coder-cli/internal/x/xterminal" - - "go.coder.com/flog" -) - -// Using a global for the version so it can be set at build time using ldflags. -var version = "unknown" - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // If requested, spin up the pprof webserver. - if os.Getenv("PPROF") != "" { - go func() { - log.Println(http.ListenAndServe("localhost:6060", nil)) - }() - } - - stdoutState, err := xterminal.MakeOutputRaw(os.Stdout.Fd()) - if err != nil { - flog.Fatal("set output to raw: %s", err) - } - defer func() { - // Best effort. Would result in broken terminal on window but nothing we can do about it. - _ = xterminal.Restore(os.Stdout.Fd(), stdoutState) - }() - - app := cmd.Make() - app.Version = fmt.Sprintf("%s %s %s/%s", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) - - if err := app.ExecuteContext(ctx); err != nil { - // NOTE: The returned error is already handled and logged by the cmd lib (cobra), so no need to re-handle it here. - // As we are in the main, if there was an error, exit the process with an error code. - os.Exit(1) - } -} diff --git a/coder-sdk/activity.go b/coder-sdk/activity.go deleted file mode 100644 index 1aaeb301..00000000 --- a/coder-sdk/activity.go +++ /dev/null @@ -1,27 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -type activityRequest struct { - Source string `json:"source"` - EnvironmentID string `json:"environment_id"` -} - -// PushActivity pushes CLI activity to Coder. -func (c Client) PushActivity(ctx context.Context, source, envID string) error { - resp, err := c.request(ctx, http.MethodPost, "/api/metrics/usage/push", activityRequest{ - Source: source, - EnvironmentID: envID, - }) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return bodyError(resp) - } - return nil -} diff --git a/coder-sdk/client.go b/coder-sdk/client.go deleted file mode 100644 index 959c7611..00000000 --- a/coder-sdk/client.go +++ /dev/null @@ -1,46 +0,0 @@ -package coder - -import ( - "net/http" - "net/http/cookiejar" - "net/url" -) - -// Me is the route param to access resources of the authenticated user -const Me = "me" - -// Client wraps the Coder HTTP API -type Client struct { - BaseURL *url.URL - Token string -} - -// newHTTPClient creates a default underlying http client and sets the auth cookie. -// -// NOTE: As we do not specify a custom transport, the default one from the stdlib will be used, -// resulting in a persistent connection pool. -// We do not set a timeout here as it could cause issue with the websocket. -// The caller is expected to set it when needed. -// -// WARNING: If the caller sets a custom transport to set TLS settings or a custom CA, the default -// pool will not be used and it might result in a new dns lookup/tls session/socket begin -// established each time. -func (c *Client) newHTTPClient() (*http.Client, error) { - jar, err := cookiejar.New(nil) - if err != nil { - return nil, err - } - - jar.SetCookies(c.BaseURL, []*http.Cookie{ - { - Name: "session_token", - Value: c.Token, - MaxAge: 86400, - Path: "/", - HttpOnly: true, - Secure: c.BaseURL.Scheme == "https", - }, - }) - - return &http.Client{Jar: jar}, nil -} diff --git a/coder-sdk/devurl.go b/coder-sdk/devurl.go deleted file mode 100644 index e5c387e6..00000000 --- a/coder-sdk/devurl.go +++ /dev/null @@ -1,94 +0,0 @@ -package coder - -import ( - "context" - "fmt" - "net/http" -) - -// DevURL is the parsed json response record for a devURL from cemanager. -type DevURL struct { - ID string `json:"id" tab:"ID"` - URL string `json:"url" tab:"URL"` - Port int `json:"port" tab:"Port"` - Access string `json:"access" tab:"Access"` - Name string `json:"name" tab:"Name"` -} - -type delDevURLRequest struct { - EnvID string `json:"environment_id"` - DevURLID string `json:"url_id"` -} - -// DelDevURL deletes the specified devurl. -func (c Client) DelDevURL(ctx context.Context, envID, urlID string) error { - reqURL := fmt.Sprintf("/api/environments/%s/devurls/%s", envID, urlID) - - resp, err := c.request(ctx, http.MethodDelete, reqURL, delDevURLRequest{ - EnvID: envID, - DevURLID: urlID, - }) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() // Best effort. Likely connection drop. - - if resp.StatusCode != http.StatusOK { - return bodyError(resp) - } - - return nil -} - -type createDevURLRequest struct { - EnvID string `json:"environment_id"` - Port int `json:"port"` - Access string `json:"access"` - Name string `json:"name"` -} - -// InsertDevURL inserts a new devurl for the authenticated user. -func (c Client) InsertDevURL(ctx context.Context, envID string, port int, name, access string) error { - reqURL := fmt.Sprintf("/api/environments/%s/devurls", envID) - - resp, err := c.request(ctx, http.MethodPost, reqURL, createDevURLRequest{ - EnvID: envID, - Port: port, - Access: access, - Name: name, - }) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() // Best effort. Likely connection drop. - - if resp.StatusCode != http.StatusOK { - return bodyError(resp) - } - - return nil -} - -type updateDevURLRequest createDevURLRequest - -// UpdateDevURL updates an existing devurl for the authenticated user. -func (c Client) UpdateDevURL(ctx context.Context, envID, urlID string, port int, name, access string) error { - reqURL := fmt.Sprintf("/api/environments/%s/devurls/%s", envID, urlID) - - resp, err := c.request(ctx, http.MethodPut, reqURL, updateDevURLRequest{ - EnvID: envID, - Port: port, - Access: access, - Name: name, - }) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() // Best effort. Likefly connection drop. - - if resp.StatusCode != http.StatusOK { - return bodyError(resp) - } - - return nil -} diff --git a/coder-sdk/env.go b/coder-sdk/env.go deleted file mode 100644 index 36214493..00000000 --- a/coder-sdk/env.go +++ /dev/null @@ -1,183 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" - - "cdr.dev/coder-cli/internal/x/xjson" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" -) - -// Environment describes a Coder environment -type Environment struct { - ID string `json:"id" tab:"-"` - Name string `json:"name" tab:"Name"` - ImageID string `json:"image_id" tab:"-"` - ImageTag string `json:"image_tag" tab:"ImageTag"` - OrganizationID string `json:"organization_id" tab:"-"` - UserID string `json:"user_id" tab:"-"` - LastBuiltAt time.Time `json:"last_built_at" tab:"-"` - CPUCores float32 `json:"cpu_cores" tab:"CPUCores"` - MemoryGB int `json:"memory_gb" tab:"MemoryGB"` - DiskGB int `json:"disk_gb" tab:"DiskGB"` - GPUs int `json:"gpus" tab:"GPUs"` - Updating bool `json:"updating" tab:"Updating"` - LatestStat EnvironmentStat `json:"latest_stat" tab:"Status"` - RebuildMessages []RebuildMessage `json:"rebuild_messages" tab:"-"` - CreatedAt time.Time `json:"created_at" tab:"-"` - UpdatedAt time.Time `json:"updated_at" tab:"-"` - LastOpenedAt time.Time `json:"last_opened_at" tab:"-"` - LastConnectionAt time.Time `json:"last_connection_at" tab:"-"` - AutoOffThreshold xjson.MSDuration `json:"auto_off_threshold" tab:"-"` -} - -// RebuildMessage defines the message shown when an Environment requires a rebuild for it can be accessed. -type RebuildMessage struct { - Text string `json:"text"` - Required bool `json:"required"` - AutoOffThreshold xjson.MSDuration `json:"auto_off_threshold" tab:"-"` - RebuildMessages []struct { - Text string `json:"text"` - Required bool `json:"required"` - } `json:"rebuild_messages" tab:"-"` -} - -// EnvironmentStat represents the state of an environment -type EnvironmentStat struct { - Time time.Time `json:"time"` - LastOnline time.Time `json:"last_online"` - ContainerStatus EnvironmentStatus `json:"container_status"` - StatError string `json:"stat_error"` - CPUUsage float32 `json:"cpu_usage"` - MemoryTotal int64 `json:"memory_total"` - MemoryUsage float32 `json:"memory_usage"` - DiskTotal int64 `json:"disk_total"` - DiskUsed int64 `json:"disk_used"` -} - -func (e EnvironmentStat) String() string { return string(e.ContainerStatus) } - -// EnvironmentStatus refers to the states of an environment. -type EnvironmentStatus string - -// The following represent the possible environment container states -const ( - EnvironmentCreating EnvironmentStatus = "CREATING" - EnvironmentOff EnvironmentStatus = "OFF" - EnvironmentOn EnvironmentStatus = "ON" - EnvironmentFailed EnvironmentStatus = "FAILED" - EnvironmentUnknown EnvironmentStatus = "UNKNOWN" -) - -// CreateEnvironmentRequest is used to configure a new environment. -type CreateEnvironmentRequest struct { - Name string `json:"name"` - ImageID string `json:"image_id"` - ImageTag string `json:"image_tag"` - CPUCores float32 `json:"cpu_cores"` - MemoryGB int `json:"memory_gb"` - DiskGB int `json:"disk_gb"` - GPUs int `json:"gpus"` - Services []string `json:"services"` -} - -// CreateEnvironment sends a request to create an environment. -func (c Client) CreateEnvironment(ctx context.Context, orgID string, req CreateEnvironmentRequest) (*Environment, error) { - var env Environment - if err := c.requestBody(ctx, http.MethodPost, "/api/orgs/"+orgID+"/environments", req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// EnvironmentsByOrganization gets the list of environments owned by the given user. -func (c Client) EnvironmentsByOrganization(ctx context.Context, userID, orgID string) ([]Environment, error) { - var envs []Environment - if err := c.requestBody(ctx, http.MethodGet, "/api/orgs/"+orgID+"/members/"+userID+"/environments", nil, &envs); err != nil { - return nil, err - } - return envs, nil -} - -// DeleteEnvironment deletes the environment. -func (c Client) DeleteEnvironment(ctx context.Context, envID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/environments/"+envID, nil, nil) -} - -// StopEnvironment stops the stops. -func (c Client) StopEnvironment(ctx context.Context, envID string) error { - return c.requestBody(ctx, http.MethodPut, "/api/environments/"+envID+"/stop", nil, nil) -} - -// DialWsep dials an environments command execution interface -// See https://github.com/cdr/wsep for details. -func (c Client) DialWsep(ctx context.Context, env *Environment) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/proxy/environments/"+env.ID+"/wsep") -} - -// DialIDEStatus opens a websocket connection for cpu load metrics on the environment -func (c Client) DialIDEStatus(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/ide/api/status") -} - -// DialEnvironmentBuildLog opens a websocket connection for the environment build log messages. -func (c Client) DialEnvironmentBuildLog(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/environments/"+envID+"/watch-update") -} - -// DialEnvironmentStats opens a websocket connection for environment stats. -func (c Client) DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/environments/"+envID+"/watch-stats") -} - -// DialResourceLoad opens a websocket connection for cpu load metrics on the environment -func (c Client) DialResourceLoad(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/environments/"+envID+"/watch-resource-load") -} - -// BuildLogType describes the type of an event. -type BuildLogType string - -const ( - // BuildLogTypeStart signals that a new build log has begun. - BuildLogTypeStart BuildLogType = "start" - // BuildLogTypeStage is a stage-level event for an environment. - // It can be thought of as a major step in the environment's - // lifecycle. - BuildLogTypeStage BuildLogType = "stage" - // BuildLogTypeError describes an error that has occurred. - BuildLogTypeError BuildLogType = "error" - // BuildLogTypeSubstage describes a subevent that occurs as - // part of a stage. This can be the output from a user's - // personalization script, or a long running command. - BuildLogTypeSubstage BuildLogType = "substage" - // BuildLogTypeDone signals that the build has completed. - BuildLogTypeDone BuildLogType = "done" -) - -type buildLogMsg struct { - Type BuildLogType `json:"type"` -} - -// WaitForEnvironmentReady will watch the build log and return when done -func (c Client) WaitForEnvironmentReady(ctx context.Context, env *Environment) error { - conn, err := c.DialEnvironmentBuildLog(ctx, env.ID) - if err != nil { - return xerrors.Errorf("%s: dial build log: %w", env.Name, err) - } - - for { - msg := buildLogMsg{} - err := wsjson.Read(ctx, conn, &msg) - if err != nil { - return xerrors.Errorf("%s: reading build log msg: %w", env.Name, err) - } - - if msg.Type == BuildLogTypeDone { - return nil - } - } -} diff --git a/coder-sdk/error.go b/coder-sdk/error.go deleted file mode 100644 index 2f365a47..00000000 --- a/coder-sdk/error.go +++ /dev/null @@ -1,46 +0,0 @@ -package coder - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httputil" - - "golang.org/x/xerrors" -) - -// ErrNotFound describes an error case in which the requested resource could not be found -var ErrNotFound = xerrors.Errorf("resource not found") - -// apiError is the expected payload format for our errors. -type apiError struct { - Err struct { - Msg string `json:"msg"` - } `json:"error"` -} - -// HTTPError represents an error from the Coder API. -type HTTPError struct { - *http.Response -} - -func (e *HTTPError) Error() string { - dump, err := httputil.DumpResponse(e.Response, false) - if err != nil { - return fmt.Sprintf("dump response: %+v", err) - } - - var msg apiError - // Try to decode the payload as an error, if it fails or if there is no error message, - // return the response URL with the dump. - if err := json.NewDecoder(e.Response.Body).Decode(&msg); err != nil || msg.Err.Msg == "" { - return fmt.Sprintf("%s\n%s", e.Response.Request.URL, dump) - } - - // If the payload was a in the expected error format with a message, include it. - return fmt.Sprintf("%s\n%s%s", e.Response.Request.URL, dump, msg.Err.Msg) -} - -func bodyError(resp *http.Response) error { - return &HTTPError{resp} -} diff --git a/coder-sdk/image.go b/coder-sdk/image.go deleted file mode 100644 index ba59e8bf..00000000 --- a/coder-sdk/image.go +++ /dev/null @@ -1,49 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -// Image describes a Coder Image -type Image struct { - ID string `json:"id"` - OrganizationID string `json:"organization_id"` - Repository string `json:"repository"` - Description string `json:"description"` - URL string `json:"url"` // User-supplied URL for image. - DefaultCPUCores float32 `json:"default_cpu_cores"` - DefaultMemoryGB int `json:"default_memory_gb"` - DefaultDiskGB int `json:"default_disk_gb"` - Deprecated bool `json:"deprecated"` -} - -// NewRegistryRequest describes a docker registry used in importing an image -type NewRegistryRequest struct { - FriendlyName string `json:"friendly_name"` - Registry string `json:"registry"` - Username string `json:"username"` - Password string `json:"password"` -} - -// ImportImageRequest is used to import new images and registries into Coder -type ImportImageRequest struct { - RegistryID *string `json:"registry_id"` // Used to import images to existing registries. - NewRegistry *NewRegistryRequest `json:"new_registry"` // Used when adding a new registry. - Repository string `json:"repository"` // Refers to the image. Ex: "codercom/ubuntu". - Tag string `json:"tag"` - DefaultCPUCores float32 `json:"default_cpu_cores"` - DefaultMemoryGB int `json:"default_memory_gb"` - DefaultDiskGB int `json:"default_disk_gb"` - Description string `json:"description"` - URL string `json:"url"` -} - -// ImportImage creates a new image and optionally a new registry -func (c Client) ImportImage(ctx context.Context, orgID string, req ImportImageRequest) (*Image, error) { - var img Image - if err := c.requestBody(ctx, http.MethodPost, "/api/orgs/"+orgID+"/images", req, &img); err != nil { - return nil, err - } - return &img, nil -} diff --git a/coder-sdk/org.go b/coder-sdk/org.go deleted file mode 100644 index 10158c40..00000000 --- a/coder-sdk/org.go +++ /dev/null @@ -1,22 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -// 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(ctx context.Context) ([]Org, error) { - var orgs []Org - if err := c.requestBody(ctx, http.MethodGet, "/api/orgs", nil, &orgs); err != nil { - return nil, err - } - return orgs, nil -} diff --git a/coder-sdk/request.go b/coder-sdk/request.go deleted file mode 100644 index 4ae2bdf2..00000000 --- a/coder-sdk/request.go +++ /dev/null @@ -1,64 +0,0 @@ -package coder - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - - "golang.org/x/xerrors" -) - -// request is a helper to set the cookie, marshal the payload and execute the request. -func (c Client) request(ctx context.Context, method, path string, in interface{}) (*http.Response, error) { - // Create a default http client with the auth in the cookie. - client, err := c.newHTTPClient() - if err != nil { - return nil, xerrors.Errorf("new http client: %w", err) - } - - // If we have incoming data, encode it as json. - var payload io.Reader - if in != nil { - body, err := json.Marshal(in) - if err != nil { - return nil, xerrors.Errorf("marshal request: %w", err) - } - payload = bytes.NewReader(body) - } - - // Create the http request. - req, err := http.NewRequestWithContext(ctx, method, c.BaseURL.String()+path, payload) - if err != nil { - return nil, xerrors.Errorf("create request: %w", err) - } - - // Execute the request. - return client.Do(req) -} - -// requestBody is a helper extending the Client.request helper, checking the response code -// and decoding the response payload. -func (c Client) requestBody(ctx context.Context, method, path string, in, out interface{}) error { - resp, err := c.request(ctx, method, path, in) - if err != nil { - return xerrors.Errorf("Execute request: %q", err) - } - defer func() { _ = resp.Body.Close() }() // Best effort, likely connection dropped. - - // Responses in the 100 are handled by the http lib, in the 200 range, we have a success. - // Consider anything at or above 300 to be an error. - if resp.StatusCode > 299 { - return fmt.Errorf("unexpected status code: %w", bodyError(resp)) - } - - // If we expect a payload, process it as json. - if out != nil { - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return xerrors.Errorf("decode response body: %w", err) - } - } - return nil -} diff --git a/coder-sdk/secrets.go b/coder-sdk/secrets.go deleted file mode 100644 index 21a1583b..00000000 --- a/coder-sdk/secrets.go +++ /dev/null @@ -1,94 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// Secret describes a Coder secret -type Secret struct { - ID string `json:"id" tab:"-"` - Name string `json:"name" tab:"Name"` - Value string `json:"value,omitempty" tab:"Value"` - Description string `json:"description" tab:"Description"` - CreatedAt time.Time `json:"created_at" tab:"CreatedAt"` - UpdatedAt time.Time `json:"updated_at" tab:"-"` -} - -// Secrets gets all secrets for the given user -func (c *Client) Secrets(ctx context.Context, userID string) ([]Secret, error) { - var secrets []Secret - if err := c.requestBody(ctx, http.MethodGet, "/api/users/"+userID+"/secrets", nil, &secrets); err != nil { - return nil, err - } - return secrets, nil -} - -// SecretWithValueByName gets the Coder secret with its value by its name. -func (c *Client) SecretWithValueByName(ctx context.Context, name, userID string) (*Secret, error) { - // Lookup the secret from the name. - s, err := c.SecretByName(ctx, name, userID) - if err != nil { - return nil, err - } - // Pull the secret value. - // NOTE: This is racy, but acceptable. If the secret is gone or the permission changed since we looked up the id, - // the call will simply fail and surface the error to the user. - var secret Secret - if err := c.requestBody(ctx, http.MethodGet, "/api/users/"+userID+"/secrets/"+s.ID, nil, &secret); err != nil { - return nil, err - } - return &secret, nil -} - -// SecretWithValueByID gets the Coder secret with its value by the secret_id. -func (c *Client) SecretWithValueByID(ctx context.Context, id, userID string) (*Secret, error) { - var secret Secret - if err := c.requestBody(ctx, http.MethodGet, "/api/users/"+userID+"/secrets/"+id, nil, &secret); err != nil { - return nil, err - } - return &secret, nil -} - -// SecretByName gets a secret object by name -func (c *Client) SecretByName(ctx context.Context, name, userID string) (*Secret, error) { - secrets, err := c.Secrets(ctx, userID) - if err != nil { - return nil, err - } - for _, s := range secrets { - if s.Name == name { - return &s, nil - } - } - return nil, ErrNotFound -} - -// InsertSecretReq describes the request body for creating a new secret. -type InsertSecretReq struct { - Name string `json:"name"` - Value string `json:"value"` - Description string `json:"description"` -} - -// InsertSecret adds a new secret for the authed user -func (c *Client) InsertSecret(ctx context.Context, user *User, req InsertSecretReq) error { - return c.requestBody(ctx, http.MethodPost, "/api/users/"+user.ID+"/secrets", req, nil) -} - -// DeleteSecretByName deletes the authenticated users secret with the given name -func (c *Client) DeleteSecretByName(ctx context.Context, name, userID string) error { - // Lookup the secret by name to get the ID. - secret, err := c.SecretByName(ctx, name, userID) - if err != nil { - return err - } - // Delete the secret. - // NOTE: This is racy, but acceptable. If the secret is gone or the permission changed since we looked up the id, - // the call will simply fail and surface the error to the user. - if _, err := c.request(ctx, http.MethodDelete, "/api/users/"+userID+"/secrets/"+secret.ID, nil); err != nil { - return err - } - return nil -} diff --git a/coder-sdk/users.go b/coder-sdk/users.go deleted file mode 100644 index 1008f38b..00000000 --- a/coder-sdk/users.go +++ /dev/null @@ -1,72 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// User describes a Coder user account. -type User struct { - ID string `json:"id" tab:"-"` - Email string `json:"email" tab:"Email"` - Username string `json:"username" tab:"Username"` - Name string `json:"name" tab:"Name"` - CreatedAt time.Time `json:"created_at" tab:"CreatedAt"` - UpdatedAt time.Time `json:"updated_at" tab:"-"` -} - -// Me gets the details of the authenticated user. -func (c Client) Me(ctx context.Context) (*User, error) { - return c.UserByID(ctx, Me) -} - -// UserByID get the details of a user by their id. -func (c Client) UserByID(ctx context.Context, id string) (*User, error) { - var u User - if err := c.requestBody(ctx, http.MethodGet, "/api/users/"+id, nil, &u); err != nil { - return nil, err - } - 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(ctx context.Context) (*SSHKey, error) { - var key SSHKey - if err := c.requestBody(ctx, http.MethodGet, "/api/users/me/sshkey", nil, &key); err != nil { - return nil, err - } - return &key, nil -} - -// Users gets the list of user accounts. -func (c Client) Users(ctx context.Context) ([]User, error) { - var u []User - if err := c.requestBody(ctx, http.MethodGet, "/api/users", nil, &u); err != nil { - return nil, err - } - return u, nil -} - -// UserByEmail gets a user by email. -func (c Client) UserByEmail(ctx context.Context, email string) (*User, error) { - if email == Me { - return c.Me(ctx) - } - users, err := c.Users(ctx) - if err != nil { - return nil, err - } - for _, u := range users { - if u.Email == email { - return &u, nil - } - } - return nil, ErrNotFound -} diff --git a/coder-sdk/ws.go b/coder-sdk/ws.go deleted file mode 100644 index 5dad6293..00000000 --- a/coder-sdk/ws.go +++ /dev/null @@ -1,30 +0,0 @@ -package coder - -import ( - "context" - "net/http" - - "nhooyr.io/websocket" -) - -// dialWebsocket establish the websocket connection while setting the authentication header. -func (c Client) dialWebsocket(ctx context.Context, path string) (*websocket.Conn, error) { - // Make a copy of the url so we can update the scheme to ws(s) without mutating the state. - url := *c.BaseURL - if url.Scheme == "https" { - url.Scheme = "wss" - } else { - url.Scheme = "ws" - } - url.Path = path - - conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{HTTPHeader: http.Header{"Session-Token": {c.Token}}}) - if err != nil { - if resp != nil { - return nil, bodyError(resp) - } - return nil, err - } - - return conn, nil -} diff --git a/docs/coder.md b/docs/coder.md deleted file mode 100644 index 378a9c66..00000000 --- a/docs/coder.md +++ /dev/null @@ -1,26 +0,0 @@ -## coder - -coder provides a CLI for working with an existing Coder Enterprise installation - -### Synopsis - -coder provides a CLI for working with an existing Coder Enterprise installation - -### Options - -``` - -h, --help help for coder -``` - -### SEE ALSO - -* [coder completion](coder_completion.md) - Generate completion script -* [coder config-ssh](coder_config-ssh.md) - Configure SSH to access Coder environments -* [coder envs](coder_envs.md) - Interact with Coder environments -* [coder login](coder_login.md) - Authenticate this client for future operations -* [coder logout](coder_logout.md) - Remove local authentication credentials if any exist -* [coder secrets](coder_secrets.md) - Interact with Coder Secrets -* [coder sh](coder_sh.md) - Open a shell and execute commands in a Coder environment -* [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder environment -* [coder urls](coder_urls.md) - Interact with environment DevURLs -* [coder users](coder_users.md) - Interact with Coder user accounts diff --git a/docs/coder_completion.md b/docs/coder_completion.md deleted file mode 100644 index 39b862af..00000000 --- a/docs/coder_completion.md +++ /dev/null @@ -1,63 +0,0 @@ -## coder completion - -Generate completion script - -### Synopsis - -To load completions: - -Bash: - -$ source <(coder completion bash) - -To load completions for each session, execute once: -Linux: - $ coder completion bash > /etc/bash_completion.d/coder -MacOS: - $ coder completion bash > /usr/local/etc/bash_completion.d/coder - -Zsh: - -If shell completion is not already enabled in your environment you will need -to enable it. You can execute the following once: - -$ echo "autoload -U compinit; compinit" >> ~/.zshrc - -To load completions for each session, execute once: -$ coder completion zsh > "${fpath[1]}/_coder" - -You will need to start a new shell for this setup to take effect. - -Fish: - -$ coder completion fish | source - -To load completions for each session, execute once: -$ coder completion fish > ~/.config/fish/completions/coder.fish - - -``` -coder completion [bash|zsh|fish|powershell] -``` - -### Examples - -``` -coder completion fish > ~/.config/fish/completions/coder.fish -coder completion zsh > "${fpath[1]}/_coder" - -Linux: - $ coder completion bash > /etc/bash_completion.d/coder -MacOS: - $ coder completion bash > /usr/local/etc/bash_completion.d/coder -``` - -### Options - -``` - -h, --help help for completion -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md deleted file mode 100644 index 41b697ef..00000000 --- a/docs/coder_config-ssh.md +++ /dev/null @@ -1,23 +0,0 @@ -## coder config-ssh - -Configure SSH to access Coder environments - -### Synopsis - -Inject the proper OpenSSH configuration into your local SSH config file. - -``` -coder config-ssh [flags] -``` - -### Options - -``` - --filepath string overide the default path of your ssh config file (default "~/.ssh/config") - -h, --help help for config-ssh - --remove remove the auto-generated Coder Enterprise ssh config -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation diff --git a/docs/coder_envs.md b/docs/coder_envs.md deleted file mode 100644 index 0cde6725..00000000 --- a/docs/coder_envs.md +++ /dev/null @@ -1,20 +0,0 @@ -## coder envs - -Interact with Coder environments - -### Synopsis - -Perform operations on the Coder environments owned by the active user. - -### Options - -``` - -h, --help help for envs - --user string Specify the user whose resources to target (default "me") -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation -* [coder envs ls](coder_envs_ls.md) - list all environments owned by the active user -* [coder envs stop](coder_envs_stop.md) - stop a Coder environment by name diff --git a/docs/coder_envs_ls.md b/docs/coder_envs_ls.md deleted file mode 100644 index d3535af8..00000000 --- a/docs/coder_envs_ls.md +++ /dev/null @@ -1,28 +0,0 @@ -## coder envs ls - -list all environments owned by the active user - -### Synopsis - -List all Coder environments owned by the active user. - -``` -coder envs ls [flags] -``` - -### Options - -``` - -h, --help help for ls - -o, --output string human | json (default "human") -``` - -### Options inherited from parent commands - -``` - --user string Specify the user whose resources to target (default "me") -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments diff --git a/docs/coder_envs_stop.md b/docs/coder_envs_stop.md deleted file mode 100644 index d5b522a5..00000000 --- a/docs/coder_envs_stop.md +++ /dev/null @@ -1,27 +0,0 @@ -## coder envs stop - -stop a Coder environment by name - -### Synopsis - -Stop a Coder environment by name - -``` -coder envs stop [environment_name] [flags] -``` - -### Options - -``` - -h, --help help for stop -``` - -### Options inherited from parent commands - -``` - --user string Specify the user whose resources to target (default "me") -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments diff --git a/docs/coder_login.md b/docs/coder_login.md deleted file mode 100644 index e943f275..00000000 --- a/docs/coder_login.md +++ /dev/null @@ -1,21 +0,0 @@ -## coder login - -Authenticate this client for future operations - -### Synopsis - -Authenticate this client for future operations - -``` -coder login [Coder Enterprise URL eg. https://my.coder.domain/] [flags] -``` - -### Options - -``` - -h, --help help for login -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation diff --git a/docs/coder_logout.md b/docs/coder_logout.md deleted file mode 100644 index 22bce303..00000000 --- a/docs/coder_logout.md +++ /dev/null @@ -1,21 +0,0 @@ -## coder logout - -Remove local authentication credentials if any exist - -### Synopsis - -Remove local authentication credentials if any exist - -``` -coder logout [flags] -``` - -### Options - -``` - -h, --help help for logout -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation diff --git a/docs/coder_secrets.md b/docs/coder_secrets.md deleted file mode 100644 index ebdd1af2..00000000 --- a/docs/coder_secrets.md +++ /dev/null @@ -1,22 +0,0 @@ -## coder secrets - -Interact with Coder Secrets - -### Synopsis - -Interact with secrets objects owned by the active user. - -### Options - -``` - -h, --help help for secrets - --user string Specify the user whose resources to target (default "me") -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation -* [coder secrets create](coder_secrets_create.md) - Create a new secret -* [coder secrets ls](coder_secrets_ls.md) - List all secrets owned by the active user -* [coder secrets rm](coder_secrets_rm.md) - Remove one or more secrets by name -* [coder secrets view](coder_secrets_view.md) - View a secret by name diff --git a/docs/coder_secrets_create.md b/docs/coder_secrets_create.md deleted file mode 100644 index c10a771e..00000000 --- a/docs/coder_secrets_create.md +++ /dev/null @@ -1,39 +0,0 @@ -## coder secrets create - -Create a new secret - -### Synopsis - -Create a new secret object to store application secrets and access them securely from within your environments. - -``` -coder secrets create [secret_name] [flags] -``` - -### Examples - -``` -coder secrets create mysql-password --from-literal 123password -coder secrets create mysql-password --from-prompt -coder secrets create aws-credentials --from-file ./credentials.json -``` - -### Options - -``` - --description string a description of the secret - --from-file string a file from which to read the value of the secret - --from-literal string the value of the secret - --from-prompt enter the secret value through a terminal prompt - -h, --help help for create -``` - -### Options inherited from parent commands - -``` - --user string Specify the user whose resources to target (default "me") -``` - -### SEE ALSO - -* [coder secrets](coder_secrets.md) - Interact with Coder Secrets diff --git a/docs/coder_secrets_ls.md b/docs/coder_secrets_ls.md deleted file mode 100644 index 40408e8e..00000000 --- a/docs/coder_secrets_ls.md +++ /dev/null @@ -1,27 +0,0 @@ -## coder secrets ls - -List all secrets owned by the active user - -### Synopsis - -List all secrets owned by the active user - -``` -coder secrets ls [flags] -``` - -### Options - -``` - -h, --help help for ls -``` - -### Options inherited from parent commands - -``` - --user string Specify the user whose resources to target (default "me") -``` - -### SEE ALSO - -* [coder secrets](coder_secrets.md) - Interact with Coder Secrets diff --git a/docs/coder_secrets_rm.md b/docs/coder_secrets_rm.md deleted file mode 100644 index d58dc6f0..00000000 --- a/docs/coder_secrets_rm.md +++ /dev/null @@ -1,33 +0,0 @@ -## coder secrets rm - -Remove one or more secrets by name - -### Synopsis - -Remove one or more secrets by name - -``` -coder secrets rm [...secret_name] [flags] -``` - -### Examples - -``` -coder secrets rm mysql-password mysql-user -``` - -### Options - -``` - -h, --help help for rm -``` - -### Options inherited from parent commands - -``` - --user string Specify the user whose resources to target (default "me") -``` - -### SEE ALSO - -* [coder secrets](coder_secrets.md) - Interact with Coder Secrets diff --git a/docs/coder_secrets_view.md b/docs/coder_secrets_view.md deleted file mode 100644 index e5a9770a..00000000 --- a/docs/coder_secrets_view.md +++ /dev/null @@ -1,33 +0,0 @@ -## coder secrets view - -View a secret by name - -### Synopsis - -View a secret by name - -``` -coder secrets view [secret_name] [flags] -``` - -### Examples - -``` -coder secrets view mysql-password -``` - -### Options - -``` - -h, --help help for view -``` - -### Options inherited from parent commands - -``` - --user string Specify the user whose resources to target (default "me") -``` - -### SEE ALSO - -* [coder secrets](coder_secrets.md) - Interact with Coder Secrets diff --git a/docs/coder_sh.md b/docs/coder_sh.md deleted file mode 100644 index 6c88f203..00000000 --- a/docs/coder_sh.md +++ /dev/null @@ -1,27 +0,0 @@ -## coder sh - -Open a shell and execute commands in a Coder environment - -### Synopsis - -Execute a remote command on the environment\nIf no command is specified, the default shell is opened. - -``` -coder sh [environment_name] [] [flags] -``` - -### Examples - -``` -coder sh backend-env -``` - -### Options - -``` - -h, --help help for sh -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation diff --git a/docs/coder_sync.md b/docs/coder_sync.md deleted file mode 100644 index 03ca7a37..00000000 --- a/docs/coder_sync.md +++ /dev/null @@ -1,22 +0,0 @@ -## coder sync - -Establish a one way directory sync to a Coder environment - -### Synopsis - -Establish a one way directory sync to a Coder environment - -``` -coder sync [local directory] [:] [flags] -``` - -### Options - -``` - -h, --help help for sync - --init do initial transfer and exit -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation diff --git a/docs/coder_urls.md b/docs/coder_urls.md deleted file mode 100644 index df4c3c70..00000000 --- a/docs/coder_urls.md +++ /dev/null @@ -1,20 +0,0 @@ -## coder urls - -Interact with environment DevURLs - -### Synopsis - -Interact with environment DevURLs - -### Options - -``` - -h, --help help for urls -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation -* [coder urls create](coder_urls_create.md) - Create a new devurl for an environment -* [coder urls ls](coder_urls_ls.md) - List all DevURLs for an environment -* [coder urls rm](coder_urls_rm.md) - Remove a dev url diff --git a/docs/coder_urls_create.md b/docs/coder_urls_create.md deleted file mode 100644 index 7afc8d8b..00000000 --- a/docs/coder_urls_create.md +++ /dev/null @@ -1,23 +0,0 @@ -## coder urls create - -Create a new devurl for an environment - -### Synopsis - -Create a new devurl for an environment - -``` -coder urls create [env_name] [port] [--access ] [--name ] [flags] -``` - -### Options - -``` - --access string Set DevURL access to [private | org | authed | public] (default "private") - -h, --help help for create - --name string DevURL name -``` - -### SEE ALSO - -* [coder urls](coder_urls.md) - Interact with environment DevURLs diff --git a/docs/coder_urls_ls.md b/docs/coder_urls_ls.md deleted file mode 100644 index 1d01c2e5..00000000 --- a/docs/coder_urls_ls.md +++ /dev/null @@ -1,22 +0,0 @@ -## coder urls ls - -List all DevURLs for an environment - -### Synopsis - -List all DevURLs for an environment - -``` -coder urls ls [environment_name] [flags] -``` - -### Options - -``` - -h, --help help for ls - -o, --output string human|json (default "human") -``` - -### SEE ALSO - -* [coder urls](coder_urls.md) - Interact with environment DevURLs diff --git a/docs/coder_urls_rm.md b/docs/coder_urls_rm.md deleted file mode 100644 index 2b69e2bb..00000000 --- a/docs/coder_urls_rm.md +++ /dev/null @@ -1,21 +0,0 @@ -## coder urls rm - -Remove a dev url - -### Synopsis - -Remove a dev url - -``` -coder urls rm [environment_name] [port] [flags] -``` - -### Options - -``` - -h, --help help for rm -``` - -### SEE ALSO - -* [coder urls](coder_urls.md) - Interact with environment DevURLs diff --git a/docs/coder_users.md b/docs/coder_users.md deleted file mode 100644 index 6482d76e..00000000 --- a/docs/coder_users.md +++ /dev/null @@ -1,18 +0,0 @@ -## coder users - -Interact with Coder user accounts - -### Synopsis - -Interact with Coder user accounts - -### Options - -``` - -h, --help help for users -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder Enterprise installation -* [coder users ls](coder_users_ls.md) - list all user accounts diff --git a/docs/coder_users_ls.md b/docs/coder_users_ls.md deleted file mode 100644 index 6cf7ccd1..00000000 --- a/docs/coder_users_ls.md +++ /dev/null @@ -1,29 +0,0 @@ -## coder users ls - -list all user accounts - -### Synopsis - -list all user accounts - -``` -coder users ls [flags] -``` - -### Examples - -``` -coder users ls -o json -coder users ls -o json | jq .[] | jq -r .email -``` - -### Options - -``` - -h, --help help for ls - -o, --output string human | json (default "human") -``` - -### SEE ALSO - -* [coder users](coder_users.md) - Interact with Coder user accounts diff --git a/go.mod b/go.mod deleted file mode 100644 index 871e8e2c..00000000 --- a/go.mod +++ /dev/null @@ -1,25 +0,0 @@ -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 - github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f - github.com/klauspost/compress v1.10.8 // indirect - github.com/manifoldco/promptui v0.7.0 - github.com/mattn/go-colorable v0.1.6 // indirect - github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 - github.com/rjeczalik/notify v0.9.2 - github.com/spf13/cobra v1.0.0 - github.com/stretchr/testify v1.6.1 - go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 - golang.org/x/crypto v0.0.0-20200422194213-44a606286825 - golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a - golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 - nhooyr.io/websocket v1.8.6 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 99efd17c..00000000 --- a/go.sum +++ /dev/null @@ -1,414 +0,0 @@ -cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ= -cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= -cdr.dev/wsep v0.0.0-20200728013649-82316a09813f h1:WnTUINBwXE11xjp5nTVt+H2qB2/KEymos1jKMcppG9U= -cdr.dev/wsep v0.0.0-20200728013649-82316a09813f/go.mod h1:2VKClUml3gfmLez0gBxTJIjSKszpQotc2ZqPdApfK/Y= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ= -cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= -github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= -github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= -github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= -github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= -github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= -github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= -github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= -github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= -github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.10.8 h1:eLeJ3dr/Y9+XRfJT4l+8ZjmtB5RPJhucH2HeCV5+IZY= -github.com/klauspost/compress v1.10.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= -github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= -github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.coder.com/cli v0.4.0 h1:PruDGwm/CPFndyK/eMowZG3vzg5CgohRWeXWCTr3zi8= -go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= -go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 h1:DjCS6dRQh+1PlfiBmnabxfdrzenb0tAwJqFxDEH/s9g= -go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200422194213-44a606286825 h1:dSChiwOTvzwbHFTMq2l6uRardHH7/E6SqEkqccinS/o= -golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= -nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/activity/pusher.go b/internal/activity/pusher.go deleted file mode 100644 index 7f0f0e00..00000000 --- a/internal/activity/pusher.go +++ /dev/null @@ -1,47 +0,0 @@ -package activity - -import ( - "context" - "time" - - "golang.org/x/time/rate" - - "cdr.dev/coder-cli/coder-sdk" - - "go.coder.com/flog" -) - -const pushInterval = time.Minute - -// Pusher pushes activity metrics no more than once per pushInterval. Pushes -// within the same interval are a no-op. -type Pusher struct { - envID string - source string - - client *coder.Client - rate *rate.Limiter // Use a rate limiter to control the sampling rate. -} - -// NewPusher instantiates a new instance of Pusher. -func NewPusher(c *coder.Client, envID, source string) *Pusher { - return &Pusher{ - envID: envID, - source: source, - client: c, - // Sample only 1 per interval to avoid spamming the api. - rate: rate.NewLimiter(rate.Every(pushInterval), 1), - } -} - -// Push pushes activity, abiding by a rate limit. -func (p *Pusher) Push(ctx context.Context) { - // If we already sampled data within the allowable range, do nothing. - if !p.rate.Allow() { - return - } - - if err := p.client.PushActivity(ctx, p.source, p.envID); err != nil { - flog.Error("push activity: %s", err) - } -} diff --git a/internal/activity/writer.go b/internal/activity/writer.go deleted file mode 100644 index 0daf6dc8..00000000 --- a/internal/activity/writer.go +++ /dev/null @@ -1,23 +0,0 @@ -package activity - -import ( - "context" - "io" -) - -// writer wraps a standard io.Writer with the activity pusher. -type writer struct { - p *Pusher - wr io.Writer -} - -// Write writes to the underlying writer and tracks activity. -func (w *writer) Write(buf []byte) (int, error) { - w.p.Push(context.Background()) - return w.wr.Write(buf) -} - -// Writer wraps the given writer such that all writes trigger an activity push -func (p *Pusher) Writer(wr io.Writer) io.Writer { - return &writer{p: p, wr: wr} -} diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go deleted file mode 100644 index 8297bbd6..00000000 --- a/internal/cmd/auth.go +++ /dev/null @@ -1,52 +0,0 @@ -package cmd - -import ( - "context" - "net/http" - "net/url" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/config" -) - -var errNeedLogin = xerrors.New("failed to read session credentials: did you run \"coder login\"?") - -func newClient() (*coder.Client, error) { - sessionToken, err := config.Session.Read() - if err != nil { - return nil, errNeedLogin - } - - rawURL, err := config.URL.Read() - if err != nil { - return nil, errNeedLogin - } - - u, err := url.Parse(rawURL) - if err != nil { - return nil, xerrors.Errorf("url misformatted: %w try runing \"coder login\" with a valid URL", err) - } - - c := &coder.Client{ - BaseURL: u, - Token: sessionToken, - } - - // Make sure we can make a request so the final - // error is more clean. - _, err = c.Me(context.Background()) - if err != nil { - var he *coder.HTTPError - if xerrors.As(err, &he) { - switch he.StatusCode { - case http.StatusUnauthorized: - return nil, xerrors.Errorf("not authenticated: try running \"coder login`\"") - } - } - return nil, err - } - - return c, nil -} diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go deleted file mode 100644 index dfce6598..00000000 --- a/internal/cmd/ceapi.go +++ /dev/null @@ -1,79 +0,0 @@ -package cmd - -import ( - "context" - - "cdr.dev/coder-cli/coder-sdk" - "golang.org/x/xerrors" - - "go.coder.com/flog" -) - -// Helpers for working with the Coder Enterprise API. - -// lookupUserOrgs gets a list of orgs the user is apart of. -func lookupUserOrgs(user *coder.User, orgs []coder.Org) []coder.Org { - // NOTE: We don't know in advance how many orgs the user is in so we can't pre-alloc. - var userOrgs []coder.Org - - for _, org := range orgs { - for _, member := range org.Members { - if member.ID != user.ID { - continue - } - // If we found the user in the org, add it to the list and skip to the next org. - userOrgs = append(userOrgs, org) - break - } - } - return userOrgs -} - -// getEnvs returns all environments for the user. -func getEnvs(ctx context.Context, client *coder.Client, email string) ([]coder.Environment, error) { - user, err := client.UserByEmail(ctx, email) - if err != nil { - return nil, xerrors.Errorf("get user: %w", err) - } - - orgs, err := client.Orgs(ctx) - if err != nil { - return nil, xerrors.Errorf("get orgs: %w", err) - } - - orgs = lookupUserOrgs(user, orgs) - - // NOTE: We don't know in advance how many envs we have so we can't pre-alloc. - var allEnvs []coder.Environment - - for _, org := range orgs { - envs, err := client.EnvironmentsByOrganization(ctx, user.ID, org.ID) - if err != nil { - return nil, xerrors.Errorf("get envs for %s: %w", org.Name, err) - } - - allEnvs = append(allEnvs, envs...) - } - return allEnvs, nil -} - -// findEnv returns a single environment by name (if it exists.) -func findEnv(ctx context.Context, client *coder.Client, envName, userEmail string) (*coder.Environment, error) { - envs, err := getEnvs(ctx, client, userEmail) - if err != nil { - return nil, xerrors.Errorf("get environments: %w", err) - } - - // NOTE: We don't know in advance where we will find the env, so we can't pre-alloc. - var found []string - for _, env := range envs { - if env.Name == envName { - return &env, nil - } - // Keep track of what we found for the logs. - found = append(found, env.Name) - } - flog.Error("found %q", found) - flog.Error("%q not found", envName) - return nil, coder.ErrNotFound -} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go deleted file mode 100644 index 6f13dddc..00000000 --- a/internal/cmd/cmd.go +++ /dev/null @@ -1,103 +0,0 @@ -package cmd - -import ( - "os" - - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" -) - -// Make constructs the "coder" root command -func Make() *cobra.Command { - app := &cobra.Command{ - Use: "coder", - Short: "coder provides a CLI for working with an existing Coder Enterprise installation", - } - - app.AddCommand( - makeLoginCmd(), - makeLogoutCmd(), - makeShellCmd(), - makeUsersCmd(), - makeConfigSSHCmd(), - makeSecretsCmd(), - makeEnvsCommand(), - makeSyncCmd(), - makeURLCmd(), - completionCmd, - genDocs(app), - ) - return app -} - -func genDocs(rootCmd *cobra.Command) *cobra.Command { - return &cobra.Command{ - Use: "gen-docs [dir_path]", - Short: "Generate a markdown documentation tree for the root command.", - Example: "coder gen-docs ./docs", - Args: cobra.ExactArgs(1), - Hidden: true, - RunE: func(_ *cobra.Command, args []string) error { - return doc.GenMarkdownTree(rootCmd, args[0]) - }, - } -} - -// reference: https://github.com/spf13/cobra/blob/master/shell_completions.md -var completionCmd = &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Example: `coder completion fish > ~/.config/fish/completions/coder.fish -coder completion zsh > "${fpath[1]}/_coder" - -Linux: - $ coder completion bash > /etc/bash_completion.d/coder -MacOS: - $ coder completion bash > /usr/local/etc/bash_completion.d/coder`, - Long: `To load completions: - -Bash: - -$ source <(coder completion bash) - -To load completions for each session, execute once: -Linux: - $ coder completion bash > /etc/bash_completion.d/coder -MacOS: - $ coder completion bash > /usr/local/etc/bash_completion.d/coder - -Zsh: - -If shell completion is not already enabled in your environment you will need -to enable it. You can execute the following once: - -$ echo "autoload -U compinit; compinit" >> ~/.zshrc - -To load completions for each session, execute once: -$ coder completion zsh > "${fpath[1]}/_coder" - -You will need to start a new shell for this setup to take effect. - -Fish: - -$ coder completion fish | source - -To load completions for each session, execute once: -$ coder completion fish > ~/.config/fish/completions/coder.fish -`, - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.ExactValidArgs(1), - Run: func(cmd *cobra.Command, args []string) { - switch args[0] { - case "bash": - _ = cmd.Root().GenBashCompletion(os.Stdout) // Best effort. - case "zsh": - _ = cmd.Root().GenZshCompletion(os.Stdout) // Best effort. - case "fish": - _ = cmd.Root().GenFishCompletion(os.Stdout, true) // Best effort. - case "powershell": - _ = cmd.Root().GenPowerShellCompletion(os.Stdout) // Best effort. - } - }, -} diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go deleted file mode 100644 index 5da6745c..00000000 --- a/internal/cmd/configssh.go +++ /dev/null @@ -1,216 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "io/ioutil" - "net" - "net/url" - "os" - "os/user" - "path/filepath" - "strings" - "time" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/config" - "github.com/spf13/cobra" - "golang.org/x/xerrors" -) - -func makeConfigSSHCmd() *cobra.Command { - var ( - configpath string - remove = false - ) - - cmd := &cobra.Command{ - Use: "config-ssh", - Short: "Configure SSH to access Coder environments", - Long: "Inject the proper OpenSSH configuration into your local SSH config file.", - RunE: configSSH(&configpath, &remove), - } - cmd.Flags().StringVar(&configpath, "filepath", filepath.Join("~", ".ssh", "config"), "overide the default path of your ssh config file") - cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder Enterprise ssh config") - - return cmd -} - -func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []string) error { - startToken := "# ------------START-CODER-ENTERPRISE-----------" - startMessage := `# The following has been auto-generated by "coder config-ssh" -# to make accessing your Coder Enterprise environments easier. -# -# To remove this blob, run: -# -# coder config-ssh --remove -# -# You should not hand-edit this section, unless you are deleting it.` - endToken := "# ------------END-CODER-ENTERPRISE------------" - - return func(cmd *cobra.Command, _ []string) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - usr, err := user.Current() - if err != nil { - return xerrors.Errorf("get user home directory: %w", err) - } - - privateKeyFilepath := filepath.Join(usr.HomeDir, ".ssh", "coder_enterprise") - - if strings.HasPrefix(*configpath, "~") { - *configpath = strings.Replace(*configpath, "~", usr.HomeDir, 1) - } - - currentConfig, err := readStr(*configpath) - if os.IsNotExist(err) { - // SSH configs are not always already there. - currentConfig = "" - } else if err != nil { - return xerrors.Errorf("read ssh config file %q: %w", *configpath, err) - } - - startIndex := strings.Index(currentConfig, startToken) - endIndex := strings.Index(currentConfig, endToken) - - if *remove { - if startIndex == -1 || endIndex == -1 { - return xerrors.Errorf("the Coder Enterprise ssh configuration section could not be safely deleted or does not exist") - } - currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(endToken)+1:] - - err = writeStr(*configpath, currentConfig) - if err != nil { - return xerrors.Errorf("write to ssh config file %q: %s", *configpath, err) - } - - return nil - } - - client, err := newClient() - if err != nil { - return err - } - - sshAvailable := isSSHAvailable(ctx) - if !sshAvailable { - return xerrors.New("SSH is disabled or not available for your Coder Enterprise deployment.") - } - - user, err := client.Me(cmd.Context()) - if err != nil { - return xerrors.Errorf("fetch username: %w", err) - } - - envs, err := getEnvs(cmd.Context(), client, coder.Me) - if err != nil { - return err - } - if len(envs) < 1 { - return xerrors.New("no environments found") - } - newConfig, err := makeNewConfigs(user.Username, envs, startToken, startMessage, endToken, privateKeyFilepath) - if err != nil { - return xerrors.Errorf("make new ssh configurations: %w", err) - } - - // if we find the old config, remove those chars from the string - if startIndex != -1 && endIndex != -1 { - currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(endToken)+1:] - } - - err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm) - if err != nil { - return xerrors.Errorf("make configuration directory: %w", err) - } - err = writeStr(*configpath, currentConfig+newConfig) - if err != nil { - return xerrors.Errorf("write new configurations to ssh config file %q: %w", *configpath, err) - } - err = writeSSHKey(ctx, client, privateKeyFilepath) - if err != nil { - return xerrors.Errorf("fetch and write ssh key: %w", err) - } - - fmt.Printf("An auto-generated ssh config was written to \"%s\"\n", *configpath) - fmt.Printf("Your private ssh key was written to \"%s\"\n", privateKeyFilepath) - fmt.Println("You should now be able to ssh into your environment") - fmt.Printf("For example, try running\n\n\t$ ssh coder.%s\n\n", envs[0].Name) - return nil - } -} - -func writeSSHKey(ctx context.Context, client *coder.Client, privateKeyPath string) error { - key, err := client.SSHKey(ctx) - if err != nil { - return err - } - return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0400) -} - -func makeNewConfigs(userName string, envs []coder.Environment, startToken, startMsg, endToken, privateKeyFilepath string) (string, error) { - hostname, err := configuredHostname() - if err != nil { - return "", nil - } - - newConfig := fmt.Sprintf("\n%s\n%s\n\n", startToken, startMsg) - for _, env := range envs { - newConfig += makeSSHConfig(hostname, userName, env.Name, privateKeyFilepath) - } - newConfig += fmt.Sprintf("\n%s\n", endToken) - - return newConfig, nil -} - -func makeSSHConfig(host, userName, envName, privateKeyFilepath string) string { - return fmt.Sprintf( - `Host coder.%s - HostName %s - User %s-%s - StrictHostKeyChecking no - ConnectTimeout=0 - IdentityFile=%s - ServerAliveInterval 60 - ServerAliveCountMax 3 -`, envName, host, userName, envName, privateKeyFilepath) -} - -func isSSHAvailable(ctx context.Context) bool { - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - - host, err := configuredHostname() - if err != nil { - return false - } - - var dialer net.Dialer - _, err = dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, "22")) - return err == nil -} - -func configuredHostname() (string, error) { - u, err := config.URL.Read() - if err != nil { - return "", err - } - url, err := url.Parse(u) - if err != nil { - return "", err - } - return url.Hostname(), nil -} - -func writeStr(filename, data string) error { - return ioutil.WriteFile(filename, []byte(data), 0777) -} - -func readStr(filename string) (string, error) { - contents, err := ioutil.ReadFile(filename) - if err != nil { - return "", err - } - return string(contents), nil -} diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go deleted file mode 100644 index 49c05005..00000000 --- a/internal/cmd/envs.go +++ /dev/null @@ -1,94 +0,0 @@ -package cmd - -import ( - "encoding/json" - "os" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xtabwriter" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "go.coder.com/flog" -) - -func makeEnvsCommand() *cobra.Command { - var outputFmt string - var user string - cmd := &cobra.Command{ - Use: "envs", - Short: "Interact with Coder environments", - Long: "Perform operations on the Coder environments owned by the active user.", - } - cmd.PersistentFlags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - - lsCmd := &cobra.Command{ - Use: "ls", - Short: "list all environments owned by the active user", - Long: "List all Coder environments owned by the active user.", - RunE: func(cmd *cobra.Command, args []string) error { - client, err := newClient() - if err != nil { - return err - } - envs, err := getEnvs(cmd.Context(), client, user) - if err != nil { - return err - } - if len(envs) < 1 { - flog.Info("no environments found") - return nil - } - - switch outputFmt { - case "human": - err := xtabwriter.WriteTable(len(envs), func(i int) interface{} { - return envs[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - case "json": - err := json.NewEncoder(os.Stdout).Encode(envs) - if err != nil { - return xerrors.Errorf("write environments as JSON: %w", err) - } - default: - return xerrors.Errorf("unknown --output value %q", outputFmt) - } - return nil - }, - } - lsCmd.Flags().StringVarP(&outputFmt, "output", "o", "human", "human | json") - cmd.AddCommand(lsCmd) - cmd.AddCommand(stopEnvCommand(&user)) - - return cmd -} - -func stopEnvCommand(user *string) *cobra.Command { - return &cobra.Command{ - Use: "stop [environment_name]", - Short: "stop a Coder environment by name", - Long: "Stop a Coder environment by name", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - client, err := newClient() - if err != nil { - return xerrors.Errorf("new client: %w", err) - } - - envName := args[0] - env, err := findEnv(cmd.Context(), client, envName, *user) - if err != nil { - return xerrors.Errorf("find environment by name: %w", err) - } - - if err = client.StopEnvironment(cmd.Context(), env.ID); err != nil { - return xerrors.Errorf("stop environment: %w", err) - } - flog.Success("Successfully stopped environment %q", envName) - return nil - }, - } -} diff --git a/internal/cmd/login.go b/internal/cmd/login.go deleted file mode 100644 index 857a1bb1..00000000 --- a/internal/cmd/login.go +++ /dev/null @@ -1,149 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "net" - "net/http" - "net/url" - "os" - "strings" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/internal/loginsrv" - "github.com/pkg/browser" - "github.com/spf13/cobra" - "golang.org/x/sync/errgroup" - "golang.org/x/xerrors" - - "go.coder.com/flog" -) - -func makeLoginCmd() *cobra.Command { - return &cobra.Command{ - Use: "login [Coder Enterprise URL eg. https://my.coder.domain/]", - Short: "Authenticate this client for future operations", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - // Pull the URL from the args and do some sanity check. - rawURL := args[0] - if rawURL == "" || !strings.HasPrefix(rawURL, "http") { - return xerrors.Errorf("invalid URL") - } - u, err := url.Parse(rawURL) - if err != nil { - return xerrors.Errorf("parse url: %w", err) - } - // Remove the trailing '/' if any. - u.Path = strings.TrimSuffix(u.Path, "/") - - // From this point, the commandline is correct. - // Don't return errors as it would print the usage. - - if err := login(cmd, u, config.URL, config.Session); err != nil { - flog.Error("Login error: %s.", err) - os.Exit(1) - } - - return nil - }, - } -} - -// newLocalListener creates up a local tcp server using port 0 (i.e. any available port). -// If ipv4 is disabled, try ipv6. -// It will be used by the http server waiting for the auth callback. -func newLocalListener() (net.Listener, error) { - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { - return nil, xerrors.Errorf("listen on a port: %w", err) - } - } - return l, nil -} - -// pingAPI creates a client from the given url/token and try to exec an api call. -// Not using the SDK as we want to verify the url/token pair before storing the config files. -func pingAPI(ctx context.Context, envURL *url.URL, token string) error { - client := &coder.Client{BaseURL: envURL, Token: token} - if _, err := client.Me(ctx); err != nil { - return xerrors.Errorf("call api: %w", err) - } - return nil -} - -// storeConfig writes the env URL and session token to the local config directory. -// The config lib will handle the local config path lookup and creation. -func storeConfig(envURL *url.URL, sessionToken string, urlCfg, sessionCfg config.File) error { - if err := urlCfg.Write(envURL.String()); err != nil { - return xerrors.Errorf("store env url: %w", err) - } - if err := sessionCfg.Write(sessionToken); err != nil { - return xerrors.Errorf("store session token: %w", err) - } - return nil -} - -func login(cmd *cobra.Command, envURL *url.URL, urlCfg, sessionCfg config.File) error { - ctx := cmd.Context() - - // Start by creating the listener so we can prompt the user with the URL. - listener, err := newLocalListener() - if err != nil { - return xerrors.Errorf("create local listener: %w", err) - } - defer func() { _ = listener.Close() }() // Best effort. - - // Forge the auth URL with the callback set to the local server. - authURL := *envURL - authURL.Path = envURL.Path + "/internal-auth" - authURL.RawQuery = "local_service=http://" + listener.Addr().String() - - // Try to open the browser on the local computer. - if err := browser.OpenURL(authURL.String()); err != nil { - // Discard the error as it is an expected one in non-X environments like over ssh. - // Tell the user to visit the URL instead. - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Visit the following URL in your browser:\n\n\t%s\n\n", &authURL) // Can't fail. - } - - // Create our channel, it is going to be the central synchronization of the command. - tokenChan := make(chan string) - - // Create the http server outside the errgroup goroutine scope so we can stop it later. - srv := &http.Server{Handler: &loginsrv.Server{TokenChan: tokenChan}} - defer func() { _ = srv.Close() }() // Best effort. Direct close as we are dealing with a one-off request. - - // Start both the readline and http server in parallel. As they are both long-running routines, - // to know when to continue, we don't wait on the errgroup, but on the tokenChan. - group, ctx := errgroup.WithContext(ctx) - group.Go(func() error { return srv.Serve(listener) }) - group.Go(func() error { return loginsrv.ReadLine(ctx, cmd.InOrStdin(), cmd.ErrOrStderr(), tokenChan) }) - - // Only close then tokenChan when the errgroup is done. Best effort basis. - // Will not return the http route is used with a regular terminal. - // Useful for non interactive session, manual input, tests or custom stdin. - go func() { defer close(tokenChan); _ = group.Wait() }() - - var token string - select { - case <-ctx.Done(): - return ctx.Err() - case token = <-tokenChan: - } - - // Perform an API call to verify that the token is valid. - if err := pingAPI(ctx, envURL, token); err != nil { - return xerrors.Errorf("ping API: %w", err) - } - - // Success. Store the config only at this point so we don't override the local one in case of failure. - if err := storeConfig(envURL, token, urlCfg, sessionCfg); err != nil { - return xerrors.Errorf("store config: %w", err) - } - - flog.Success("Logged in.") - - return nil -} diff --git a/internal/cmd/logout.go b/internal/cmd/logout.go deleted file mode 100644 index 621ed89e..00000000 --- a/internal/cmd/logout.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "os" - - "cdr.dev/coder-cli/internal/config" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "go.coder.com/flog" -) - -func makeLogoutCmd() *cobra.Command { - return &cobra.Command{ - Use: "logout", - Short: "Remove local authentication credentials if any exist", - RunE: logout, - } -} - -func logout(_ *cobra.Command, _ []string) error { - err := config.Session.Delete() - if err != nil { - if os.IsNotExist(err) { - flog.Info("no active session") - return nil - } - return xerrors.Errorf("delete session: %w", err) - } - flog.Success("logged out") - return nil -} diff --git a/internal/cmd/secrets.go b/internal/cmd/secrets.go deleted file mode 100644 index 9d98faac..00000000 --- a/internal/cmd/secrets.go +++ /dev/null @@ -1,226 +0,0 @@ -package cmd - -import ( - "fmt" - "io/ioutil" - "os" - - "github.com/manifoldco/promptui" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xtabwriter" - - "go.coder.com/flog" -) - -func makeSecretsCmd() *cobra.Command { - var user string - cmd := &cobra.Command{ - Use: "secrets", - Short: "Interact with Coder Secrets", - Long: "Interact with secrets objects owned by the active user.", - } - cmd.PersistentFlags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - cmd.AddCommand( - &cobra.Command{ - Use: "ls", - Short: "List all secrets owned by the active user", - RunE: listSecrets(&user), - }, - makeCreateSecret(&user), - &cobra.Command{ - Use: "rm [...secret_name]", - Short: "Remove one or more secrets by name", - Args: cobra.MinimumNArgs(1), - RunE: makeRemoveSecrets(&user), - Example: "coder secrets rm mysql-password mysql-user", - }, - &cobra.Command{ - Use: "view [secret_name]", - Short: "View a secret by name", - Args: cobra.ExactArgs(1), - RunE: makeViewSecret(&user), - Example: "coder secrets view mysql-password", - }, - ) - return cmd -} - -func makeCreateSecret(userEmail *string) *cobra.Command { - var ( - fromFile string - fromLiteral string - fromPrompt bool - description string - ) - - cmd := &cobra.Command{ - Use: "create [secret_name]", - Short: "Create a new secret", - Long: "Create a new secret object to store application secrets and access them securely from within your environments.", - Example: `coder secrets create mysql-password --from-literal 123password -coder secrets create mysql-password --from-prompt -coder secrets create aws-credentials --from-file ./credentials.json`, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return xerrors.Errorf("[secret_name] is a required argument") - } - if fromPrompt && (fromLiteral != "" || fromFile != "") { - return xerrors.Errorf("--from-prompt cannot be set along with --from-file or --from-literal") - } - if fromLiteral != "" && fromFile != "" { - return xerrors.Errorf("--from-literal and --from-file cannot both be set") - } - if !fromPrompt && fromFile == "" && fromLiteral == "" { - return xerrors.Errorf("one of [--from-literal, --from-file, --from-prompt] is required") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - var ( - name = args[0] - value string - err error - ) - client, err := newClient() - if err != nil { - return err - } - if fromLiteral != "" { - value = fromLiteral - } else if fromFile != "" { - contents, err := ioutil.ReadFile(fromFile) - if err != nil { - return xerrors.Errorf("read file: %w", err) - } - value = string(contents) - } else { - prompt := promptui.Prompt{ - Label: "value", - Mask: '*', - Validate: func(s string) error { - if len(s) < 1 { - return xerrors.Errorf("a length > 0 is required") - } - return nil - }, - } - value, err = prompt.Run() - if err != nil { - return xerrors.Errorf("prompt for value: %w", err) - } - } - - user, err := client.UserByEmail(cmd.Context(), *userEmail) - if err != nil { - return xerrors.Errorf("get user %q by email: %w", *userEmail, err) - } - err = client.InsertSecret(cmd.Context(), user, coder.InsertSecretReq{ - Name: name, - Value: value, - Description: description, - }) - if err != nil { - return xerrors.Errorf("insert secret: %w", err) - } - return nil - }, - } - - cmd.Flags().StringVar(&fromFile, "from-file", "", "a file from which to read the value of the secret") - cmd.Flags().StringVar(&fromLiteral, "from-literal", "", "the value of the secret") - cmd.Flags().BoolVar(&fromPrompt, "from-prompt", false, "enter the secret value through a terminal prompt") - cmd.Flags().StringVar(&description, "description", "", "a description of the secret") - - return cmd -} - -func listSecrets(userEmail *string) func(cmd *cobra.Command, _ []string) error { - return func(cmd *cobra.Command, _ []string) error { - client, err := newClient() - if err != nil { - return err - } - user, err := client.UserByEmail(cmd.Context(), *userEmail) - if err != nil { - return xerrors.Errorf("get user %q by email: %w", *userEmail, err) - } - - secrets, err := client.Secrets(cmd.Context(), user.ID) - if err != nil { - return xerrors.Errorf("get secrets: %w", err) - } - - if len(secrets) < 1 { - flog.Info("No secrets found") - return nil - } - - err = xtabwriter.WriteTable(len(secrets), func(i int) interface{} { - s := secrets[i] - s.Value = "******" // value is omitted from bulk responses - return s - }) - if err != nil { - return xerrors.Errorf("write table of secrets: %w", err) - } - return nil - } -} - -func makeViewSecret(userEmail *string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - var ( - name = args[0] - ) - client, err := newClient() - if err != nil { - return err - } - user, err := client.UserByEmail(cmd.Context(), *userEmail) - if err != nil { - return xerrors.Errorf("get user %q by email: %w", *userEmail, err) - } - - secret, err := client.SecretWithValueByName(cmd.Context(), name, user.ID) - if err != nil { - return xerrors.Errorf("get secret by name: %w", err) - } - - _, err = fmt.Fprintln(os.Stdout, secret.Value) - if err != nil { - return xerrors.Errorf("write secret value: %w", err) - } - return nil - } -} - -func makeRemoveSecrets(userEmail *string) func(c *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - client, err := newClient() - if err != nil { - return err - } - user, err := client.UserByEmail(cmd.Context(), *userEmail) - if err != nil { - return xerrors.Errorf("get user %q by email: %w", *userEmail, err) - } - - errorSeen := false - for _, n := range args { - err := client.DeleteSecretByName(cmd.Context(), n, user.ID) - if err != nil { - flog.Error("failed to delete secret %q: %v", n, err) - errorSeen = true - } else { - flog.Success("successfully deleted secret %q", n) - } - } - if errorSeen { - os.Exit(1) - } - return nil - } -} diff --git a/internal/cmd/shell.go b/internal/cmd/shell.go deleted file mode 100644 index 3f34075a..00000000 --- a/internal/cmd/shell.go +++ /dev/null @@ -1,215 +0,0 @@ -package cmd - -import ( - "context" - "io" - "os" - "strings" - "time" - - "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" - "golang.org/x/time/rate" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/activity" - "cdr.dev/coder-cli/internal/x/xterminal" - "cdr.dev/wsep" - - "go.coder.com/flog" -) - -func getEnvsForCompletion(user string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - client, err := newClient() - if err != nil { - return nil, cobra.ShellCompDirectiveDefault - } - envs, err := getEnvs(context.TODO(), client, user) - if err != nil { - return nil, cobra.ShellCompDirectiveDefault - } - - envNames := make([]string, 0, len(envs)) - for _, e := range envs { - envNames = append(envNames, e.Name) - } - return envNames, cobra.ShellCompDirectiveDefault - } -} - -func makeShellCmd() *cobra.Command { - return &cobra.Command{ - Use: "sh [environment_name] []", - Short: "Open a shell and execute commands in a Coder environment", - Long: "Execute a remote command on the environment\\nIf no command is specified, the default shell is opened.", - Args: cobra.MinimumNArgs(1), - DisableFlagParsing: true, - ValidArgsFunction: getEnvsForCompletion(coder.Me), - RunE: shell, - Example: "coder sh backend-env", - } -} - -func shell(_ *cobra.Command, cmdArgs []string) error { - ctx := context.Background() - - command := "sh" - args := []string{"-c"} - if len(cmdArgs) > 1 { - args = append(args, strings.Join(cmdArgs[1:], " ")) - } else { - // Bring user into shell if no command is specified. - args = append(args, "exec $(getent passwd $(whoami) | awk -F: '{ print $7 }')") - } - - envName := cmdArgs[0] - - if err := runCommand(ctx, envName, command, args); err != nil { - if exitErr, ok := err.(wsep.ExitError); ok { - os.Exit(exitErr.Code) - } - flog.Fatal("%+v", err) - } - return nil -} - -// sendResizeEvents starts watching for the client's terminal resize signals -// and sends the event to the server so the remote tty can match the client. -func sendResizeEvents(ctx context.Context, termFD uintptr, process wsep.Process) { - events := xterminal.ResizeEvents(ctx, termFD) - - // Limit the frequency of resizes to prevent a stuttering effect. - resizeLimiter := rate.NewLimiter(rate.Every(100*time.Millisecond), 1) - for { - select { - case newsize := <-events: - if err := process.Resize(ctx, newsize.Height, newsize.Width); err != nil { - return - } - _ = resizeLimiter.Wait(ctx) - case <-ctx.Done(): - return - } - } -} - -func runCommand(ctx context.Context, envName, command string, args []string) error { - client, err := newClient() - if err != nil { - return err - } - env, err := findEnv(ctx, client, envName, coder.Me) - if err != nil { - return xerrors.Errorf("find environment: %w", err) - } - - termFD := os.Stdout.Fd() - - isInteractive := terminal.IsTerminal(int(termFD)) - if isInteractive { - // If the client has a tty, take over it by setting the raw mode. - // This allows for all input to be directly forwarded to the remote process, - // otherwise, the local terminal would buffer input, interpret special keys, etc. - stdinState, err := xterminal.MakeRaw(os.Stdin.Fd()) - if err != nil { - return err - } - defer func() { - // Best effort. If this fails it will result in a broken terminal, - // but there is nothing we can do about it. - _ = xterminal.Restore(os.Stdin.Fd(), stdinState) - }() - } - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - conn, err := client.DialWsep(ctx, env) - if err != nil { - return xerrors.Errorf("dial websocket: %w", err) - } - go heartbeat(ctx, conn, 15*time.Second) - - var cmdEnv []string - if isInteractive { - term := os.Getenv("TERM") - if term == "" { - term = "xterm" - } - cmdEnv = append(cmdEnv, "TERM="+term) - } - - execer := wsep.RemoteExecer(conn) - process, err := execer.Start(ctx, wsep.Command{ - Command: command, - Args: args, - TTY: isInteractive, - Stdin: true, - Env: cmdEnv, - }) - if err != nil { - var closeErr websocket.CloseError - if xerrors.As(err, &closeErr) { - return xerrors.Errorf("network error, is %q online? (%w)", envName, err) - } - return xerrors.Errorf("start remote command: %w", err) - } - - // Now that the remote process successfully started, if we have a tty, start the resize event watcher. - if isInteractive { - go sendResizeEvents(ctx, termFD, process) - } - - go func() { - stdin := process.Stdin() - defer func() { _ = stdin.Close() }() // Best effort. - - ap := activity.NewPusher(client, env.ID, sshActivityName) - wr := ap.Writer(stdin) - if _, err := io.Copy(wr, os.Stdin); err != nil { - cancel() - } - }() - go func() { - if _, err := io.Copy(os.Stdout, process.Stdout()); err != nil { - cancel() - } - }() - go func() { - - if _, err := io.Copy(os.Stderr, process.Stderr()); err != nil { - cancel() - } - }() - - if err := process.Wait(); err != nil { - var closeErr websocket.CloseError - if xerrors.Is(err, ctx.Err()) || xerrors.As(err, &closeErr) { - return xerrors.Errorf("network error, is %q online?", envName) - } - return err - } - return nil -} - -func heartbeat(ctx context.Context, conn *websocket.Conn, interval time.Duration) { - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - if err := conn.Ping(ctx); err != nil { - // NOTE: Prefix with \r\n to attempt to have clearer output as we might still be in raw mode. - flog.Fatal("\r\nFailed to ping websocket: %s, exiting.", err) - } - } - } -} - -const sshActivityName = "ssh" diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go deleted file mode 100644 index 0ba32095..00000000 --- a/internal/cmd/sync.go +++ /dev/null @@ -1,112 +0,0 @@ -package cmd - -import ( - "bytes" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/sync" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "go.coder.com/flog" -) - -func makeSyncCmd() *cobra.Command { - var init bool - cmd := &cobra.Command{ - Use: "sync [local directory] [:]", - Short: "Establish a one way directory sync to a Coder environment", - Args: cobra.ExactArgs(2), - RunE: makeRunSync(&init), - } - cmd.Flags().BoolVar(&init, "init", false, "do initial transfer and exit") - return cmd -} - -// rsyncVersion returns local rsync protocol version as a string. -func rsyncVersion() string { - cmd := exec.Command("rsync", "--version") - out, err := cmd.CombinedOutput() - if err != nil { - log.Fatal(err) - } - - firstLine, err := bytes.NewBuffer(out).ReadString('\n') - if err != nil { - log.Fatal(err) - } - versionString := strings.Split(firstLine, "protocol version ") - - return versionString[1] -} - -func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - var ( - local = args[0] - remote = args[1] - ) - - client, err := newClient() - if err != nil { - return err - } - - info, err := os.Stat(local) - if err != nil { - return err - } - if !info.IsDir() { - return xerrors.Errorf("%s must be a directory", local) - } - - remoteTokens := strings.SplitN(remote, ":", 2) - if len(remoteTokens) != 2 { - return xerrors.New("remote misformatted") - } - var ( - envName = remoteTokens[0] - remoteDir = remoteTokens[1] - ) - - env, err := findEnv(cmd.Context(), client, envName, coder.Me) - if err != nil { - return err - } - - absLocal, err := filepath.Abs(local) - if err != nil { - return xerrors.Errorf("make abs path out of %s, %s: %w", local, absLocal, err) - } - - s := sync.Sync{ - Init: *init, - Env: *env, - RemoteDir: remoteDir, - LocalDir: absLocal, - Client: client, - } - - localVersion := rsyncVersion() - remoteVersion, rsyncErr := s.Version() - - if rsyncErr != nil { - flog.Info("Unable to determine remote rsync version. Proceeding cautiously.") - } else if localVersion != remoteVersion { - return xerrors.Errorf("rsync protocol mismatch: local = %s, remote = %s", localVersion, remoteVersion) - } - - for err == nil || err == sync.ErrRestartSync { - err = s.Run() - } - if err != nil { - return err - } - return nil - } -} diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go deleted file mode 100644 index 7a3300fc..00000000 --- a/internal/cmd/urls.go +++ /dev/null @@ -1,283 +0,0 @@ -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "os" - "regexp" - "strconv" - "strings" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xtabwriter" - - "go.coder.com/flog" -) - -func makeURLCmd() *cobra.Command { - var outputFmt string - cmd := &cobra.Command{ - Use: "urls", - Short: "Interact with environment DevURLs", - } - lsCmd := &cobra.Command{ - Use: "ls [environment_name]", - Short: "List all DevURLs for an environment", - Args: cobra.ExactArgs(1), - ValidArgsFunction: getEnvsForCompletion(coder.Me), - RunE: makeListDevURLs(&outputFmt), - } - lsCmd.Flags().StringVarP(&outputFmt, "output", "o", "human", "human|json") - - rmCmd := &cobra.Command{ - Use: "rm [environment_name] [port]", - Args: cobra.ExactArgs(2), - Short: "Remove a dev url", - RunE: removeDevURL, - } - - cmd.AddCommand( - lsCmd, - rmCmd, - makeCreateDevURL(), - ) - - return cmd -} - -// DevURL is the parsed json response record for a devURL from cemanager -type DevURL struct { - ID string `json:"id" tab:"-"` - URL string `json:"url" tab:"URL"` - Port int `json:"port" tab:"Port"` - Name string `json:"name" tab:"-"` - Access string `json:"access" tab:"Access"` -} - -var urlAccessLevel = map[string]string{ - // Remote API endpoint requires these in uppercase. - "PRIVATE": "Only you can access", - "ORG": "All members of your organization can access", - "AUTHED": "Authenticated users can access", - "PUBLIC": "Anyone on the internet can access this link", -} - -func validatePort(port string) (int, error) { - p, err := strconv.ParseUint(port, 10, 16) - if err != nil { - flog.Error("Invalid port") - return 0, err - } - if p < 1 { - // Port 0 means 'any free port', which we don't support. - flog.Error("Port must be > 0") - return 0, strconv.ErrRange - } - return int(p), nil -} - -func accessLevelIsValid(level string) bool { - _, ok := urlAccessLevel[level] - if !ok { - flog.Error("Invalid access level") - } - return ok -} - -// Run gets the list of active devURLs from the cemanager for the -// specified environment and outputs info to stdout. -func makeListDevURLs(outputFmt *string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - envName := args[0] - devURLs, err := urlList(cmd.Context(), envName) - if err != nil { - return err - } - - switch *outputFmt { - case "human": - if len(devURLs) < 1 { - flog.Info("No devURLs found for environment %q", envName) - return nil - } - err := xtabwriter.WriteTable(len(devURLs), func(i int) interface{} { - return devURLs[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - case "json": - if err := json.NewEncoder(os.Stdout).Encode(devURLs); err != nil { - return xerrors.Errorf("encode DevURLs as json: %w", err) - } - default: - return xerrors.Errorf("unknown --output value %q", *outputFmt) - } - return nil - } -} - -func makeCreateDevURL() *cobra.Command { - var ( - access string - urlname string - ) - cmd := &cobra.Command{ - Use: "create [env_name] [port] [--access ] [--name ]", - Short: "Create a new devurl for an environment", - Aliases: []string{"edit"}, - Args: cobra.ExactArgs(2), - // Run creates or updates a devURL - RunE: func(cmd *cobra.Command, args []string) error { - var ( - envName = args[0] - port = args[1] - ) - - portNum, err := validatePort(port) - if err != nil { - return err - } - - access = strings.ToUpper(access) - if !accessLevelIsValid(access) { - return xerrors.Errorf("invalid access level %q", access) - } - - if urlname != "" && !devURLNameValidRx.MatchString(urlname) { - return xerrors.New("update devurl: name must be < 64 chars in length, begin with a letter and only contain letters or digits.") - } - client, err := newClient() - if err != nil { - return err - } - - env, err := findEnv(cmd.Context(), client, envName, coder.Me) - if err != nil { - return err - } - - urls, err := urlList(cmd.Context(), envName) - if err != nil { - return err - } - - urlID, found := devURLID(portNum, urls) - if found { - flog.Info("Updating devurl for port %v", port) - err := client.UpdateDevURL(cmd.Context(), env.ID, urlID, portNum, urlname, access) - if err != nil { - return xerrors.Errorf("update DevURL: %w", err) - } - } else { - flog.Info("Adding devurl for port %v", port) - err := client.InsertDevURL(cmd.Context(), env.ID, portNum, urlname, access) - if err != nil { - return xerrors.Errorf("insert DevURL: %w", err) - } - } - return nil - }, - } - - cmd.Flags().StringVar(&access, "access", "private", "Set DevURL access to [private | org | authed | public]") - cmd.Flags().StringVar(&urlname, "name", "", "DevURL name") - _ = cmd.MarkFlagRequired("name") - - return cmd -} - -// devURLNameValidRx is the regex used to validate devurl names specified -// via the --name subcommand. Named devurls must begin with a letter, and -// consist solely of letters and digits, with a max length of 64 chars. -var devURLNameValidRx = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]{0,63}$") - -// devURLID returns the ID of a devURL, given the env name and port -// from a list of DevURL records. -// ("", false) is returned if no match is found. -func devURLID(port int, urls []DevURL) (string, bool) { - for _, url := range urls { - if url.Port == port { - return url.ID, true - } - } - return "", false -} - -// Run deletes a devURL, specified by env ID and port, from the cemanager. -func removeDevURL(cmd *cobra.Command, args []string) error { - var ( - envName = args[0] - port = args[1] - ) - - portNum, err := validatePort(port) - if err != nil { - return xerrors.Errorf("validate port: %w", err) - } - - client, err := newClient() - if err != nil { - return err - } - env, err := findEnv(cmd.Context(), client, envName, coder.Me) - if err != nil { - return err - } - - urls, err := urlList(cmd.Context(), envName) - if err != nil { - return err - } - - urlID, found := devURLID(portNum, urls) - if found { - flog.Info("Deleting devurl for port %v", port) - } else { - return xerrors.Errorf("No devurl found for port %v", port) - } - - if err := client.DelDevURL(cmd.Context(), env.ID, urlID); err != nil { - return xerrors.Errorf("delete DevURL: %w", err) - } - return nil -} - -// urlList returns the list of active devURLs from the cemanager. -func urlList(ctx context.Context, envName string) ([]DevURL, error) { - client, err := newClient() - if err != nil { - return nil, err - } - env, err := findEnv(ctx, client, envName, coder.Me) - if err != nil { - return nil, err - } - - reqString := "%s/api/environments/%s/devurls?session_token=%s" - reqURL := fmt.Sprintf(reqString, client.BaseURL, env.ID, client.Token) - - resp, err := http.Get(reqURL) - if err != nil { - return nil, err - } - defer func() { _ = resp.Body.Close() }() // Best effort. - - if resp.StatusCode != http.StatusOK { - return nil, xerrors.Errorf("non-success status code: %d", resp.StatusCode) - } - - dec := json.NewDecoder(resp.Body) - - var devURLs []DevURL - if err := dec.Decode(&devURLs); err != nil { - return nil, err - } - - return devURLs, nil -} diff --git a/internal/cmd/users.go b/internal/cmd/users.go deleted file mode 100644 index b74b1758..00000000 --- a/internal/cmd/users.go +++ /dev/null @@ -1,61 +0,0 @@ -package cmd - -import ( - "encoding/json" - "os" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/internal/x/xtabwriter" -) - -func makeUsersCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "users", - Short: "Interact with Coder user accounts", - } - - var outputFmt string - lsCmd := &cobra.Command{ - Use: "ls", - Short: "list all user accounts", - Example: `coder users ls -o json -coder users ls -o json | jq .[] | jq -r .email`, - RunE: listUsers(&outputFmt), - } - lsCmd.Flags().StringVarP(&outputFmt, "output", "o", "human", "human | json") - - cmd.AddCommand(lsCmd) - return cmd -} - -func listUsers(outputFmt *string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - client, err := newClient() - if err != nil { - return err - } - - users, err := client.Users(cmd.Context()) - if err != nil { - return xerrors.Errorf("get users: %w", err) - } - - switch *outputFmt { - case "human": - // For each element, return the user. - each := func(i int) interface{} { return users[i] } - if err := xtabwriter.WriteTable(len(users), each); err != nil { - return xerrors.Errorf("write table: %w", err) - } - case "json": - if err := json.NewEncoder(os.Stdout).Encode(users); err != nil { - return xerrors.Errorf("encode users as json: %w", err) - } - default: - return xerrors.New("unknown value for --output") - } - return nil - } -} diff --git a/internal/config/dir.go b/internal/config/dir.go deleted file mode 100644 index 34cc7ab5..00000000 --- a/internal/config/dir.go +++ /dev/null @@ -1,49 +0,0 @@ -package config - -import ( - "io/ioutil" - "os" - "path/filepath" - - "github.com/kirsle/configdir" -) - -func dir() string { - return configdir.LocalConfig("coder") -} - -// open opens a file in the configuration directory, -// creating all intermediate directories. -func open(path string, flag int, mode os.FileMode) (*os.File, error) { - path = filepath.Join(dir(), path) - - err := os.MkdirAll(filepath.Dir(path), 0750) - if err != nil { - return nil, err - } - - return os.OpenFile(path, flag, mode) -} - -func write(path string, mode os.FileMode, dat []byte) error { - fi, err := open(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, mode) - if err != nil { - return err - } - defer fi.Close() - _, err = fi.Write(dat) - return err -} - -func read(path string) ([]byte, error) { - fi, err := open(path, os.O_RDONLY, 0) - if err != nil { - return nil, err - } - defer fi.Close() - return ioutil.ReadAll(fi) -} - -func rm(path string) error { - return os.Remove(filepath.Join(dir(), path)) -} diff --git a/internal/config/doc.go b/internal/config/doc.go deleted file mode 100644 index 69ff5641..00000000 --- a/internal/config/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package config provides facilities for working with the local configuration -// directory. -package config diff --git a/internal/config/file.go b/internal/config/file.go deleted file mode 100644 index 8ef1a910..00000000 --- a/internal/config/file.go +++ /dev/null @@ -1,26 +0,0 @@ -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/loginsrv/input.go b/internal/loginsrv/input.go deleted file mode 100644 index 99de2015..00000000 --- a/internal/loginsrv/input.go +++ /dev/null @@ -1,58 +0,0 @@ -package loginsrv - -import ( - "bufio" - "context" - "fmt" - "io" - "net/url" - "strings" - - "golang.org/x/xerrors" -) - -// ReadLine waits for the manual login input to send the session token. -// NOTE: As we are dealing with a Read, cancelling the context will not unblock. -// The caller is expected to close the reader. -func ReadLine(ctx context.Context, r io.Reader, w io.Writer, tokenChan chan<- string) error { - // Wrap the reader with bufio to simplify the readline. - buf := bufio.NewReader(r) - -retry: - _, _ = fmt.Fprintf(w, "or enter token manually:\n") // Best effort. Can only fail on custom writers. - line, err := buf.ReadString('\n') - if err != nil { - // If we get an expected error, discard it and stop the routine. - // NOTE: UnexpectedEOF is indeed an expected error as we can get it if we receive the token via the http server. - if err == io.EOF || err == io.ErrClosedPipe || err == io.ErrUnexpectedEOF { - return nil - } - // In the of error, we don't try again. Error out right away. - return xerrors.Errorf("read input: %w", err) - } - - // If we don't have any data, try again to read. - line = strings.TrimSpace(line) - if line == "" { - goto retry - } - - // Handle the case where we copy/paste the full URL instead of just the token. - // Useful as most browser will auto-select the full URL. - if u, err := url.Parse(line); err == nil { - // Check the query string only in case of success, ignore the error otherwise - // as we consider the input to be the token itself. - if token := u.Query().Get("session_token"); token != "" { - line = token - } - // If the session_token is missing, we also consider the input the be the token, don't error out. - } - - select { - case <-ctx.Done(): - return ctx.Err() - case tokenChan <- line: - } - - return nil -} diff --git a/internal/loginsrv/input_test.go b/internal/loginsrv/input_test.go deleted file mode 100644 index 714ec86c..00000000 --- a/internal/loginsrv/input_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package loginsrv_test - -import ( - "context" - "fmt" - "io" - "io/ioutil" - "testing" - "time" - - "cdr.dev/coder-cli/internal/loginsrv" - "github.com/stretchr/testify/require" -) - -// 100ms is plenty of time as we are dealing with simple in-memory pipe. -const readTimeout = 100 * time.Millisecond - -func TestReadLine(t *testing.T) { - t.Parallel() - - const testToken = "hellosession" - - for _, scene := range []struct{ name, format string }{ - {"full_url", "http://localhost:4321?session_token=%s\n"}, - {"direct", "%s\n"}, - {"whitespaces", "\n\n %s \n\n"}, - } { - scene := scene - t.Run(scene.name, func(t *testing.T) { - t.Parallel() - - tokenChan := make(chan string) - defer close(tokenChan) - - ctx, cancel := context.WithTimeout(context.Background(), readTimeout) - defer cancel() - - r, w := io.Pipe() - defer func() { _, _ = r.Close(), w.Close() }() // Best effort. - - errChan := make(chan error) - go func() { defer close(errChan); errChan <- loginsrv.ReadLine(ctx, r, ioutil.Discard, tokenChan) }() - - doneChan := make(chan struct{}) - go func() { - defer close(doneChan) - _, _ = fmt.Fprintf(w, scene.format, testToken) // Best effort. - }() - - select { - case <-ctx.Done(): - t.Fatal("Timeout sending the input.") - case err := <-errChan: - t.Fatalf("ReadLine returned before we got the token (%v).", err) - case <-doneChan: - } - - select { - case <-ctx.Done(): - t.Fatal("Timeout waiting for the input.") - case err := <-errChan: - t.Fatalf("ReadLine returned before we got the token (%v).", err) - case actualToken := <-tokenChan: - require.Equal(t, testToken, actualToken, "Unexpected token received from readline.") - } - - select { - case <-ctx.Done(): - t.Fatal("Timeout waiting for readline to finish.") - case err := <-errChan: - require.NoError(t, err, "Error reading the line.") - } - }) - } -} - -func TestReadLineMissingToken(t *testing.T) { - t.Parallel() - - tokenChan := make(chan string) - defer close(tokenChan) - - ctx, cancel := context.WithTimeout(context.Background(), readTimeout) - defer cancel() - - r, w := io.Pipe() - defer func() { _, _ = r.Close(), w.Close() }() // Best effort. - - errChan := make(chan error) - go func() { defer close(errChan); errChan <- loginsrv.ReadLine(ctx, r, ioutil.Discard, tokenChan) }() - - doneChan := make(chan struct{}) - go func() { - defer close(doneChan) - - // Send multiple empty lines. - for i := 0; i < 5; i++ { - _, _ = fmt.Fprint(w, "\n") // Best effort. - } - }() - - // Make sure the write doesn't timeout. - select { - case <-ctx.Done(): - t.Fatal("Timeout sending the input.") - case err := <-errChan: - t.Fatalf("ReadLine returned before we got the token (%+v).", err) - case token, ok := <-tokenChan: - t.Fatalf("Token channel unexpectedly unblocked. Data: %q, state: %t.", token, ok) - case <-doneChan: - } - - // Manually close the input. - _ = r.CloseWithError(io.EOF) // Best effort. - - // Make sure ReadLine properly ended. - select { - case <-ctx.Done(): - t.Fatal("Timeout waiting for readline to finish.") - case err := <-errChan: - require.NoError(t, err, "Error reading the line.") - case token, ok := <-tokenChan: - t.Fatalf("Token channel unexpectedly unblocked. Data: %q, state: %t.", token, ok) - } -} diff --git a/internal/loginsrv/server.go b/internal/loginsrv/server.go deleted file mode 100644 index 4add3775..00000000 --- a/internal/loginsrv/server.go +++ /dev/null @@ -1,30 +0,0 @@ -package loginsrv - -import ( - "fmt" - "net/http" -) - -// Server waits for the login callback to send the session token. -type Server struct { - TokenChan chan<- string -} - -func (srv *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { - ctx := req.Context() - - token := req.URL.Query().Get("session_token") - if token == "" { - w.WriteHeader(http.StatusBadRequest) - _, _ = fmt.Fprintf(w, "No session_token found.\n") // Best effort. - return - } - - select { - case <-ctx.Done(): - // Client disconnect. Nothing to do. - case srv.TokenChan <- token: - w.WriteHeader(http.StatusOK) - _, _ = fmt.Fprintf(w, "You may close this window now.\n") // Best effort. - } -} diff --git a/internal/loginsrv/server_test.go b/internal/loginsrv/server_test.go deleted file mode 100644 index 694c68b6..00000000 --- a/internal/loginsrv/server_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package loginsrv_test - -import ( - "context" - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - "time" - - "cdr.dev/coder-cli/internal/loginsrv" - "github.com/stretchr/testify/require" -) - -// 500ms should be plenty enough, even on slow machine to perform the request/response cycle. -const httpTimeout = 500 * time.Millisecond - -func TestLocalLoginHTTPServer(t *testing.T) { - t.Parallel() - - t.Run("happy_path", func(t *testing.T) { - t.Parallel() - - tokenChan := make(chan string) - defer close(tokenChan) - - ts := httptest.NewServer(&loginsrv.Server{TokenChan: tokenChan}) - defer ts.Close() - - ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) - defer cancel() - - const testToken = "hellosession" - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL+"?session_token="+testToken, nil) // Can't fail. - require.NoError(t, err, "Error creating the http request.") - - errChan := make(chan error) - go func() { - defer close(errChan) - resp, err := http.DefaultClient.Do(req) - - _, _ = io.Copy(ioutil.Discard, resp.Body) // Ignore the body, worry about the response code. - _ = resp.Body.Close() // Best effort. - - require.Equal(t, http.StatusOK, resp.StatusCode, "Unexpected status code.") - - errChan <- err - }() - - select { - case <-ctx.Done(): - t.Fatal("Timeout waiting for the session token.") - case err := <-errChan: - t.Fatalf("The HTTP client returned before we got the token (%+v).", err) - case actualToken := <-tokenChan: - require.Equal(t, actualToken, actualToken, "Unexpected token received from the local server.") - } - - select { - case <-ctx.Done(): - t.Fatal("Timeout waiting for the handler to finish.") - case err := <-errChan: - require.NoError(t, err, "Error calling test server.") - if t.Failed() { // Case where the assert within the goroutine failed. - return - } - } - }) - - t.Run("missing_token", func(t *testing.T) { - t.Parallel() - - tokenChan := make(chan string) - defer close(tokenChan) - - ts := httptest.NewServer(&loginsrv.Server{TokenChan: tokenChan}) - defer ts.Close() - - ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) - defer cancel() - - req, _ := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil) // Can't fail. - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err, "Error calling test server.") - - _, _ = io.Copy(ioutil.Discard, resp.Body) // Ignore the body, worry about the response code. - _ = resp.Body.Close() // Best effort. - - require.Equal(t, http.StatusBadRequest, resp.StatusCode, "Unexpected status code.") - select { - case err := <-ctx.Done(): - t.Fatalf("Unexpected context termination: %s.", err) - case token, ok := <-tokenChan: - t.Fatalf("Token channel unexpectedly unblocked. Data: %q, state: %t.", token, ok) - default: - // Expected case: valid and live context. - } - }) -} diff --git a/internal/sync/eventcache.go b/internal/sync/eventcache.go deleted file mode 100644 index 85a5e8ac..00000000 --- a/internal/sync/eventcache.go +++ /dev/null @@ -1,70 +0,0 @@ -package sync - -import ( - "os" - "time" - - "github.com/rjeczalik/notify" - - "go.coder.com/flog" -) - -type timedEvent struct { - CreatedAt time.Time - notify.EventInfo -} - -type eventCache map[string]timedEvent - -func (cache eventCache) Add(ev timedEvent) { - log := flog.New() - log.Prefix = ev.Path() + ": " - lastEvent, ok := cache[ev.Path()] - if ok { - switch { - // If the file was quickly created and then destroyed, pretend nothing ever happened. - case lastEvent.Event() == notify.Create && ev.Event() == notify.Remove: - delete(cache, ev.Path()) - log.Info("ignored Create then Remove") - return - } - log.Info("replaced %s with %s", lastEvent.Event(), ev.Event()) - } - // Only let the latest event for a path have action. - cache[ev.Path()] = ev -} - -// SequentialEvents returns the list of events that pertain to directories. -// The set of returned events is disjoint with ConcurrentEvents. -func (cache eventCache) SequentialEvents() []timedEvent { - var r []timedEvent - for _, ev := range cache { - info, err := os.Stat(ev.Path()) - if err == nil && !info.IsDir() { - continue - } - // Include files that have deleted here. - // It's unclear whether they're files or folders. - r = append(r, ev) - - } - return r -} - -// ConcurrentEvents returns the list of events that are safe to process after SequentialEvents. -// The set of returns events is disjoint with SequentialEvents. -func (cache eventCache) ConcurrentEvents() []timedEvent { - var r []timedEvent - for _, ev := range cache { - info, err := os.Stat(ev.Path()) - if err != nil { - continue - } - if info.IsDir() { - continue - } - r = append(r, ev) - - } - return r -} diff --git a/internal/sync/sync.go b/internal/sync/sync.go deleted file mode 100644 index 83e6ca85..00000000 --- a/internal/sync/sync.go +++ /dev/null @@ -1,385 +0,0 @@ -package sync - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/gorilla/websocket" - "github.com/rjeczalik/notify" - "golang.org/x/sync/semaphore" - "golang.org/x/xerrors" - - "go.coder.com/flog" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/activity" - "cdr.dev/wsep" -) - -// Sync runs a live sync daemon. -type Sync struct { - // Init sets whether the sync will do the initial init and then return fast. - Init bool - // LocalDir is an absolute path. - LocalDir string - // RemoteDir is an absolute path. - RemoteDir string - // DisableMetrics disables activity metric pushing. - DisableMetrics bool - - Env coder.Environment - Client *coder.Client -} - -// See https://lxadm.com/Rsync_exit_codes#List_of_standard_rsync_exit_codes. -const ( - rsyncExitCodeIncompat = 2 - rsyncExitCodeDataStream = 12 -) - -func (s Sync) syncPaths(delete bool, local, remote string) error { - self := os.Args[0] - - args := []string{"-zz", - "-a", - "--delete", - "-e", self + " sh", local, s.Env.Name + ":" + remote, - } - if delete { - args = append([]string{"--delete"}, args...) - } - if os.Getenv("DEBUG_RSYNC") != "" { - args = append([]string{"--progress"}, args...) - } - - // See https://unix.stackexchange.com/questions/188737/does-compression-option-z-with-rsync-speed-up-backup - // on compression level. - // (AB): compression sped up the initial sync of the enterprise repo by 30%, leading me to believe it's - // good in general for codebases. - cmd := exec.Command("rsync", args...) - cmd.Stdout = os.Stdout - cmd.Stderr = ioutil.Discard - cmd.Stdin = os.Stdin - - if err := cmd.Run(); err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - if exitError.ExitCode() == rsyncExitCodeIncompat { - return xerrors.Errorf("no compatible rsync on remote machine: rsync: %w", err) - } else if exitError.ExitCode() == rsyncExitCodeDataStream { - return xerrors.Errorf("protocol datastream error or no remote rsync found: %w", err) - } else { - return xerrors.Errorf("rsync: %w", err) - } - } - return xerrors.Errorf("rsync: %w", err) - } - return nil -} - -func (s Sync) remoteCmd(ctx context.Context, prog string, args ...string) error { - conn, err := s.Client.DialWsep(ctx, &s.Env) - if err != nil { - return xerrors.Errorf("dial websocket: %w", err) - } - defer func() { _ = conn.Close(websocket.CloseNormalClosure, "") }() // Best effort. - - execer := wsep.RemoteExecer(conn) - process, err := execer.Start(ctx, wsep.Command{ - Command: prog, - Args: args, - }) - if err != nil { - return xerrors.Errorf("exec remote process: %w", err) - } - // NOTE: If the copy routine fail, it will result in `process.Wait` to unblock and report an error. - go func() { _, _ = io.Copy(os.Stdout, process.Stdout()) }() // Best effort. - go func() { _, _ = io.Copy(os.Stderr, process.Stderr()) }() // Best effort. - - if err := process.Wait(); err != nil { - if code, ok := err.(wsep.ExitError); ok { - return xerrors.Errorf("%s exit status: %d", prog, code) - } - return xerrors.Errorf("execution failure: %w", err) - } - - return nil -} - -// initSync performs the initial synchronization of the directory. -func (s Sync) initSync() error { - flog.Info("doing initial sync (%s -> %s)", s.LocalDir, s.RemoteDir) - - start := time.Now() - // Delete old files on initial sync (e.g git checkout). - // Add the "/." to the local directory so rsync doesn't try to place the directory - // into the remote dir. - if err := s.syncPaths(true, s.LocalDir+"/.", s.RemoteDir); err != nil { - return err - } - flog.Success("finished initial sync (%s)", time.Since(start).Truncate(time.Millisecond)) - return nil -} - -func (s Sync) convertPath(local string) string { - relLocalPath, err := filepath.Rel(s.LocalDir, local) - if err != nil { - panic(err) - } - return filepath.Join(s.RemoteDir, relLocalPath) -} - -func (s Sync) handleCreate(localPath string) error { - target := s.convertPath(localPath) - - if err := s.syncPaths(false, localPath, target); err != nil { - // File was quickly deleted. - if _, e1 := os.Stat(localPath); os.IsNotExist(e1) { // NOTE: Discard any other stat error and just expose the syncPath one. - return nil - } - return err - } - return nil -} - -func (s Sync) handleDelete(localPath string) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - return s.remoteCmd(ctx, "rm", "-rf", s.convertPath(localPath)) -} - -func (s Sync) handleRename(localPath string) error { - // The rename operation is sent in two events, one - // for the old (gone) file and one for the new file. - // Catching both would require complex state. - // Instead, we turn it into a Create or Delete based - // on file existence. - info, err := os.Stat(localPath) - if err != nil { - if os.IsNotExist(err) { - return s.handleDelete(localPath) - } - return err - } - if info.IsDir() { - // Without this, the directory will be created as a subdirectory. - localPath += "/." - } - return s.handleCreate(localPath) -} - -func (s Sync) work(ev timedEvent) { - var ( - localPath = ev.Path() - err error - ) - switch ev.Event() { - case notify.Write, notify.Create: - err = s.handleCreate(localPath) - case notify.Rename: - err = s.handleRename(localPath) - case notify.Remove: - err = s.handleDelete(localPath) - default: - flog.Info("unhandled event %+v %s", ev.Event(), ev.Path()) - } - - log := fmt.Sprintf("%v %s (%s)", - ev.Event(), filepath.Base(localPath), time.Since(ev.CreatedAt).Truncate(time.Millisecond*10), - ) - if err != nil { - flog.Error(log+": %s", err) - } else { - flog.Success(log) - } -} - -// 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. -func (s Sync) workEventGroup(evs []timedEvent) { - cache := eventCache{} - for _, ev := range evs { - cache.Add(ev) - } - - // We want to process events concurrently but safely for speed. - // Because the event cache prevents duplicate events for the same file, race conditions of that type - // are impossible. - // What is possible is a dependency on a previous Rename or Create. For example, if a directory is renamed - // and then a file is moved to it. AFAIK this dependecy only exists with Directories. - // So, we sequentially process the list of directory Renames and Creates, and then concurrently - // perform all Writes. - for _, ev := range cache.SequentialEvents() { - s.work(ev) - } - - sem := semaphore.NewWeighted(8) - - var wg sync.WaitGroup - for _, ev := range cache.ConcurrentEvents() { - setConsoleTitle(fmtUpdateTitle(ev.Path())) - - wg.Add(1) - // TODO: Document why this error is discarded. See https://github.com/cdr/coder-cli/issues/122 for reference. - _ = sem.Acquire(context.Background(), 1) - - ev := ev // Copy the event in the scope to make sure the go routine use the proper value. - go func() { - defer sem.Release(1) - defer wg.Done() - s.work(ev) - }() - } - - wg.Wait() -} - -const ( - // maxinflightInotify sets the maximum number of inotifies before the - // sync just restarts. Syncing a large amount of small files (e.g .git - // or node_modules) is impossible to do performantly with individual - // rsyncs. - maxInflightInotify = 8 - maxEventDelay = 7 * time.Second - // maxAcceptableDispatch is the maximum amount of time before an event - // should begin its journey to the server. This sets a lower bound for - // perceivable latency, but the higher it is, the better the - // optimization. - maxAcceptableDispatch = 50 * time.Millisecond -) - -// Version returns remote protocol version as a string. -// Or, an error if one exists. -func (s Sync) Version() (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - conn, err := s.Client.DialWsep(ctx, &s.Env) - if err != nil { - return "", err - } - defer func() { _ = conn.Close(websocket.CloseNormalClosure, "") }() // Best effort. - - execer := wsep.RemoteExecer(conn) - process, err := execer.Start(ctx, wsep.Command{ - Command: "rsync", - Args: []string{"--version"}, - }) - if err != nil { - return "", err - } - buf := &bytes.Buffer{} - _, _ = io.Copy(buf, process.Stdout()) // Ignore error, if any, it would be handled by the process.Wait return. - - if err := process.Wait(); err != nil { - return "", err - } - - firstLine, err := buf.ReadString('\n') - if err != nil { - return "", err - } - - versionString := strings.Split(firstLine, "protocol version ") - - return versionString[1], nil -} - -// Run starts the sync synchronously. -// Use this command to debug what wasn't sync'd correctly: -// rsync -e "coder sh" -nicr ~/Projects/cdr/coder-cli/. ammar:/home/coder/coder-cli/ -func (s Sync) Run() error { - events := make(chan notify.EventInfo, maxInflightInotify) - // Set up a recursive watch. - // We do this before the initial sync so we can capture any changes - // that may have happened during sync. - if err := notify.Watch(path.Join(s.LocalDir, "..."), events, notify.All); err != nil { - return xerrors.Errorf("create watch: %w", err) - } - defer notify.Stop(events) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - if err := s.remoteCmd(ctx, "mkdir", "-p", s.RemoteDir); err != nil { - return xerrors.Errorf("create remote directory: %w", err) - } - - ap := activity.NewPusher(s.Client, s.Env.ID, activityName) - ap.Push(ctx) - - setConsoleTitle("⏳ syncing project") - if err := s.initSync(); err != nil { - return err - } - - if s.Init { - return nil - } - - flog.Info("watching %s for changes", s.LocalDir) - - var droppedEvents uint64 - // Timed events lets us track how long each individual file takes to - // update. - timedEvents := make(chan timedEvent, cap(events)) - go func() { - defer close(timedEvents) - for event := range events { - select { - case timedEvents <- timedEvent{ - CreatedAt: time.Now(), - EventInfo: event, - }: - default: - if atomic.AddUint64(&droppedEvents, 1) == 1 { - flog.Info("dropped event, sync should restart soon") - } - } - } - }() - - var eventGroup []timedEvent - - dispatchEventGroup := time.NewTicker(maxAcceptableDispatch) - defer dispatchEventGroup.Stop() - for { - const watchingFilesystemTitle = "🛰 watching filesystem" - setConsoleTitle(watchingFilesystemTitle) - - select { - case ev := <-timedEvents: - if atomic.LoadUint64(&droppedEvents) > 0 { - return ErrRestartSync - } - eventGroup = append(eventGroup, ev) - case <-dispatchEventGroup.C: - if len(eventGroup) == 0 { - continue - } - // We're too backlogged and should restart the sync. - if time.Since(eventGroup[0].CreatedAt) > maxEventDelay { - return ErrRestartSync - } - s.workEventGroup(eventGroup) - eventGroup = eventGroup[:0] - ap.Push(context.TODO()) - } - } -} - -const activityName = "sync" diff --git a/internal/sync/title.go b/internal/sync/title.go deleted file mode 100644 index c9a91c8b..00000000 --- a/internal/sync/title.go +++ /dev/null @@ -1,20 +0,0 @@ -package sync - -import ( - "fmt" - "os" - "path/filepath" - - "golang.org/x/crypto/ssh/terminal" -) - -func setConsoleTitle(title string) { - if !terminal.IsTerminal(int(os.Stdout.Fd())) { - return - } - fmt.Printf("\033]0;%s\007", title) -} - -func fmtUpdateTitle(path string) string { - return "🚀 updating " + filepath.Base(path) -} diff --git a/internal/x/xjson/duration.go b/internal/x/xjson/duration.go deleted file mode 100644 index 04ec1f17..00000000 --- a/internal/x/xjson/duration.go +++ /dev/null @@ -1,31 +0,0 @@ -package xjson - -import ( - "encoding/json" - "strconv" - "time" -) - -// MSDuration is a time.MSDuration that marshals to millisecond precision. -// While is looses precision, most javascript applications expect durations to be in milliseconds. -type MSDuration time.Duration - -// MarshalJSON marshals the duration to millisecond precision. -func (d MSDuration) MarshalJSON() ([]byte, error) { - du := time.Duration(d) - return json.Marshal(du.Milliseconds()) -} - -// UnmarshalJSON unmarshals a millisecond-precision integer to -// a time.Duration. -func (d *MSDuration) UnmarshalJSON(b []byte) error { - i, err := strconv.ParseInt(string(b), 10, 64) - if err != nil { - return err - } - - *d = MSDuration(time.Duration(i) * time.Millisecond) - return nil -} - -func (d MSDuration) String() string { return time.Duration(d).String() } diff --git a/internal/x/xtabwriter/tabwriter.go b/internal/x/xtabwriter/tabwriter.go deleted file mode 100644 index 6c28b647..00000000 --- a/internal/x/xtabwriter/tabwriter.go +++ /dev/null @@ -1,83 +0,0 @@ -package xtabwriter - -import ( - "fmt" - "os" - "reflect" - "strings" - "text/tabwriter" -) - -const structFieldTagKey = "tab" - -// NewWriter chooses reasonable defaults for a human readable output of tabular data. -func NewWriter() *tabwriter.Writer { - return tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) -} - -// StructValues tab delimits the values of a given struct. -// -// Tag a field `tab:"-"` to hide it from output. -func StructValues(data interface{}) string { - v := reflect.ValueOf(data) - s := &strings.Builder{} - for i := 0; i < v.NumField(); i++ { - if shouldHideField(v.Type().Field(i)) { - continue - } - fmt.Fprintf(s, "%v\t", v.Field(i).Interface()) - } - return s.String() -} - -// StructFieldNames tab delimits the field names of a given struct. -// -// Tag a field `tab:"-"` to hide it from output. -func StructFieldNames(data interface{}) string { - v := reflect.ValueOf(data) - s := &strings.Builder{} - for i := 0; i < v.NumField(); i++ { - field := v.Type().Field(i) - if shouldHideField(field) { - continue - } - fmt.Fprintf(s, "%s\t", fieldName(field)) - } - return s.String() -} - -// WriteTable writes the given list elements to stdout in a human readable -// tabular format. Headers abide by the `tab` struct tag. -// -// `tab:"-"` omits the field and no tag defaults to the Go identifier. -func WriteTable(length int, each func(i int) interface{}) error { - if length < 1 { - return nil - } - w := NewWriter() - defer func() { _ = w.Flush() }() // Best effort. - for ix := 0; ix < length; ix++ { - item := each(ix) - if ix == 0 { - if _, err := fmt.Fprintln(w, StructFieldNames(item)); err != nil { - return err - } - } - if _, err := fmt.Fprintln(w, StructValues(item)); err != nil { - return err - } - } - return nil -} - -func fieldName(f reflect.StructField) string { - custom, ok := f.Tag.Lookup(structFieldTagKey) - if ok { - return custom - } - return f.Name -} - -func shouldHideField(f reflect.StructField) bool { - return f.Tag.Get(structFieldTagKey) == "-" -} diff --git a/internal/x/xterminal/doc.go b/internal/x/xterminal/doc.go deleted file mode 100644 index 21e0ae0e..00000000 --- a/internal/x/xterminal/doc.go +++ /dev/null @@ -1,13 +0,0 @@ -// Package xterminal provides functions to change termios or console attributes -// and restore them later on. It supports Unix and Windows. -// -// This does the same thing as x/crypto/ssh/terminal on Linux. On Windows, it -// sets the same console modes as the terminal package but also sets -// `ENABLE_VIRTUAL_TERMINAL_INPUT` and `ENABLE_VIRTUAL_TERMINAL_PROCESSING` to -// allow for VT100 sequences in the console. This is important, otherwise Linux -// apps (with colors or ncurses) that are run through SSH or wsep get -// garbled in a Windows console. -// -// More details can be found out about Windows console modes here: -// https://docs.microsoft.com/en-us/windows/console/setconsolemode -package xterminal diff --git a/internal/x/xterminal/terminal.go b/internal/x/xterminal/terminal.go deleted file mode 100644 index ab21db04..00000000 --- a/internal/x/xterminal/terminal.go +++ /dev/null @@ -1,96 +0,0 @@ -// +build !windows - -package xterminal - -import ( - "context" - "os" - "os/signal" - - "golang.org/x/crypto/ssh/terminal" - "golang.org/x/sys/unix" -) - -// State differs per-platform. -type State struct { - s *terminal.State -} - -// MakeRaw sets the terminal to raw. -func MakeRaw(fd uintptr) (*State, error) { - previousState, err := terminal.MakeRaw(int(fd)) - if err != nil { - return nil, err - } - return &State{s: previousState}, nil -} - -// MakeOutputRaw does nothing on non-Windows platforms. -func MakeOutputRaw(fd uintptr) (*State, error) { return nil, nil } - -// Restore terminal back to original state. -func Restore(fd uintptr, state *State) error { - if state == nil { - return nil - } - - return terminal.Restore(int(fd), state.s) -} - -// ColorEnabled returns true on Linux if handle is a terminal. -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 uint16 - Width uint16 -} - -// ResizeEvents sends terminal resize events. -func ResizeEvents(ctx context.Context, termFD uintptr) chan ResizeEvent { - // Use a buffered chan to avoid blocking when we emit the initial resize event. - // We send the event right away while the main routine might not be ready just yet. - events := make(chan ResizeEvent, 1) - - go func() { - sigChan := make(chan os.Signal, 16) // Arbitrary large buffer size to allow for "continuous" resizing without blocking. - defer close(sigChan) - - // Terminal resize event are notified using the SIGWINCH signal, start watching for it. - signal.Notify(sigChan, unix.SIGWINCH) - defer signal.Stop(sigChan) - - // Emit an inital signal event to make sure the server receives our current window size. - select { - case <-ctx.Done(): - return - case sigChan <- unix.SIGWINCH: - } - - for { - select { - case <-ctx.Done(): - return - case <-sigChan: - width, height, err := terminal.GetSize(int(termFD)) - if err != nil { - return - } - event := ResizeEvent{ - Height: uint16(height), - Width: uint16(width), - } - select { - case <-ctx.Done(): - return - case events <- event: - } - - } - } - }() - - return events -} diff --git a/internal/x/xterminal/terminal_windows.go b/internal/x/xterminal/terminal_windows.go deleted file mode 100644 index ba93b8ec..00000000 --- a/internal/x/xterminal/terminal_windows.go +++ /dev/null @@ -1,130 +0,0 @@ -// +build windows - -package xterminal - -import ( - "context" - "time" - - "golang.org/x/crypto/ssh/terminal" - "golang.org/x/sys/windows" -) - -// State differs per-platform. -type State struct { - mode uint32 -} - -// makeRaw sets the terminal in raw mode and returns the previous state so it can be restored. -func makeRaw(handle windows.Handle, input bool) (uint32, error) { - var prevState uint32 - if err := windows.GetConsoleMode(handle, &prevState); err != nil { - return 0, err - } - - var raw uint32 - if input { - raw = prevState &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) - raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT - } else { - raw = prevState | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING - } - - if err := windows.SetConsoleMode(handle, raw); err != nil { - return 0, err - } - return prevState, nil -} - -// MakeRaw sets an input terminal to raw and enables VT100 processing. -func MakeRaw(handle uintptr) (*State, error) { - prevState, err := makeRaw(windows.Handle(handle), true) - if err != nil { - return nil, err - } - - return &State{mode: prevState}, nil -} - -// MakeOutputRaw sets an output terminal to raw and enables VT100 processing. -func MakeOutputRaw(handle uintptr) (*State, error) { - prevState, err := makeRaw(windows.Handle(handle), false) - if err != nil { - return nil, err - } - - return &State{mode: prevState}, nil -} - -// Restore terminal back to original state. -func Restore(handle uintptr, state *State) error { - return windows.SetConsoleMode(windows.Handle(handle), state.mode) -} - -// ColorEnabled returns true if VT100 processing is enabled on the output -// console. -func ColorEnabled(handle uintptr) (bool, error) { - var state uint32 - if err := windows.GetConsoleMode(windows.Handle(handle), &state); err != nil { - return false, err - } - - return state&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0, nil -} - -// ResizeEvent represent the new size of the terminal. -type ResizeEvent struct { - Height uint16 - Width uint16 -} - -func (s ResizeEvent) equal(s2 *ResizeEvent) bool { - if s2 == nil { - return false - } - return s.Height == s2.Height && s.Width == s2.Width -} - -// ResizeEvents sends terminal resize events when the dimensions change. -// Windows does not have a unix.SIGWINCH equivalent, so we poll the terminal size -// at a fixed interval -func ResizeEvents(ctx context.Context, termFD uintptr) chan ResizeEvent { - // Use a buffered chan to avoid blocking if the main is not ready yet when we send the initial resize event. - events := make(chan ResizeEvent, 1) - - go func() { - defer close(events) - - // On windows, as we don't have a signal to know the size changed, we - // use a ticker and emit then event if the current size differs from last time we checked. - ticker := time.NewTicker(100 * time.Millisecond) - defer ticker.Stop() - - var lastEvent *ResizeEvent - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - width, height, err := terminal.GetSize(int(windows.Handle(termFD))) - if err != nil { - return - } - event := ResizeEvent{ - Height: uint16(height), - Width: uint16(width), - } - if !event.equal(lastEvent) { - select { - case <-ctx.Done(): - return - case events <- event: - } - } - lastEvent = &event - } - } - }() - - return events -}