diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index ebbc08af..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - time: "06:00" - timezone: "America/Chicago" - commit-message: - prefix: "chore" - ignore: - # GitHub always delivers the latest versions for each major - # release tag, so handle updates manually - - dependency-name: "actions/*" - - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "daily" - time: "06:00" - timezone: "America/Chicago" - commit-message: - prefix: "chore" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index cb2e37a7..00000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: build -on: [push] - -jobs: - build: - runs-on: ubuntu-20.04 - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Build - run: make -j build/linux build/windows - - name: Upload - uses: actions/upload-artifact@v2 - with: - name: coder-cli - path: ./ci/bin/coder-cli-* - build_darwin: - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Install Gon - run: | - brew tap mitchellh/gon - brew install mitchellh/gon/gon - - name: Import Signing Certificates - uses: Apple-Actions/import-codesign-certs@v1 - with: - p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} - p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} - - name: Build - run: make build/macos - env: - AC_USERNAME: ${{ secrets.AC_USERNAME }} - AC_PASSWORD: ${{ secrets.AC_PASSWORD }} - - 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 1c0d03da..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: integration tests - run: ./ci/scripts/integration.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 7ac0fdcf..00000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,99 +0,0 @@ -on: - create: - tags: "v*" -name: create_github_release -jobs: - build: - name: Build binaries - runs-on: ubuntu-20.04 - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Build - run: make -j build/linux build/windows - - name: Upload linux - uses: actions/upload-artifact@v2 - with: - name: coder-cli-linux-amd64 - path: ./ci/bin/coder-cli-linux-amd64.tar.gz - - name: Upload windows - uses: actions/upload-artifact@v2 - with: - name: coder-cli-windows-386 - path: ./ci/bin/coder-cli-windows-386.zip - build_darwin: - name: Build darwin binary - runs-on: macos-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Install Gon - run: | - brew tap mitchellh/gon - brew install mitchellh/gon/gon - - name: Import Signing Certificates - uses: Apple-Actions/import-codesign-certs@v1 - with: - p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} - p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} - - name: Build Release Assets - run: make build/macos - env: - AC_USERNAME: ${{ secrets.AC_USERNAME }} - AC_PASSWORD: ${{ secrets.AC_PASSWORD }} - - name: Upload darwin - uses: actions/upload-artifact@v2 - with: - name: coder-cli-darwin-amd64 - path: ./ci/bin/coder-cli-darwin-amd64.zip - draft_release: - name: Create Release - runs-on: ubuntu-20.04 - needs: - - build_darwin - - build - steps: - - uses: actions/download-artifact@v2 - - name: content - run: sh -c "ls -al" - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - body: "" - draft: true - prerelease: false - - name: Upload Linux Release - id: upload-linux-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: coder-cli-linux-amd64/coder-cli-linux-amd64.tar.gz - asset_name: coder-cli-linux-amd64.tar.gz - asset_content_type: application/tar+gzip - - name: Upload MacOS Release - id: upload-macos-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: coder-cli-darwin-amd64/coder-cli-darwin-amd64.zip - asset_name: coder-cli-darwin-amd64.zip - asset_content_type: application/zip - - name: Upload Windows Release - id: upload-windows-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: coder-cli-windows-386/coder-cli-windows-386.zip - asset_name: coder-cli-windows-386.zip - asset_content_type: application/zip diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 9a3261ea..00000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,62 +0,0 @@ -name: test -on: [push] - -jobs: - fmt: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: fmt - uses: ./ci/image - with: - args: make -j fmt - - run: ./ci/scripts/files_changed.sh - lint: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - name: golangci-lint - uses: golangci/golangci-lint-action@v2 - with: - # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.36 - test: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: test - uses: ./ci/image - env: - COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CODER_URL: ${{ secrets.CODER_URL }} - CODER_EMAIL: ${{ secrets.CODER_EMAIL }} - CODER_PASSWORD: ${{ secrets.CODER_PASSWORD }} - with: - args: make -j test/coverage - gendocs: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 - 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: make -j gendocs - - run: ./ci/scripts/files_changed.sh diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c3d31169..00000000 --- a/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -./coder -.idea -ci/bin -cmd/coder/coder -ci/integration/bin -ci/integration/env.sh -coder-sdk/env.sh -.vscode diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index 423540d5..00000000 --- a/.golangci.yml +++ /dev/null @@ -1,71 +0,0 @@ -# See https://golangci-lint.run/usage/configuration/ -linters-settings: - goconst: - min-len: 4 - min-occurrences: 3 - gocognit: - min-complexity: 46 - nestif: - min-complexity: 10 - govet: - settings: - printf: - funcs: # Run `go tool vet help printf` to see available settings for `printf` analyzer. - - (cdr.dev/coder-cli/pkg/clog).Tipf - - (cdr.dev/coder-cli/pkg/clog).Hintf - - (cdr.dev/coder-cli/pkg/clog).Causef -linters: - disable-all: true - exclude-use-default: false - enable: - - megacheck - - govet - - golint - - goconst - - gocognit - - nestif - - misspell - - unparam - - unused - - bodyclose - - deadcode - - depguard - - dogsled - - errcheck - - unconvert - - unparam - - varcheck - - whitespace - - structcheck - - stylecheck - - typecheck - - nolintlint - - rowserrcheck - - scopelint - - goprintffuncname - - gofmt - - godot - - ineffassign - - gocritic - -issues: - exclude-use-default: false - exclude: - # errcheck: Almost all programs ignore errors on these functions and in most cases it's ok - - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked - # golint: False positive when tests are defined in package 'test' - - func name will be used as test\.Test.* by other packages, and that stutters; consider calling this - # govet: Common false positives - - (possible misuse of unsafe.Pointer|should have signature) - # staticcheck: Developers tend to write in C-style with an explicit 'break' in a 'switch', so it's ok to ignore - - ineffective break statement. Did you mean to break out of the outer loop - # gosec: Too many false-positives on 'unsafe' usage - - Use of unsafe calls should be audited - # gosec: Too many false-positives for parametrized shell calls - - Subprocess launch(ed with variable|ing should be audited) - # gosec: Duplicated errcheck checks - - G104 - # gosec: Too many issues in popular repos - - (Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less) - # gosec: False positive is triggered by 'src, err := ioutil.ReadFile(filename)' - - Potential file inclusion via variable \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index edd1a303..00000000 --- a/Makefile +++ /dev/null @@ -1,51 +0,0 @@ -# Makefile for Coder CLI - -.PHONY: clean build build/macos build/windows build/linux fmt lint gendocs test/go dev - -PROJECT_ROOT := $(shell git rev-parse --show-toplevel) -MAKE_ROOT := $(shell pwd) - -clean: - rm -rf ./ci/bin - -build: build/macos build/windows build/linux - -build/macos: - # requires darwin - CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ./ci/scripts/build.sh -build/windows: - CGO_ENABLED=0 GOOS=windows GOARCH=386 ./ci/scripts/build.sh -build/linux: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ./ci/scripts/build.sh - -fmt: - go mod tidy - gofmt -w -s . - goimports -w "-local=$$(go list -m)" . - -lint: - golangci-lint run -c .golangci.yml - -gendocs: - rm -rf ./docs - mkdir ./docs - go run ./cmd/coder gen-docs ./docs - -test/go: - go test $$(go list ./... | grep -v pkg/tcli | grep -v ci/integration) - -test/coverage: - go test \ - -race \ - -covermode atomic \ - -coverprofile coverage \ - $$(go list ./... | grep -v pkg/tcli | grep -v ci/integration) - - goveralls -coverprofile=coverage -service=github - -dev: build/linux - @echo "removing project root binary if exists" - -rm ./coder - @echo "untarring..." - @tar -xzf ./ci/bin/coder-cli-linux-amd64.tar.gz - @echo "new dev binary ready" \ No newline at end of file diff --git a/README.md b/README.md index 9757c3ec..4fbeb053 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,13 @@ -# Coder CLI +# Coder v1 CLI [![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) -![build](https://github.com/cdr/coder-cli/workflows/build/badge.svg) -[![Go Report Card](https://goreportcard.com/badge/cdr.dev/coder-cli)](https://goreportcard.com/report/cdr.dev/coder-cli) -`coder` is a command line utility for Coder. +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. -To report bugs and request features, please [open an issue](https://github.com/cdr/coder-cli/issues/new). +The Coder v1 CLI is now closed-source. You may download binary releases from this repo. -## Installation - -### Homebrew (Mac, Linux) - -```sh -brew install cdr/coder/coder-cli -``` - -### Download (Windows, Linux, Mac) - -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. - -## Usage - -View the usage documentation [here](./docs/coder.md). - -You can find additional Coder usage documentation on [coder.com/docs/cli](https://coder.com/docs/cli). +[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 1daee639..00000000 --- a/ci/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# ci - -## checks - -- `steps/build.sh` builds release assets with the appropriate tag injected. Required to pass for merging. -- `steps/fmt.sh` formats all Go source files. -- `steps/gendocs.sh` generates CLI documentation into `/docs` from the command specifications. -- `steps/lint.sh` lints all Go source files based on the rules set fourth in `/.golangci.yml`. - - -## 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. - -### running - -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 -./ci/steps/integration.sh -``` diff --git a/ci/gon.json b/ci/gon.json deleted file mode 100644 index 0762638f..00000000 --- a/ci/gon.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "source": ["./coder"], - "bundle_id": "com.coder.cli", - "sign": { - "application_identity": "3C4F31D15F9D57461A8D7D0BD970D23CE1F7C2BE" - }, - "zip": { - "output_path": "coder.zip" - } -} \ No newline at end of file diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile deleted file mode 100644 index f814866d..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 github.com/mattn/goveralls -RUN apt update && apt install grep diff --git a/ci/integration/Dockerfile b/ci/integration/Dockerfile deleted file mode 100644 index 70dcc2c0..00000000 --- a/ci/integration/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM ubuntu:20.04 - -RUN apt-get update && apt-get install -y jq curl build-essential diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go deleted file mode 100644 index f92d733e..00000000 --- a/ci/integration/integration_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package integration - -import ( - "context" - "math/rand" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/pkg/tcli" -) - -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(), 20*time.Minute) - defer cancel() - - c, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ - Image: "coder-cli-integration:latest", - 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.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 envs ls -o json").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder tokens").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder tokens ls").Assert(t, - tcli.Success(), - ) - - c.Run(ctx, "coder tokens ls -o json").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(), - ) - - c.Run(ctx, "coder tokens ls").Assert(t, - tcli.Error(), - ) - }) -} - -var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) - -func randString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyz" - 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/setup_test.go b/ci/integration/setup_test.go deleted file mode 100644 index 45cb7f04..00000000 --- a/ci/integration/setup_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package integration - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/pkg/tcli" -) - -// 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 && make build/linux && 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 $HOME/.config/coder && cat > $HOME/.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(), - ) - - cmd = exec.CommandContext(ctx, "sh", "-c", "cat > $HOME/.config/coder/url") - cmd.Stdin = strings.NewReader(creds.url) - runner.RunCmd(cmd).Assert(t, - tcli.Success(), - ) -} diff --git a/ci/integration/ssh_test.go b/ci/integration/ssh_test.go deleted file mode 100644 index 6a084a0d..00000000 --- a/ci/integration/ssh_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package integration - -import ( - "context" - "testing" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/tcli" -) - -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/statictokens_test.go b/ci/integration/statictokens_test.go deleted file mode 100644 index d38dcd99..00000000 --- a/ci/integration/statictokens_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package integration - -import ( - "context" - "fmt" - "os" - "os/exec" - "strings" - "testing" - - "cdr.dev/coder-cli/pkg/tcli" -) - -func TestStaticAuth(t *testing.T) { - t.Parallel() - t.Skip() - run(t, "static-auth-test", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) { - headlessLogin(ctx, t, c) - - c.Run(ctx, "coder tokens ls").Assert(t, - tcli.Success(), - ) - - var result *tcli.CommandResult - tokenName := randString(5) - c.Run(ctx, "coder tokens create "+tokenName).Assert(t, - tcli.Success(), - tcli.GetResult(&result), - ) - - // remove loging credentials - c.Run(ctx, "rm -rf ~/.config/coder").Assert(t, - tcli.Success(), - ) - - // make requests with token environment variable authentication - cmd := exec.CommandContext(ctx, "sh", "-c", - fmt.Sprintf("export CODER_URL=%s && export CODER_TOKEN=$(cat) && coder envs ls", os.Getenv("CODER_URL")), - ) - cmd.Stdin = strings.NewReader(string(result.Stdout)) - c.RunCmd(cmd).Assert(t, - tcli.Success(), - ) - - // should error when the environment variabels aren't set - c.Run(ctx, "coder envs ls").Assert(t, - tcli.Error(), - ) - }) -} diff --git a/ci/scripts/build.sh b/ci/scripts/build.sh deleted file mode 100755 index d19f1bf6..00000000 --- a/ci/scripts/build.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -# Make pushd and popd silent -pushd() { builtin pushd "$@" >/dev/null; } -popd() { builtin popd >/dev/null; } - -set -euo pipefail - -cd "$(git rev-parse --show-toplevel)/ci/scripts" - -tag=$(git describe --tags) - -echo "--- building coder-cli for $GOOS-$GOARCH" - -tmpdir=$(mktemp -d) -go build -ldflags "-X cdr.dev/coder-cli/internal/version.Version=${tag}" -o "$tmpdir/coder" ../../cmd/coder - -cp ../gon.json $tmpdir/gon.json - -pushd "$tmpdir" -case "$GOOS" in -"windows") - artifact="coder-cli-$GOOS-$GOARCH.zip" - mv coder coder.exe - zip "$artifact" coder.exe - ;; -"linux") - artifact="coder-cli-$GOOS-$GOARCH.tar.gz" - tar -czf "$artifact" coder - ;; -"darwin") - if [[ ${CI-} ]]; then - artifact="coder-cli-$GOOS-$GOARCH.zip" - gon -log-level debug ./gon.json - mv coder.zip $artifact - else - artifact="coder-cli-$GOOS-$GOARCH.tar.gz" - tar -czf "$artifact" coder - echo "--- warning: not in ci, skipping signed release of darwin" - fi - ;; -esac -popd - -mkdir -p ../bin -cp "$tmpdir/$artifact" ../bin/$artifact -rm -rf "$tmpdir" diff --git a/ci/scripts/files_changed.sh b/ci/scripts/files_changed.sh deleted file mode 100755 index 759c68d3..00000000 --- a/ci/scripts/files_changed.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -cd "$(git rev-parse --show-toplevel)" - -if [[ $(git ls-files --other --modified --exclude-standard) ]]; then - echo "Files have changed:" - git -c color.ui=never status - exit 1 -fi diff --git a/ci/scripts/integration.sh b/ci/scripts/integration.sh deleted file mode 100755 index 6f82475c..00000000 --- a/ci/scripts/integration.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -cd "$(git rev-parse --show-toplevel)" - -echo "--- building integration test image" -docker build -f ./ci/integration/Dockerfile -t coder-cli-integration:latest . - -echo "--- starting integration tests" -go test ./ci/integration -count=1 diff --git a/cmd/coder/main.go b/cmd/coder/main.go deleted file mode 100644 index 51ce8614..00000000 --- a/cmd/coder/main.go +++ /dev/null @@ -1,50 +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/version" - "cdr.dev/coder-cli/internal/x/xterminal" - "cdr.dev/coder-cli/pkg/clog" -) - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - - // 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 { - clog.Log(clog.Fatal(fmt.Sprintf("set output to raw: %s", err))) - cancel() - os.Exit(1) - } - restoreTerminal := 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.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH) - - if err := app.ExecuteContext(ctx); err != nil { - clog.Log(err) - cancel() - restoreTerminal() - os.Exit(1) - } - cancel() - restoreTerminal() -} diff --git a/coder-sdk/README.md b/coder-sdk/README.md deleted file mode 100644 index 75ffd8dd..00000000 --- a/coder-sdk/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# coder-sdk - -`coder-sdk` is a Go client library for [Coder](https://coder.com). -It is not yet stable and therefore we do not recommend depending on the current state of its public APIs. - -## Usage - -```bash -go get cdr.dev/coder-cli/coder-sdk -``` diff --git a/coder-sdk/activity.go b/coder-sdk/activity.go deleted file mode 100644 index c885f619..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 *DefaultClient) PushActivity(ctx context.Context, source, envID string) error { - resp, err := c.request(ctx, http.MethodPost, "/api/private/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/activity_test.go b/coder-sdk/activity_test.go deleted file mode 100644 index 807b04f9..00000000 --- a/coder-sdk/activity_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package coder_test - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func TestPushActivity(t *testing.T) { - t.Parallel() - - const source = "test" - const envID = "602d377a-e6b8d763cae7561885c5f1b2" - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "PushActivity is a POST", http.MethodPost, r.Method) - assert.Equal(t, "URL matches", "/api/private/metrics/usage/push", r.URL.Path) - - expected := map[string]interface{}{ - "source": source, - "environment_id": envID, - } - var request map[string]interface{} - err := json.NewDecoder(r.Body).Decode(&request) - assert.Success(t, "error decoding JSON", err) - assert.Equal(t, "unexpected request data", expected, request) - - w.WriteHeader(http.StatusOK) - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: "SwdcSoq5Jc-0C1r8wfwm7h6h9i0RDk7JT", - }) - assert.Success(t, "failed to create coder.Client", err) - - err = client.PushActivity(context.Background(), source, envID) - assert.Success(t, "expected successful response from PushActivity", err) -} diff --git a/coder-sdk/client.go b/coder-sdk/client.go deleted file mode 100644 index 6d172126..00000000 --- a/coder-sdk/client.go +++ /dev/null @@ -1,128 +0,0 @@ -package coder - -import ( - "context" - "errors" - "net/http" - "net/url" - "time" - - "golang.org/x/xerrors" -) - -// ensure that DefaultClient implements Client. -var _ = Client(&DefaultClient{}) - -// Me is the user ID of the authenticated user. -const Me = "me" - -// ClientOptions contains options for the Coder SDK Client. -type ClientOptions struct { - // BaseURL is the root URL of the Coder installation (required). - BaseURL *url.URL - - // Client is the http.Client to use for requests (optional). - // - // If omitted, the http.DefaultClient will be used. - HTTPClient *http.Client - - // Token is the API Token used to authenticate (optional). - // - // If Token is provided, the DefaultClient will use it to - // authenticate. If it is not provided, the client requires - // another type of credential, such as an Email/Password pair. - Token string - - // Email used to authenticate with Coder. - // - // If you supply an Email and Password pair, NewClient will - // exchange these credentials for a Token during initialization. - // This is only applicable for the built-in authentication - // provider. The client will not retain these credentials in - // memory after NewClient returns. - Email string - - // Password used to authenticate with Coder. - // - // If you supply an Email and Password pair, NewClient will - // exchange these credentials for a Token during initialization. - // This is only applicable for the built-in authentication - // provider. The client will not retain these credentials in - // memory after NewClient returns. - Password string -} - -// NewClient creates a new default Coder SDK client. -func NewClient(opts ClientOptions) (*DefaultClient, error) { - httpClient := opts.HTTPClient - if httpClient == nil { - httpClient = http.DefaultClient - } - - if opts.BaseURL == nil { - return nil, errors.New("the BaseURL parameter is required") - } - - token := opts.Token - if token == "" { - if opts.Email == "" || opts.Password == "" { - return nil, errors.New("either an API Token or email/password pair are required") - } - - // Exchange the username/password for a token. - // We apply a default timeout of 5 seconds here. - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - - resp, err := LoginWithPassword(ctx, httpClient, opts.BaseURL, &LoginRequest{ - Email: opts.Email, - Password: opts.Password, - }) - if err != nil { - return nil, xerrors.Errorf("failed to login with email/password: %w", err) - } - - token = resp.SessionToken - if token == "" { - return nil, errors.New("server returned an empty session token") - } - } - - // TODO: add basic validation to make sure the token looks OK. - - client := &DefaultClient{ - baseURL: opts.BaseURL, - httpClient: httpClient, - token: token, - } - - return client, nil -} - -// DefaultClient is the default implementation of the coder.Client -// interface. -// -// The empty value is meaningless and the fields are unexported; -// use NewClient to create an instance. -type DefaultClient struct { - // baseURL is the URL (scheme, hostname/IP address, port, - // path prefix of the Coder installation) - baseURL *url.URL - - // httpClient is the http.Client used to issue requests. - httpClient *http.Client - - // token is the API Token credential. - token string -} - -// Token returns the API Token used to authenticate. -func (c *DefaultClient) Token() string { - return c.token -} - -// BaseURL returns the BaseURL configured for this Client. -func (c *DefaultClient) BaseURL() url.URL { - return *c.baseURL -} diff --git a/coder-sdk/client_test.go b/coder-sdk/client_test.go deleted file mode 100644 index 732678d0..00000000 --- a/coder-sdk/client_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package coder_test - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func TestAuthentication(t *testing.T) { - t.Parallel() - - const token = "g4mtIPUaKt-pPl9Q0xmgKs7acSypHt4Jf" - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotToken := r.Header.Get("Session-Token") - assert.Equal(t, "token does not match", token, gotToken) - - w.WriteHeader(http.StatusServiceUnavailable) - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: token, - }) - assert.Success(t, "failed to create coder.Client", err) - - assert.Equal(t, "expected Token to match", token, client.Token()) - assert.Equal(t, "expected BaseURL to match", *u, client.BaseURL()) - - _, err = client.APIVersion(context.Background()) - assert.Success(t, "failed to get API version information", err) -} - -func TestPasswordAuthentication(t *testing.T) { - t.Parallel() - - const email = "user@coder.com" - const password = "coder4all" - const token = "g4mtIPUaKt-pPl9Q0xmgKs7acSypHt4Jf" - - mux := http.NewServeMux() - mux.HandleFunc("/auth/basic/login", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "login is a POST", http.MethodPost, r.Method) - - expected := map[string]interface{}{ - "email": email, - "password": password, - } - var request map[string]interface{} - err := json.NewDecoder(r.Body).Decode(&request) - assert.Success(t, "error decoding JSON", err) - assert.Equal(t, "unexpected request data", expected, request) - - response := map[string]interface{}{ - "session_token": token, - } - - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(response) - assert.Success(t, "error encoding JSON", err) - }) - mux.HandleFunc("/api/v0/users/me", func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Users is a GET", http.MethodGet, r.Method) - - gotToken := r.Header.Get("Session-Token") - assert.Equal(t, "expected session token to match return of login", token, gotToken) - - user := map[string]interface{}{ - "id": "default", - "email": email, - "username": "charlie", - "name": "Charlie Root", - "roles": []coder.Role{coder.SiteAdmin}, - "temporary_password": false, - "login_type": coder.LoginTypeBuiltIn, - "key_regenerated_at": time.Now(), - "created_at": time.Now(), - "updated_at": time.Now(), - } - - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(user) - assert.Success(t, "error encoding JSON", err) - }) - server := httptest.NewTLSServer(mux) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - assert.Equal(t, "expected HTTPS base URL", "https", u.Scheme) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - HTTPClient: server.Client(), - Email: email, - Password: password, - }) - assert.Success(t, "failed to create Client", err) - assert.Equal(t, "expected token to match", token, client.Token()) - - user, err := client.Me(context.Background()) - assert.Success(t, "failed to get information about current user", err) - assert.Equal(t, "expected test user", email, user.Email) -} - -func TestContextRoot(t *testing.T) { - t.Parallel() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Users is a GET", http.MethodGet, r.Method) - assert.Equal(t, "expected context root", "/context-root/api/v0/users", r.URL.Path) - - w.WriteHeader(http.StatusServiceUnavailable) - })) - t.Cleanup(func() { - server.Close() - }) - - contextRoots := []string{ - "/context-root", - "/context-root/", - } - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - for _, prefix := range contextRoots { - u.Path = prefix - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: "FrOgA6xhpM-p5nTfsupmvzYJA6DJSOUoE", - }) - assert.Success(t, "failed to create coder.Client", err) - - _, err = client.Users(context.Background()) - assert.Error(t, "expected 503 error", err) - } -} diff --git a/coder-sdk/config.go b/coder-sdk/config.go deleted file mode 100644 index 7012f90d..00000000 --- a/coder-sdk/config.go +++ /dev/null @@ -1,139 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -// AuthProviderType is an enum of each valid auth provider. -type AuthProviderType string - -// AuthProviderType enum. -const ( - AuthProviderBuiltIn AuthProviderType = "built-in" - AuthProviderSAML AuthProviderType = "saml" - AuthProviderOIDC AuthProviderType = "oidc" -) - -// ConfigAuth describes the authentication configuration for a Coder deployment. -type ConfigAuth struct { - ProviderType *AuthProviderType `json:"provider_type"` - OIDC *ConfigOIDC `json:"oidc"` - SAML *ConfigSAML `json:"saml"` -} - -// ConfigOIDC describes the OIDC configuration for single-signon support in Coder. -type ConfigOIDC struct { - ClientID *string `json:"client_id"` - ClientSecret *string `json:"client_secret"` - Issuer *string `json:"issuer"` -} - -// ConfigSAML describes the SAML configuration values. -type ConfigSAML struct { - IdentityProviderMetadataURL *string `json:"idp_metadata_url"` - SignatureAlgorithm *string `json:"signature_algorithm"` - NameIDFormat *string `json:"name_id_format"` - PrivateKey *string `json:"private_key"` - PublicKeyCertificate *string `json:"public_key_certificate"` -} - -// ConfigOAuthBitbucketServer describes the Bitbucket integration configuration for a Coder deployment. -type ConfigOAuthBitbucketServer struct { - BaseURL string `json:"base_url" diff:"oauth.bitbucket_server.base_url"` -} - -// ConfigOAuthGitHub describes the Github integration configuration for a Coder deployment. -type ConfigOAuthGitHub struct { - BaseURL string `json:"base_url"` - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` -} - -// ConfigOAuthGitLab describes the GitLab integration configuration for a Coder deployment. -type ConfigOAuthGitLab struct { - BaseURL string `json:"base_url"` - ClientID string `json:"client_id" ` - ClientSecret string `json:"client_secret"` -} - -// ConfigOAuth describes the aggregate git integration configuration for a Coder deployment. -type ConfigOAuth struct { - BitbucketServer ConfigOAuthBitbucketServer `json:"bitbucket_server"` - GitHub ConfigOAuthGitHub `json:"github"` - GitLab ConfigOAuthGitLab `json:"gitlab"` -} - -// SiteConfigAuth fetches the sitewide authentication configuration. -func (c *DefaultClient) SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) { - var conf ConfigAuth - if err := c.requestBody(ctx, http.MethodGet, "/api/private/auth/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigAuth sets the sitewide authentication configuration. -func (c *DefaultClient) PutSiteConfigAuth(ctx context.Context, req ConfigAuth) error { - return c.requestBody(ctx, http.MethodPut, "/api/private/auth/config", req, nil) -} - -// SiteConfigOAuth fetches the sitewide git provider OAuth configuration. -func (c *DefaultClient) SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) { - var conf ConfigOAuth - if err := c.requestBody(ctx, http.MethodGet, "/api/private/oauth/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigOAuth sets the sitewide git provider OAuth configuration. -func (c *DefaultClient) PutSiteConfigOAuth(ctx context.Context, req ConfigOAuth) error { - return c.requestBody(ctx, http.MethodPut, "/api/private/oauth/config", req, nil) -} - -type configSetupMode struct { - SetupMode bool `json:"setup_mode"` -} - -// SiteSetupModeEnabled fetches the current setup_mode state of a Coder deployment. -func (c *DefaultClient) SiteSetupModeEnabled(ctx context.Context) (bool, error) { - var conf configSetupMode - if err := c.requestBody(ctx, http.MethodGet, "/api/private/config/setup-mode", nil, &conf); err != nil { - return false, err - } - return conf.SetupMode, nil -} - -// ExtensionMarketplaceType is an enum of the valid extension marketplace configurations. -type ExtensionMarketplaceType string - -// ExtensionMarketplaceType enum. -const ( - ExtensionMarketplaceInternal ExtensionMarketplaceType = "internal" - ExtensionMarketplaceCustom ExtensionMarketplaceType = "custom" - ExtensionMarketplacePublic ExtensionMarketplaceType = "public" -) - -// MarketplaceExtensionPublicURL is the URL of the coder.com public marketplace that serves open source Code OSS extensions. -const MarketplaceExtensionPublicURL = "https://extensions.coder.com/api" - -// ConfigExtensionMarketplace describes the sitewide extension marketplace configuration. -type ConfigExtensionMarketplace struct { - URL string `json:"url"` - Type ExtensionMarketplaceType `json:"type"` -} - -// SiteConfigExtensionMarketplace fetches the extension marketplace configuration. -func (c *DefaultClient) SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExtensionMarketplace, error) { - var conf ConfigExtensionMarketplace - if err := c.requestBody(ctx, http.MethodGet, "/api/private/extensions/config", nil, &conf); err != nil { - return nil, err - } - return &conf, nil -} - -// PutSiteConfigExtensionMarketplace sets the extension marketplace configuration. -func (c *DefaultClient) PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error { - return c.requestBody(ctx, http.MethodPut, "/api/private/extensions/config", req, nil) -} diff --git a/coder-sdk/devurl.go b/coder-sdk/devurl.go deleted file mode 100644 index ea7e013e..00000000 --- a/coder-sdk/devurl.go +++ /dev/null @@ -1,63 +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" table:"-"` - URL string `json:"url" table:"URL"` - Port int `json:"port" table:"Port"` - Access string `json:"access" table:"Access"` - Name string `json:"name" table:"Name"` - Scheme string `json:"scheme" table:"Scheme"` -} - -type delDevURLRequest struct { - EnvID string `json:"environment_id"` - DevURLID string `json:"url_id"` -} - -// DeleteDevURL deletes the specified devurl. -func (c *DefaultClient) DeleteDevURL(ctx context.Context, envID, urlID string) error { - reqURL := fmt.Sprintf("/api/v0/environments/%s/devurls/%s", envID, urlID) - - return c.requestBody(ctx, http.MethodDelete, reqURL, delDevURLRequest{ - EnvID: envID, - DevURLID: urlID, - }, nil) -} - -// CreateDevURLReq defines the request parameters for creating a new DevURL. -type CreateDevURLReq struct { - EnvID string `json:"environment_id"` - Port int `json:"port"` - Access string `json:"access"` - Name string `json:"name"` - Scheme string `json:"scheme"` -} - -// CreateDevURL inserts a new devurl for the authenticated user. -func (c *DefaultClient) CreateDevURL(ctx context.Context, envID string, req CreateDevURLReq) error { - return c.requestBody(ctx, http.MethodPost, "/api/v0/environments/"+envID+"/devurls", req, nil) -} - -// DevURLs fetches the Dev URLs for a given environment. -func (c *DefaultClient) DevURLs(ctx context.Context, envID string) ([]DevURL, error) { - var devurls []DevURL - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments/"+envID+"/devurls", nil, &devurls); err != nil { - return nil, err - } - return devurls, nil -} - -// PutDevURLReq defines the request parameters for overwriting a DevURL. -type PutDevURLReq CreateDevURLReq - -// PutDevURL updates an existing devurl for the authenticated user. -func (c *DefaultClient) PutDevURL(ctx context.Context, envID, urlID string, req PutDevURLReq) error { - return c.requestBody(ctx, http.MethodPut, "/api/v0/environments/"+envID+"/devurls/"+urlID, req, nil) -} diff --git a/coder-sdk/doc.go b/coder-sdk/doc.go deleted file mode 100644 index 5fe3bd46..00000000 --- a/coder-sdk/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package coder provides simple APIs for integrating Go applications with Coder. -package coder diff --git a/coder-sdk/env.go b/coder-sdk/env.go deleted file mode 100644 index e807b219..00000000 --- a/coder-sdk/env.go +++ /dev/null @@ -1,364 +0,0 @@ -package coder - -import ( - "context" - "io" - "net/http" - "net/url" - "time" - - "cdr.dev/wsep" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" -) - -// Environment describes a Coder environment. -type Environment struct { - ID string `json:"id" table:"-"` - Name string `json:"name" table:"Name"` - ImageID string `json:"image_id" table:"-"` - ImageTag string `json:"image_tag" table:"ImageTag"` - OrganizationID string `json:"organization_id" table:"-"` - UserID string `json:"user_id" table:"-"` - LastBuiltAt time.Time `json:"last_built_at" table:"-"` - CPUCores float32 `json:"cpu_cores" table:"CPUCores"` - MemoryGB float32 `json:"memory_gb" table:"MemoryGB"` - DiskGB int `json:"disk_gb" table:"DiskGB"` - GPUs int `json:"gpus" table:"-"` - Updating bool `json:"updating" table:"-"` - LatestStat EnvironmentStat `json:"latest_stat" table:"Status"` - RebuildMessages []RebuildMessage `json:"rebuild_messages" table:"-"` - CreatedAt time.Time `json:"created_at" table:"-"` - UpdatedAt time.Time `json:"updated_at" table:"-"` - LastOpenedAt time.Time `json:"last_opened_at" table:"-"` - LastConnectionAt time.Time `json:"last_connection_at" table:"-"` - AutoOffThreshold Duration `json:"auto_off_threshold" table:"-"` - UseContainerVM bool `json:"use_container_vm" table:"CVM"` - ResourcePoolID string `json:"resource_pool_id" table:"-"` -} - -// 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 Duration `json:"auto_off_threshold"` -} - -// 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"` - OrgID string `json:"org_id"` - ImageTag string `json:"image_tag"` - CPUCores float32 `json:"cpu_cores"` - MemoryGB float32 `json:"memory_gb"` - DiskGB int `json:"disk_gb"` - GPUs int `json:"gpus"` - UseContainerVM bool `json:"use_container_vm"` - ResourcePoolID string `json:"resource_pool_id"` - Namespace string `json:"namespace"` - EnableAutoStart bool `json:"autostart_enabled"` - - // TemplateID comes from the parse template route on cemanager. - TemplateID string `json:"template_id,omitempty"` -} - -// CreateEnvironment sends a request to create an environment. -func (c *DefaultClient) CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) (*Environment, error) { - var env Environment - if err := c.requestBody(ctx, http.MethodPost, "/api/v0/environments", req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// ParseTemplateRequest parses a template. If Local is a non-nil reader -// it will obviate any other fields on the request. -type ParseTemplateRequest struct { - RepoURL string `json:"repo_url"` - Ref string `json:"ref"` - Filepath string `json:"filepath"` - OrgID string `json:"-"` - Local io.Reader `json:"-"` -} - -// TemplateVersion is a Workspaces As Code (WAC) template. -// For now, let's not interpret it on the CLI level. We just need -// to forward this as part of the create env request. -type TemplateVersion struct { - ID string `json:"id"` - TemplateID string `json:"template_id"` - // FileHash is the sha256 hash of the template's file contents. - FileHash string `json:"file_hash"` - // Commit is the git commit from which the template was derived. - Commit string `json:"commit"` - CommitMessage string `json:"commit_message"` - CreatedAt time.Time `json:"created_at"` -} - -// ParseTemplate parses a template config. It support both remote repositories and local files. -// If a local file is specified then all other values in the request are ignored. -func (c *DefaultClient) ParseTemplate(ctx context.Context, req ParseTemplateRequest) (*TemplateVersion, error) { - const path = "/api/private/environments/template/parse" - var ( - tpl TemplateVersion - opts []requestOption - headers = http.Header{} - query = url.Values{} - ) - - query.Set("org-id", req.OrgID) - - opts = append(opts, withQueryParams(query)) - - if req.Local == nil { - if err := c.requestBody(ctx, http.MethodPost, path, req, &tpl, opts...); err != nil { - return &tpl, err - } - return &tpl, nil - } - - headers.Set("Content-Type", "application/octet-stream") - opts = append(opts, withBody(req.Local), withHeaders(headers)) - - err := c.requestBody(ctx, http.MethodPost, path, nil, &tpl, opts...) - if err != nil { - return &tpl, err - } - - return &tpl, nil -} - -// CreateEnvironmentFromRepo sends a request to create an environment from a repository. -func (c *DefaultClient) CreateEnvironmentFromRepo(ctx context.Context, orgID string, req TemplateVersion) (*Environment, error) { - var env Environment - if err := c.requestBody(ctx, http.MethodPost, "/api/private/orgs/"+orgID+"/environments/from-repo", req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// Environments lists environments returned by the given filter. -// TODO: add the filter options, explore performance issue. -func (c *DefaultClient) Environments(ctx context.Context) ([]Environment, error) { - var envs []Environment - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments", nil, &envs); err != nil { - return nil, err - } - return envs, nil -} - -// UserEnvironmentsByOrganization gets the list of environments owned by the given user. -func (c *DefaultClient) UserEnvironmentsByOrganization(ctx context.Context, userID, orgID string) ([]Environment, error) { - var ( - envs []Environment - query = url.Values{} - ) - - query.Add("orgs", orgID) - query.Add("users", userID) - - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments", nil, &envs, withQueryParams(query)); err != nil { - return nil, err - } - return envs, nil -} - -// DeleteEnvironment deletes the environment. -func (c *DefaultClient) DeleteEnvironment(ctx context.Context, envID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/environments/"+envID, nil, nil) -} - -// StopEnvironment stops the environment. -func (c *DefaultClient) StopEnvironment(ctx context.Context, envID string) error { - return c.requestBody(ctx, http.MethodPut, "/api/v0/environments/"+envID+"/stop", nil, nil) -} - -// UpdateEnvironmentReq defines the update operation, only setting -// nil-fields. -type UpdateEnvironmentReq struct { - ImageID *string `json:"image_id"` - ImageTag *string `json:"image_tag"` - CPUCores *float32 `json:"cpu_cores"` - MemoryGB *float32 `json:"memory_gb"` - DiskGB *int `json:"disk_gb"` - GPUs *int `json:"gpus"` -} - -// RebuildEnvironment requests that the given envID is rebuilt with no changes to its specification. -func (c *DefaultClient) RebuildEnvironment(ctx context.Context, envID string) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/environments/"+envID, UpdateEnvironmentReq{}, nil) -} - -// EditEnvironment modifies the environment specification and initiates a rebuild. -func (c *DefaultClient) EditEnvironment(ctx context.Context, envID string, req UpdateEnvironmentReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/environments/"+envID, req, nil) -} - -// DialWsep dials an environments command execution interface -// See https://github.com/cdr/wsep for details. -func (c *DefaultClient) DialWsep(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/wsep", withBaseURL(baseURL)) -} - -// DialExecutor gives a remote execution interface for performing commands inside an environment. -func (c *DefaultClient) DialExecutor(ctx context.Context, baseURL *url.URL, envID string) (wsep.Execer, error) { - ws, err := c.DialWsep(ctx, baseURL, envID) - if err != nil { - return nil, err - } - return wsep.RemoteExecer(ws), nil -} - -// DialIDEStatus opens a websocket connection for cpu load metrics on the environment. -func (c *DefaultClient) DialIDEStatus(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/proxy/environments/"+envID+"/ide/api/status", withBaseURL(baseURL)) -} - -// DialEnvironmentBuildLog opens a websocket connection for the environment build log messages. -func (c *DefaultClient) DialEnvironmentBuildLog(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/private/environments/"+envID+"/watch-update") -} - -// BuildLog defines a build log record for a Coder environment. -type BuildLog struct { - ID string `db:"id" json:"id"` - EnvironmentID string `db:"environment_id" json:"environment_id"` - // BuildID allows the frontend to separate the logs from the old build with the logs from the new. - BuildID string `db:"build_id" json:"build_id"` - Time time.Time `db:"time" json:"time"` - Type BuildLogType `db:"type" json:"type"` - Msg string `db:"msg" json:"msg"` -} - -// BuildLogFollowMsg wraps the base BuildLog and adds a field for collecting -// errors that may occur when follow or parsing. -type BuildLogFollowMsg struct { - BuildLog - Err error -} - -// FollowEnvironmentBuildLog trails the build log of a Coder environment. -func (c *DefaultClient) FollowEnvironmentBuildLog(ctx context.Context, envID string) (<-chan BuildLogFollowMsg, error) { - ch := make(chan BuildLogFollowMsg) - ws, err := c.DialEnvironmentBuildLog(ctx, envID) - if err != nil { - return nil, err - } - go func() { - defer ws.Close(websocket.StatusNormalClosure, "normal closure") - defer close(ch) - for { - var msg BuildLog - if err := wsjson.Read(ctx, ws, &msg); err != nil { - ch <- BuildLogFollowMsg{Err: err} - if xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded) { - return - } - continue - } - ch <- BuildLogFollowMsg{BuildLog: msg} - } - }() - return ch, nil -} - -// DialEnvironmentStats opens a websocket connection for environment stats. -func (c *DefaultClient) DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/private/environments/"+envID+"/watch-stats") -} - -// DialResourceLoad opens a websocket connection for cpu load metrics on the environment. -func (c *DefaultClient) DialResourceLoad(ctx context.Context, envID string) (*websocket.Conn, error) { - return c.dialWebsocket(ctx, "/api/private/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 *DefaultClient) WaitForEnvironmentReady(ctx context.Context, envID string) error { - conn, err := c.DialEnvironmentBuildLog(ctx, envID) - if err != nil { - return xerrors.Errorf("%s: dial build log: %w", envID, err) - } - - for { - msg := buildLogMsg{} - err := wsjson.Read(ctx, conn, &msg) - if err != nil { - return xerrors.Errorf("%s: reading build log msg: %w", envID, err) - } - - if msg.Type == BuildLogTypeDone { - return nil - } - } -} - -// EnvironmentByID get the details of an environment by its id. -func (c *DefaultClient) EnvironmentByID(ctx context.Context, id string) (*Environment, error) { - var env Environment - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/environments/"+id, nil, &env); err != nil { - return nil, err - } - return &env, nil -} - -// EnvironmentsByWorkspaceProvider returns all environments that belong to a particular workspace provider. -func (c *DefaultClient) EnvironmentsByWorkspaceProvider(ctx context.Context, wpID string) ([]Environment, error) { - var envs []Environment - if err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+wpID+"/environments", nil, &envs); err != nil { - return nil, err - } - return envs, nil -} diff --git a/coder-sdk/error.go b/coder-sdk/error.go deleted file mode 100644 index 2973046a..00000000 --- a/coder-sdk/error.go +++ /dev/null @@ -1,70 +0,0 @@ -package coder - -import ( - "encoding/json" - "fmt" - "net/http" - - "golang.org/x/xerrors" -) - -// ErrNotFound describes an error case in which the requested resource could not be found. -var ErrNotFound = xerrors.New("resource not found") - -// ErrPermissions describes an error case in which the requester has insufficient permissions to access the requested resource. -var ErrPermissions = xerrors.New("insufficient permissions") - -// ErrAuthentication describes the error case in which the requester has invalid authentication. -var ErrAuthentication = xerrors.New("invalid authentication") - -// APIError is the expected payload format for API errors. -type APIError struct { - Err APIErrorMsg `json:"error"` -} - -// APIErrorMsg contains the rich error information returned by API errors. -type APIErrorMsg struct { - Msg string `json:"msg"` - Code string `json:"code"` - Details json.RawMessage `json:"details"` -} - -// HTTPError represents an error from the Coder API. -type HTTPError struct { - *http.Response - cached *APIError - cachedErr error -} - -// Payload decode the response body into the standard error structure. The `details` -// section is stored as a raw json, and type depends on the `code` field. -func (e *HTTPError) Payload() (*APIError, error) { - var msg APIError - if e.cached != nil || e.cachedErr != nil { - return e.cached, e.cachedErr - } - - // Try to decode the payload as an error, if it fails or if there is no error message, - // return the response URL with the status. - if err := json.NewDecoder(e.Response.Body).Decode(&msg); err != nil { - e.cachedErr = err - return nil, err - } - - e.cached = &msg - return &msg, nil -} - -func (e *HTTPError) Error() string { - apiErr, err := e.Payload() - if err != nil || apiErr.Err.Msg == "" { - return fmt.Sprintf("%s: %d %s", e.Request.URL, e.StatusCode, e.Status) - } - - // If the payload was a in the expected error format with a message, include it. - return apiErr.Err.Msg -} - -func bodyError(resp *http.Response) error { - return &HTTPError{Response: resp} -} diff --git a/coder-sdk/image.go b/coder-sdk/image.go deleted file mode 100644 index c5101c87..00000000 --- a/coder-sdk/image.go +++ /dev/null @@ -1,101 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "net/url" - "time" -) - -// Image describes a Coder Image. -type Image struct { - ID string `json:"id" table:"-"` - OrganizationID string `json:"organization_id" table:"-"` - Repository string `json:"repository" table:"Repository"` - Description string `json:"description" table:"-"` - URL string `json:"url" table:"-"` // User-supplied URL for image. - Registry *Registry `json:"registry" table:"-"` - DefaultTag *ImageTag `json:"default_tag" table:"DefaultTag"` - DefaultCPUCores float32 `json:"default_cpu_cores" table:"DefaultCPUCores"` - DefaultMemoryGB float32 `json:"default_memory_gb" table:"DefaultMemoryGB"` - DefaultDiskGB int `json:"default_disk_gb" table:"DefaultDiskGB"` - Deprecated bool `json:"deprecated" table:"-"` - CreatedAt time.Time `json:"created_at" table:"-"` - UpdatedAt time.Time `json:"updated_at" table:"-"` -} - -// 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"` -} - -// ImportImageReq is used to import new images and registries into Coder. -type ImportImageReq 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". - OrgID string `json:"org_id"` - 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"` -} - -// UpdateImageReq defines the requests parameters for a partial update of an image resource. -type UpdateImageReq struct { - DefaultCPUCores *float32 `json:"default_cpu_cores"` - DefaultMemoryGB *float32 `json:"default_memory_gb"` - DefaultDiskGB *int `json:"default_disk_gb"` - Description *string `json:"description"` - URL *string `json:"url"` - Deprecated *bool `json:"deprecated"` - DefaultTag *string `json:"default_tag"` -} - -// ImportImage creates a new image and optionally a new registry. -func (c *DefaultClient) ImportImage(ctx context.Context, req ImportImageReq) (*Image, error) { - var img Image - if err := c.requestBody(ctx, http.MethodPost, "/api/v0/images", req, &img); err != nil { - return nil, err - } - return &img, nil -} - -// ImageByID returns an image entity, fetched by its ID. -func (c *DefaultClient) ImageByID(ctx context.Context, id string) (*Image, error) { - var img Image - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images/"+id, nil, &img); err != nil { - return nil, err - } - return &img, nil -} - -// OrganizationImages returns all of the images imported for orgID. -func (c *DefaultClient) OrganizationImages(ctx context.Context, orgID string) ([]Image, error) { - var ( - imgs []Image - query = url.Values{} - ) - - query.Set("org", orgID) - - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images", nil, &imgs, withQueryParams(query)); err != nil { - return nil, err - } - return imgs, nil -} - -// UpdateImage applies a partial update to an image resource. -func (c *DefaultClient) UpdateImage(ctx context.Context, imageID string, req UpdateImageReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/images/"+imageID, req, nil) -} - -// UpdateImageTags refreshes the latest digests for all tags of the image. -func (c *DefaultClient) UpdateImageTags(ctx context.Context, imageID string) error { - return c.requestBody(ctx, http.MethodPost, "/api/v0/images/"+imageID+"/tags/update", nil, nil) -} diff --git a/coder-sdk/interface.go b/coder-sdk/interface.go deleted file mode 100644 index 93043ee7..00000000 --- a/coder-sdk/interface.go +++ /dev/null @@ -1,235 +0,0 @@ -package coder - -import ( - "context" - "net/url" - - "cdr.dev/wsep" - "nhooyr.io/websocket" -) - -// Client wraps the Coder HTTP API. -// This is an interface to allow for mocking of coder-sdk client usage. -type Client interface { - // PushActivity pushes CLI activity to Coder. - PushActivity(ctx context.Context, source, envID string) error - - // Me gets the details of the authenticated user. - Me(ctx context.Context) (*User, error) - - // UserByID get the details of a user by their id. - UserByID(ctx context.Context, id string) (*User, error) - - // SSHKey gets the current SSH kepair of the authenticated user. - SSHKey(ctx context.Context) (*SSHKey, error) - - // Users gets the list of user accounts. - Users(ctx context.Context) ([]User, error) - - // UserByEmail gets a user by email. - UserByEmail(ctx context.Context, email string) (*User, error) - - // UpdateUser applyes the partial update to the given user. - UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error - - // UpdateUXState applies a partial update of the user's UX State. - UpdateUXState(ctx context.Context, userID string, uxsPartial map[string]interface{}) error - - // CreateUser creates a new user account. - CreateUser(ctx context.Context, req CreateUserReq) error - - // DeleteUser deletes a user account. - DeleteUser(ctx context.Context, userID string) error - - // SiteConfigAuth fetches the sitewide authentication configuration. - SiteConfigAuth(ctx context.Context) (*ConfigAuth, error) - - // PutSiteConfigAuth sets the sitewide authentication configuration. - PutSiteConfigAuth(ctx context.Context, req ConfigAuth) error - - // SiteConfigOAuth fetches the sitewide git provider OAuth configuration. - SiteConfigOAuth(ctx context.Context) (*ConfigOAuth, error) - - // PutSiteConfigOAuth sets the sitewide git provider OAuth configuration. - PutSiteConfigOAuth(ctx context.Context, req ConfigOAuth) error - - // SiteSetupModeEnabled fetches the current setup_mode state of a Coder deployment. - SiteSetupModeEnabled(ctx context.Context) (bool, error) - - // SiteConfigExtensionMarketplace fetches the extension marketplace configuration. - SiteConfigExtensionMarketplace(ctx context.Context) (*ConfigExtensionMarketplace, error) - - // PutSiteConfigExtensionMarketplace sets the extension marketplace configuration. - PutSiteConfigExtensionMarketplace(ctx context.Context, req ConfigExtensionMarketplace) error - - // DeleteDevURL deletes the specified devurl. - DeleteDevURL(ctx context.Context, envID, urlID string) error - - // CreateDevURL inserts a new devurl for the authenticated user. - CreateDevURL(ctx context.Context, envID string, req CreateDevURLReq) error - - // DevURLs fetches the Dev URLs for a given environment. - DevURLs(ctx context.Context, envID string) ([]DevURL, error) - - // PutDevURL updates an existing devurl for the authenticated user. - PutDevURL(ctx context.Context, envID, urlID string, req PutDevURLReq) error - - // CreateEnvironment sends a request to create an environment. - CreateEnvironment(ctx context.Context, req CreateEnvironmentRequest) (*Environment, error) - - // ParseTemplate parses a template config. It support both remote repositories and local files. - // If a local file is specified then all other values in the request are ignored. - ParseTemplate(ctx context.Context, req ParseTemplateRequest) (*TemplateVersion, error) - - // CreateEnvironmentFromRepo sends a request to create an environment from a repository. - CreateEnvironmentFromRepo(ctx context.Context, orgID string, req TemplateVersion) (*Environment, error) - - // Environments lists environments returned by the given filter. - Environments(ctx context.Context) ([]Environment, error) - - // UserEnvironmentsByOrganization gets the list of environments owned by the given user. - UserEnvironmentsByOrganization(ctx context.Context, userID, orgID string) ([]Environment, error) - - // DeleteEnvironment deletes the environment. - DeleteEnvironment(ctx context.Context, envID string) error - - // StopEnvironment stops the environment. - StopEnvironment(ctx context.Context, envID string) error - - // RebuildEnvironment requests that the given envID is rebuilt with no changes to its specification. - RebuildEnvironment(ctx context.Context, envID string) error - - // EditEnvironment modifies the environment specification and initiates a rebuild. - EditEnvironment(ctx context.Context, envID string, req UpdateEnvironmentReq) error - - // DialWsep dials an environments command execution interface - // See https://github.com/cdr/wsep for details. - DialWsep(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) - - // DialExecutor gives a remote execution interface for performing commands inside an environment. - DialExecutor(ctx context.Context, baseURL *url.URL, envID string) (wsep.Execer, error) - - // DialIDEStatus opens a websocket connection for cpu load metrics on the environment. - DialIDEStatus(ctx context.Context, baseURL *url.URL, envID string) (*websocket.Conn, error) - - // DialEnvironmentBuildLog opens a websocket connection for the environment build log messages. - DialEnvironmentBuildLog(ctx context.Context, envID string) (*websocket.Conn, error) - - // FollowEnvironmentBuildLog trails the build log of a Coder environment. - FollowEnvironmentBuildLog(ctx context.Context, envID string) (<-chan BuildLogFollowMsg, error) - - // DialEnvironmentStats opens a websocket connection for environment stats. - DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) - - // DialResourceLoad opens a websocket connection for cpu load metrics on the environment. - DialResourceLoad(ctx context.Context, envID string) (*websocket.Conn, error) - - // WaitForEnvironmentReady will watch the build log and return when done. - WaitForEnvironmentReady(ctx context.Context, envID string) error - - // EnvironmentByID get the details of an environment by its id. - EnvironmentByID(ctx context.Context, id string) (*Environment, error) - - // EnvironmentsByWorkspaceProvider returns environments that belong to a particular workspace provider. - EnvironmentsByWorkspaceProvider(ctx context.Context, wpID string) ([]Environment, error) - - // ImportImage creates a new image and optionally a new registry. - ImportImage(ctx context.Context, req ImportImageReq) (*Image, error) - - // ImageByID returns an image entity, fetched by its ID. - ImageByID(ctx context.Context, id string) (*Image, error) - - // OrganizationImages returns all of the images imported for orgID. - OrganizationImages(ctx context.Context, orgID string) ([]Image, error) - - // UpdateImage applies a partial update to an image resource. - UpdateImage(ctx context.Context, imageID string, req UpdateImageReq) error - - // UpdateImageTags refreshes the latest digests for all tags of the image. - UpdateImageTags(ctx context.Context, imageID string) error - - // Organizations gets all Organizations. - Organizations(ctx context.Context) ([]Organization, error) - - // OrganizationByID get the Organization by its ID. - OrganizationByID(ctx context.Context, orgID string) (*Organization, error) - - // OrganizationMembers get all members of the given organization. - OrganizationMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) - - // UpdateOrganization applys a partial update of an Organization resource. - UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationReq) error - - // CreateOrganization creates a new Organization in Coder. - CreateOrganization(ctx context.Context, req CreateOrganizationReq) error - - // DeleteOrganization deletes an organization. - DeleteOrganization(ctx context.Context, orgID string) error - - // Registries fetches all registries in an organization. - Registries(ctx context.Context, orgID string) ([]Registry, error) - - // RegistryByID fetches a registry resource by its ID. - RegistryByID(ctx context.Context, registryID string) (*Registry, error) - - // UpdateRegistry applies a partial update to a registry resource. - UpdateRegistry(ctx context.Context, registryID string, req UpdateRegistryReq) error - - // DeleteRegistry deletes a registry resource by its ID. - DeleteRegistry(ctx context.Context, registryID string) error - - // CreateImageTag creates a new image tag resource. - CreateImageTag(ctx context.Context, imageID string, req CreateImageTagReq) (*ImageTag, error) - - // DeleteImageTag deletes an image tag resource. - DeleteImageTag(ctx context.Context, imageID, tag string) error - - // ImageTags fetch all image tags. - ImageTags(ctx context.Context, imageID string) ([]ImageTag, error) - - // ImageTagByID fetch an image tag by ID. - ImageTagByID(ctx context.Context, imageID, tagID string) (*ImageTag, error) - - // CreateAPIToken creates a new APIToken for making authenticated requests to Coder. - CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (string, error) - - // APITokens fetches all APITokens owned by the given user. - APITokens(ctx context.Context, userID string) ([]APIToken, error) - - // APITokenByID fetches the metadata for a given APIToken. - APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) - - // DeleteAPIToken deletes an APIToken. - DeleteAPIToken(ctx context.Context, userID, tokenID string) error - - // RegenerateAPIToken regenerates the given APIToken and returns the new value. - RegenerateAPIToken(ctx context.Context, userID, tokenID string) (string, error) - - // APIVersion parses the coder-version http header from an authenticated request. - APIVersion(ctx context.Context) (string, error) - - // WorkspaceProviderByID fetches a workspace provider entity by its unique ID. - WorkspaceProviderByID(ctx context.Context, id string) (*KubernetesProvider, error) - - // WorkspaceProviders fetches all workspace providers known to the Coder control plane. - WorkspaceProviders(ctx context.Context) (*WorkspaceProviders, error) - - // CreateWorkspaceProvider creates a new WorkspaceProvider entity. - CreateWorkspaceProvider(ctx context.Context, req CreateWorkspaceProviderReq) (*CreateWorkspaceProviderRes, error) - - // DeleteWorkspaceProviderByID deletes a workspace provider entity from the Coder control plane. - DeleteWorkspaceProviderByID(ctx context.Context, id string) error - - // Token returns the API Token used to authenticate. - Token() string - - // BaseURL returns the BaseURL configured for this Client. - BaseURL() url.URL - - // CordonWorkspaceProvider prevents the provider from having any more workspaces placed on it. - CordonWorkspaceProvider(ctx context.Context, id, reason string) error - - // UnCordonWorkspaceProvider changes an existing cordoned providers status to 'Ready'; - // allowing it to continue creating new workspaces and provisioning resources for them. - UnCordonWorkspaceProvider(ctx context.Context, id string) error -} diff --git a/coder-sdk/login.go b/coder-sdk/login.go deleted file mode 100644 index 6899df12..00000000 --- a/coder-sdk/login.go +++ /dev/null @@ -1,69 +0,0 @@ -package coder - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - - "golang.org/x/xerrors" -) - -// LoginRequest is a request to authenticate using email -// and password credentials. -// -// This is provided for use in tests, and we recommend users authenticate -// using an API Token. -type LoginRequest struct { - Email string `json:"email"` - Password string `json:"password"` -} - -// LoginResponse contains successful response data for an authentication -// request, including an API Token to be used for subsequent requests. -// -// This is provided for use in tests, and we recommend users authenticate -// using an API Token. -type LoginResponse struct { - SessionToken string `json:"session_token"` -} - -// LoginWithPassword exchanges the email/password pair for -// a Session Token. -// -// If client is nil, the http.DefaultClient will be used. -func LoginWithPassword(ctx context.Context, client *http.Client, baseURL *url.URL, req *LoginRequest) (resp *LoginResponse, err error) { - if client == nil { - client = http.DefaultClient - } - - url := *baseURL - url.Path = fmt.Sprint(strings.TrimSuffix(url.Path, "/"), "/auth/basic/login") - - buf := &bytes.Buffer{} - err = json.NewEncoder(buf).Encode(req) - if err != nil { - return nil, xerrors.Errorf("failed to marshal JSON: %w", err) - } - - request, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), buf) - if err != nil { - return nil, xerrors.Errorf("failed to create request: %w", err) - } - - response, err := client.Do(request) - if err != nil { - return nil, xerrors.Errorf("error processing login request: %w", err) - } - defer response.Body.Close() - - err = json.NewDecoder(response.Body).Decode(&resp) - if err != nil { - return nil, xerrors.Errorf("failed to decode response: %w", err) - } - - return resp, nil -} diff --git a/coder-sdk/org.go b/coder-sdk/org.go deleted file mode 100644 index 92718dcd..00000000 --- a/coder-sdk/org.go +++ /dev/null @@ -1,101 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// Organization describes an Organization in Coder. -type Organization struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Default bool `json:"default"` - Members []OrganizationUser `json:"members"` - EnvironmentCount int `json:"environment_count"` - ResourceNamespace string `json:"resource_namespace"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - AutoOffThreshold Duration `json:"auto_off_threshold"` - CPUProvisioningRate float32 `json:"cpu_provisioning_rate"` - MemoryProvisioningRate float32 `json:"memory_provisioning_rate"` -} - -// OrganizationUser user wraps the basic User type and adds data specific to the user's membership of an organization. -type OrganizationUser struct { - User - OrganizationRoles []Role `json:"organization_roles"` - RolesUpdatedAt time.Time `json:"roles_updated_at"` -} - -// Organization Roles. -const ( - RoleOrgMember Role = "organization-member" - RoleOrgAdmin Role = "organization-admin" - RoleOrgManager Role = "organization-manager" -) - -// Organizations gets all Organizations. -func (c *DefaultClient) Organizations(ctx context.Context) ([]Organization, error) { - var orgs []Organization - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/orgs", nil, &orgs); err != nil { - return nil, err - } - return orgs, nil -} - -// OrganizationByID get the Organization by its ID. -func (c *DefaultClient) OrganizationByID(ctx context.Context, orgID string) (*Organization, error) { - var org Organization - err := c.requestBody(ctx, http.MethodGet, "/api/v0/orgs/"+orgID, nil, &org) - if err != nil { - return nil, err - } - return &org, nil -} - -// OrganizationMembers get all members of the given organization. -func (c *DefaultClient) OrganizationMembers(ctx context.Context, orgID string) ([]OrganizationUser, error) { - var members []OrganizationUser - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/orgs/"+orgID+"/members", nil, &members); err != nil { - return nil, err - } - return members, nil -} - -// UpdateOrganizationReq describes the patch request parameters to provide partial updates to an Organization resource. -type UpdateOrganizationReq struct { - Name *string `json:"name"` - Description *string `json:"description"` - Default *bool `json:"default"` - AutoOffThreshold *Duration `json:"auto_off_threshold"` - CPUProvisioningRate *float32 `json:"cpu_provisioning_rate"` - MemoryProvisioningRate *float32 `json:"memory_provisioning_rate"` -} - -// UpdateOrganization applys a partial update of an Organization resource. -func (c *DefaultClient) UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/orgs/"+orgID, req, nil) -} - -// CreateOrganizationReq describes the request parameters to create a new Organization. -type CreateOrganizationReq struct { - Name string `json:"name"` - Description string `json:"description"` - Default bool `json:"default"` - ResourceNamespace string `json:"resource_namespace"` - AutoOffThreshold Duration `json:"auto_off_threshold"` - CPUProvisioningRate float32 `json:"cpu_provisioning_rate"` - MemoryProvisioningRate float32 `json:"memory_provisioning_rate"` -} - -// CreateOrganization creates a new Organization in Coder. -func (c *DefaultClient) CreateOrganization(ctx context.Context, req CreateOrganizationReq) error { - return c.requestBody(ctx, http.MethodPost, "/api/v0/orgs", req, nil) -} - -// DeleteOrganization deletes an organization. -func (c *DefaultClient) DeleteOrganization(ctx context.Context, orgID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/orgs/"+orgID, nil, nil) -} diff --git a/coder-sdk/registries.go b/coder-sdk/registries.go deleted file mode 100644 index 074155b3..00000000 --- a/coder-sdk/registries.go +++ /dev/null @@ -1,60 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "net/url" - "time" -) - -// Registry defines an image registry configuration. -type Registry struct { - ID string `json:"id"` - OrganizationID string `json:"organization_id"` - FriendlyName string `json:"friendly_name"` - Registry string `json:"registry"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// Registries fetches all registries in an organization. -func (c *DefaultClient) Registries(ctx context.Context, orgID string) ([]Registry, error) { - var ( - r []Registry - query = url.Values{} - ) - - query.Set("org", orgID) - - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/registries", nil, &r, withQueryParams(query)); err != nil { - return nil, err - } - return r, nil -} - -// RegistryByID fetches a registry resource by its ID. -func (c *DefaultClient) RegistryByID(ctx context.Context, registryID string) (*Registry, error) { - var r Registry - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/registries/"+registryID, nil, &r); err != nil { - return nil, err - } - return &r, nil -} - -// UpdateRegistryReq defines the requests parameters for a partial update of a registry resource. -type UpdateRegistryReq struct { - Registry *string `json:"registry"` - FriendlyName *string `json:"friendly_name"` - Username *string `json:"username"` - Password *string `json:"password"` -} - -// UpdateRegistry applies a partial update to a registry resource. -func (c *DefaultClient) UpdateRegistry(ctx context.Context, registryID string, req UpdateRegistryReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/registries/"+registryID, req, nil) -} - -// DeleteRegistry deletes a registry resource by its ID. -func (c *DefaultClient) DeleteRegistry(ctx context.Context, registryID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/registries/"+registryID, nil, nil) -} diff --git a/coder-sdk/request.go b/coder-sdk/request.go deleted file mode 100644 index a49ddda1..00000000 --- a/coder-sdk/request.go +++ /dev/null @@ -1,127 +0,0 @@ -package coder - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "os" - "strings" - - "golang.org/x/xerrors" -) - -type requestOptions struct { - BaseURLOverride *url.URL - Query url.Values - Headers http.Header - Reader io.Reader -} - -type requestOption func(*requestOptions) - -// withQueryParams sets the provided query parameters on the request. -func withQueryParams(q url.Values) func(o *requestOptions) { - return func(o *requestOptions) { - o.Query = q - } -} - -func withHeaders(h http.Header) func(o *requestOptions) { - return func(o *requestOptions) { - o.Headers = h - } -} - -func withBaseURL(base *url.URL) func(o *requestOptions) { - return func(o *requestOptions) { - o.BaseURLOverride = base - } -} - -func withBody(w io.Reader) func(o *requestOptions) { - return func(o *requestOptions) { - o.Reader = w - } -} - -// request is a helper to set the cookie, marshal the payload and execute the request. -func (c *DefaultClient) request(ctx context.Context, method, path string, in interface{}, options ...requestOption) (*http.Response, error) { - url := *c.baseURL - - var config requestOptions - for _, o := range options { - o(&config) - } - if config.BaseURLOverride != nil { - url = *config.BaseURLOverride - } - if config.Query != nil { - url.RawQuery = config.Query.Encode() - } - url.Path = fmt.Sprint(strings.TrimSuffix(url.Path, "/"), path) - - // 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) - } - - if config.Reader != nil { - payload = config.Reader - } - - // Create the http request. - req, err := http.NewRequestWithContext(ctx, method, url.String(), payload) - if err != nil { - return nil, xerrors.Errorf("create request: %w", err) - } - - if config.Headers == nil { - req.Header = http.Header{} - } else { - req.Header = config.Headers - } - - // Provide the session token in a header - req.Header.Set("Session-Token", c.token) - - customAuthHeader, ok := os.LookupEnv("ENDPOINT_AUTH_HEADER") - if ok { - req.Header.Set("Authorization", customAuthHeader) - } - - // Execute the request. - return c.httpClient.Do(req) -} - -// requestBody is a helper extending the Client.request helper, checking the response code -// and decoding the response payload. -func (c *DefaultClient) requestBody(ctx context.Context, method, path string, in, out interface{}, opts ...requestOption) error { - resp, err := c.request(ctx, method, path, in, opts...) - 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 %d: %w", resp.StatusCode, 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/tags.go b/coder-sdk/tags.go deleted file mode 100644 index 7e4563a9..00000000 --- a/coder-sdk/tags.go +++ /dev/null @@ -1,72 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// ImageTag is a Docker image tag. -type ImageTag struct { - ImageID string `json:"image_id" table:"-"` - Tag string `json:"tag" table:"Tag"` - LatestHash string `json:"latest_hash" table:"-"` - HashLastUpdatedAt time.Time `json:"hash_last_updated_at" table:"-"` - OSRelease *OSRelease `json:"os_release" table:"OS"` - Environments []*Environment `json:"environments" table:"-"` - UpdatedAt time.Time `json:"updated_at" table:"UpdatedAt"` - CreatedAt time.Time `json:"created_at" table:"-"` -} - -func (i ImageTag) String() string { - return i.Tag -} - -// OSRelease is the marshalled /etc/os-release file. -type OSRelease struct { - ID string `json:"id"` - PrettyName string `json:"pretty_name"` - HomeURL string `json:"home_url"` -} - -func (o OSRelease) String() string { - return o.PrettyName -} - -// CreateImageTagReq defines the request parameters for creating a new image tag. -type CreateImageTagReq struct { - Tag string `json:"tag"` - Default bool `json:"default"` -} - -// CreateImageTag creates a new image tag resource. -func (c *DefaultClient) CreateImageTag(ctx context.Context, imageID string, req CreateImageTagReq) (*ImageTag, error) { - var tag ImageTag - if err := c.requestBody(ctx, http.MethodPost, "/api/v0/images/"+imageID+"/tags", req, tag); err != nil { - return nil, err - } - return &tag, nil -} - -// DeleteImageTag deletes an image tag resource. -func (c *DefaultClient) DeleteImageTag(ctx context.Context, imageID, tag string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/images/"+imageID+"/tags/"+tag, nil, nil) -} - -// ImageTags fetch all image tags. -func (c *DefaultClient) ImageTags(ctx context.Context, imageID string) ([]ImageTag, error) { - var tags []ImageTag - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images/"+imageID+"/tags", nil, &tags); err != nil { - return nil, err - } - return tags, nil -} - -// ImageTagByID fetch an image tag by ID. -func (c *DefaultClient) ImageTagByID(ctx context.Context, imageID, tagID string) (*ImageTag, error) { - var tag ImageTag - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/images/"+imageID+"/tags/"+tagID, nil, &tag); err != nil { - return nil, err - } - return &tag, nil -} diff --git a/coder-sdk/tokens.go b/coder-sdk/tokens.go deleted file mode 100644 index dc12a173..00000000 --- a/coder-sdk/tokens.go +++ /dev/null @@ -1,67 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// APIToken describes a Coder APIToken resource for use in API requests. -type APIToken struct { - ID string `json:"id"` - Name string `json:"name"` - Application bool `json:"application"` - UserID string `json:"user_id"` - LastUsed time.Time `json:"last_used"` -} - -// CreateAPITokenReq defines the paramemters for creating a new APIToken. -type CreateAPITokenReq struct { - Name string `json:"name"` -} - -type createAPITokenResp struct { - Key string `json:"key"` -} - -// CreateAPIToken creates a new APIToken for making authenticated requests to Coder. -func (c *DefaultClient) CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (token string, _ error) { - var resp createAPITokenResp - err := c.requestBody(ctx, http.MethodPost, "/api/v0/api-keys/"+userID, req, &resp) - if err != nil { - return "", err - } - return resp.Key, nil -} - -// APITokens fetches all APITokens owned by the given user. -func (c *DefaultClient) APITokens(ctx context.Context, userID string) ([]APIToken, error) { - var tokens []APIToken - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/api-keys/"+userID, nil, &tokens); err != nil { - return nil, err - } - return tokens, nil -} - -// APITokenByID fetches the metadata for a given APIToken. -func (c *DefaultClient) APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) { - var token APIToken - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/api-keys/"+userID+"/"+tokenID, nil, &token); err != nil { - return nil, err - } - return &token, nil -} - -// DeleteAPIToken deletes an APIToken. -func (c *DefaultClient) DeleteAPIToken(ctx context.Context, userID, tokenID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/api-keys/"+userID+"/"+tokenID, nil, nil) -} - -// RegenerateAPIToken regenerates the given APIToken and returns the new value. -func (c *DefaultClient) RegenerateAPIToken(ctx context.Context, userID, tokenID string) (token string, _ error) { - var resp createAPITokenResp - if err := c.requestBody(ctx, http.MethodPost, "/api/v0/api-keys/"+userID+"/"+tokenID+"/regen", nil, &resp); err != nil { - return "", err - } - return resp.Key, nil -} diff --git a/coder-sdk/users.go b/coder-sdk/users.go deleted file mode 100644 index 8b02f5f1..00000000 --- a/coder-sdk/users.go +++ /dev/null @@ -1,165 +0,0 @@ -package coder - -import ( - "context" - "net/http" - "time" -) - -// User describes a Coder user account. -type User struct { - ID string `json:"id" table:"-"` - Email string `json:"email" table:"Email"` - Username string `json:"username" table:"Username"` - Name string `json:"name" table:"Name"` - Roles []Role `json:"roles" table:"-"` - TemporaryPassword bool `json:"temporary_password" table:"-"` - LoginType string `json:"login_type" table:"-"` - KeyRegeneratedAt time.Time `json:"key_regenerated_at" table:"-"` - CreatedAt time.Time `json:"created_at" table:"CreatedAt"` - UpdatedAt time.Time `json:"updated_at" table:"-"` -} - -// Role defines a Coder permissions role group. -type Role string - -// Site Roles. -const ( - SiteAdmin Role = "site-admin" - SiteAuditor Role = "site-auditor" - SiteManager Role = "site-manager" - SiteMember Role = "site-member" -) - -// LoginType defines the enum of valid user login types. -type LoginType string - -// LoginType enum options. -const ( - LoginTypeBuiltIn LoginType = "built-in" - LoginTypeSAML LoginType = "saml" - LoginTypeOIDC LoginType = "oidc" -) - -// Me gets the details of the authenticated user. -func (c *DefaultClient) Me(ctx context.Context) (*User, error) { - return c.UserByID(ctx, Me) -} - -// UserByID get the details of a user by their id. -func (c *DefaultClient) UserByID(ctx context.Context, id string) (*User, error) { - var u User - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/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 *DefaultClient) SSHKey(ctx context.Context) (*SSHKey, error) { - var key SSHKey - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/users/me/sshkey", nil, &key); err != nil { - return nil, err - } - return &key, nil -} - -// Users gets the list of user accounts. -func (c *DefaultClient) Users(ctx context.Context) ([]User, error) { - var u []User - if err := c.requestBody(ctx, http.MethodGet, "/api/v0/users", nil, &u); err != nil { - return nil, err - } - return u, nil -} - -// UserByEmail gets a user by email. -func (c *DefaultClient) 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 -} - -// UpdateUserReq defines a modification to the user, updating the -// value of all non-nil values. -type UpdateUserReq struct { - *UserPasswordSettings - Revoked *bool `json:"revoked,omitempty"` - Roles *[]Role `json:"roles,omitempty"` - LoginType *LoginType `json:"login_type,omitempty"` - Name *string `json:"name,omitempty"` - Username *string `json:"username,omitempty"` - Email *string `json:"email,omitempty"` - DotfilesGitURL *string `json:"dotfiles_git_uri,omitempty"` -} - -// UserPasswordSettings allows modification of the user's password -// settings. -// -// These settings are only applicable to users managed using the -// built-in authentication provider; users authenticating using -// OAuth must change their password through the identity provider -// instead. -type UserPasswordSettings struct { - // OldPassword is the account's current password. - OldPassword string `json:"old_password,omitempty"` - - // Password is the new password, which may be a temporary password. - Password string `json:"password,omitempty"` - - // Temporary indicates that API access should be restricted to the - // password change API and a few other APIs. If set to true, Coder - // will prompt the user to change their password upon their next - // login through the web interface. - Temporary bool `json:"temporary_password,omitempty"` -} - -// UpdateUser applyes the partial update to the given user. -func (c *DefaultClient) UpdateUser(ctx context.Context, userID string, req UpdateUserReq) error { - return c.requestBody(ctx, http.MethodPatch, "/api/v0/users/"+userID, req, nil) -} - -// UpdateUXState applies a partial update of the user's UX State. -func (c *DefaultClient) UpdateUXState(ctx context.Context, userID string, uxsPartial map[string]interface{}) error { - if err := c.requestBody(ctx, http.MethodPut, "/api/private/users/"+userID+"/ux-state", uxsPartial, nil); err != nil { - return err - } - return nil -} - -// CreateUserReq defines the request parameters for creating a new user resource. -type CreateUserReq struct { - Name string `json:"name"` - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` - TemporaryPassword bool `json:"temporary_password"` - LoginType LoginType `json:"login_type"` - OrganizationsIDs []string `json:"organizations"` -} - -// CreateUser creates a new user account. -func (c *DefaultClient) CreateUser(ctx context.Context, req CreateUserReq) error { - return c.requestBody(ctx, http.MethodPost, "/api/v0/users", req, nil) -} - -// DeleteUser deletes a user account. -func (c *DefaultClient) DeleteUser(ctx context.Context, userID string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/v0/users/"+userID, nil, nil) -} diff --git a/coder-sdk/users_test.go b/coder-sdk/users_test.go deleted file mode 100644 index 31240880..00000000 --- a/coder-sdk/users_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package coder_test - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func TestUsers(t *testing.T) { - t.Parallel() - - const username = "root" - const name = "Charlie Root" - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Users is a GET", http.MethodGet, r.Method) - assert.Equal(t, "Path matches", "/api/v0/users", r.URL.Path) - - users := []map[string]interface{}{ - { - "id": "default", - "email": "root@user.com", - "username": username, - "name": name, - "roles": []coder.Role{coder.SiteAdmin}, - "temporary_password": false, - "login_type": coder.LoginTypeBuiltIn, - "key_regenerated_at": time.Now(), - "created_at": time.Now(), - "updated_at": time.Now(), - }, - } - - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(users) - assert.Success(t, "error encoding JSON", err) - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: "JcmErkJjju-KSrztst0IJX7xGJhKQPtfv", - }) - assert.Success(t, "failed to create coder.Client", err) - - users, err := client.Users(context.Background()) - assert.Success(t, "error getting Users", err) - assert.True(t, "users should return a single user", len(users) == 1) - assert.Equal(t, "expected user's name to match", name, users[0].Name) - assert.Equal(t, "expected user's username to match", username, users[0].Username) -} - -func TestUserUpdatePassword(t *testing.T) { - t.Parallel() - - const oldPassword = "vt9g9rxsptrq" - const newPassword = "wmf39jw2f7pk" - - server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Users is a PATCH", http.MethodPatch, r.Method) - assert.Equal(t, "Path matches", "/api/v0/users/me", r.URL.Path) - - expected := map[string]interface{}{ - "old_password": oldPassword, - "password": newPassword, - } - var request map[string]interface{} - err := json.NewDecoder(r.Body).Decode(&request) - assert.Success(t, "error decoding JSON", err) - assert.Equal(t, "unexpected request data", expected, request) - - w.WriteHeader(http.StatusOK) - })) - t.Cleanup(func() { - server.Close() - }) - - u, err := url.Parse(server.URL) - assert.Success(t, "failed to parse test server URL", err) - - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - HTTPClient: server.Client(), - Token: "JcmErkJjju-KSrztst0IJX7xGJhKQPtfv", - }) - assert.Success(t, "failed to create coder.Client", err) - - err = client.UpdateUser(context.Background(), "me", coder.UpdateUserReq{ - UserPasswordSettings: &coder.UserPasswordSettings{ - OldPassword: oldPassword, - Password: newPassword, - Temporary: false, - }, - }) - assert.Success(t, "error when updating password", err) -} diff --git a/coder-sdk/util.go b/coder-sdk/util.go deleted file mode 100644 index 0abba3c9..00000000 --- a/coder-sdk/util.go +++ /dev/null @@ -1,36 +0,0 @@ -package coder - -import ( - "encoding/json" - "strconv" - "time" -) - -// String gives a string pointer. -func String(s string) *string { - return &s -} - -// Duration is a time.Duration wrapper that marshals to millisecond precision. -// While it looses precision, most javascript applications expect durations to be in milliseconds. -type Duration time.Duration - -// MarshalJSON marshals the duration to millisecond precision. -func (d Duration) MarshalJSON() ([]byte, error) { - du := time.Duration(d) - return json.Marshal(du.Milliseconds()) -} - -// UnmarshalJSON unmarshals a millisecond-precision integer to -// a time.Duration. -func (d *Duration) UnmarshalJSON(b []byte) error { - i, err := strconv.ParseInt(string(b), 10, 64) - if err != nil { - return err - } - - *d = Duration(time.Duration(i) * time.Millisecond) - return nil -} - -func (d Duration) String() string { return time.Duration(d).String() } diff --git a/coder-sdk/version.go b/coder-sdk/version.go deleted file mode 100644 index dd1ae9ec..00000000 --- a/coder-sdk/version.go +++ /dev/null @@ -1,23 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -// APIVersion parses the coder-version http header from an authenticated request. -func (c *DefaultClient) APIVersion(ctx context.Context) (string, error) { - const coderVersionHeaderKey = "coder-version" - resp, err := c.request(ctx, http.MethodGet, "/api", nil) - if err != nil { - return "", err - } - defer resp.Body.Close() - - version := resp.Header.Get(coderVersionHeaderKey) - if version == "" { - version = "unknown" - } - - return version, nil -} diff --git a/coder-sdk/workspace_providers.go b/coder-sdk/workspace_providers.go deleted file mode 100644 index 673dc63e..00000000 --- a/coder-sdk/workspace_providers.go +++ /dev/null @@ -1,125 +0,0 @@ -package coder - -import ( - "context" - "net/http" -) - -// WorkspaceProviders defines all available Coder workspace provider targets. -type WorkspaceProviders struct { - Kubernetes []KubernetesProvider `json:"kubernetes"` -} - -// KubernetesProvider defines an entity capable of deploying and acting as an ingress for Coder environments. -type KubernetesProvider struct { - ID string `json:"id" table:"-"` - Name string `json:"name" table:"Name"` - Status WorkspaceProviderStatus `json:"status" table:"Status"` - BuiltIn bool `json:"built_in" table:"-"` - EnvproxyAccessURL string `json:"envproxy_access_url" table:"Access URL" validate:"required"` - DevurlHost string `json:"devurl_host" table:"Devurl Host"` - OrgWhitelist []string `json:"org_whitelist" table:"-"` - KubeProviderConfig `json:"config" table:"_"` -} - -// KubeProviderConfig defines Kubernetes-specific configuration options. -type KubeProviderConfig struct { - ClusterAddress string `json:"cluster_address" table:"Cluster Address"` - DefaultNamespace string `json:"default_namespace" table:"Namespace"` - StorageClass string `json:"storage_class" table:"Storage Class"` - ClusterDomainSuffix string `json:"cluster_domain_suffix" table:"Cluster Domain Suffix"` - SSHEnabled bool `json:"ssh_enabled" table:"SSH Enabled"` -} - -// WorkspaceProviderStatus represents the configuration state of a workspace provider. -type WorkspaceProviderStatus string - -// Workspace Provider statuses. -const ( - WorkspaceProviderPending WorkspaceProviderStatus = "pending" - WorkspaceProviderReady WorkspaceProviderStatus = "ready" -) - -// WorkspaceProviderType represents the type of workspace provider. -type WorkspaceProviderType string - -// Workspace Provider types. -const ( - WorkspaceProviderKubernetes WorkspaceProviderType = "kubernetes" -) - -// WorkspaceProviderByID fetches a workspace provider entity by its unique ID. -func (c *DefaultClient) WorkspaceProviderByID(ctx context.Context, id string) (*KubernetesProvider, error) { - var wp KubernetesProvider - err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools/"+id, nil, &wp) - if err != nil { - return nil, err - } - return &wp, nil -} - -// WorkspaceProviders fetches all workspace providers known to the Coder control plane. -func (c *DefaultClient) WorkspaceProviders(ctx context.Context) (*WorkspaceProviders, error) { - var providers WorkspaceProviders - err := c.requestBody(ctx, http.MethodGet, "/api/private/resource-pools", nil, &providers) - if err != nil { - return nil, err - } - return &providers, nil -} - -// CreateWorkspaceProviderReq defines the request parameters for creating a new workspace provider entity. -type CreateWorkspaceProviderReq struct { - Name string `json:"name"` - Type WorkspaceProviderType `json:"type"` - Hostname string `json:"hostname"` - ClusterAddress string `json:"cluster_address"` -} - -// CreateWorkspaceProviderRes defines the response from creating a new workspace provider entity. -type CreateWorkspaceProviderRes struct { - ID string `json:"id" table:"ID"` - Name string `json:"name" table:"Name"` - Status WorkspaceProviderStatus `json:"status" table:"Status"` - EnvproxyToken string `json:"envproxy_token" table:"Envproxy Token"` -} - -// CreateWorkspaceProvider creates a new WorkspaceProvider entity. -func (c *DefaultClient) CreateWorkspaceProvider(ctx context.Context, req CreateWorkspaceProviderReq) (*CreateWorkspaceProviderRes, error) { - var res CreateWorkspaceProviderRes - err := c.requestBody(ctx, http.MethodPost, "/api/private/resource-pools", req, &res) - if err != nil { - return nil, err - } - return &res, nil -} - -// DeleteWorkspaceProviderByID deletes a workspace provider entity from the Coder control plane. -func (c *DefaultClient) DeleteWorkspaceProviderByID(ctx context.Context, id string) error { - return c.requestBody(ctx, http.MethodDelete, "/api/private/resource-pools/"+id, nil, nil) -} - -// CordoneWorkspaceProviderReq defines the request parameters for creating a new workspace provider entity. -type CordoneWorkspaceProviderReq struct { - Reason string `json:"reason"` -} - -// CordonWorkspaceProvider prevents the provider from having any more workspaces placed on it. -func (c *DefaultClient) CordonWorkspaceProvider(ctx context.Context, id, reason string) error { - req := CordoneWorkspaceProviderReq{Reason: reason} - err := c.requestBody(ctx, http.MethodPost, "/api/private/resource-pools/"+id+"/cordon", req, nil) - if err != nil { - return err - } - return nil -} - -// UnCordonWorkspaceProvider changes an existing cordoned providers status to 'Ready'; -// allowing it to continue creating new workspaces and provisioning resources for them. -func (c *DefaultClient) UnCordonWorkspaceProvider(ctx context.Context, id string) error { - err := c.requestBody(ctx, http.MethodPost, "/api/private/resource-pools/"+id+"/uncordon", nil, nil) - if err != nil { - return err - } - return nil -} diff --git a/coder-sdk/ws.go b/coder-sdk/ws.go deleted file mode 100644 index 6f6a920f..00000000 --- a/coder-sdk/ws.go +++ /dev/null @@ -1,35 +0,0 @@ -package coder - -import ( - "context" - "net/http" - - "nhooyr.io/websocket" -) - -// dialWebsocket establish the websocket connection while setting the authentication header. -func (c *DefaultClient) dialWebsocket(ctx context.Context, path string, options ...requestOption) (*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 - var config requestOptions - for _, o := range options { - o(&config) - } - if config.BaseURLOverride != nil { - url = *config.BaseURLOverride - } - url.Path = path - - headers := http.Header{} - headers.Set("Session-Token", c.token) - - conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{HTTPHeader: headers}) - 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 bd37b0bd..00000000 --- a/docs/coder.md +++ /dev/null @@ -1,25 +0,0 @@ -## coder - -coder provides a CLI for working with an existing Coder installation - -### Options - -``` - -h, --help help for coder - -v, --verbose show verbose output -``` - -### 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 images](coder_images.md) - Manage Coder images -* [coder login](coder_login.md) - Authenticate this client for future operations -* [coder logout](coder_logout.md) - Remove local authentication credentials if any exist -* [coder ssh](coder_ssh.md) - Enter a shell of execute a command over SSH into a Coder environment -* [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder environment -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user -* [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 7478c155..00000000 --- a/docs/coder_completion.md +++ /dev/null @@ -1,70 +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 -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_config-ssh.md b/docs/coder_config-ssh.md deleted file mode 100644 index 8ac849cf..00000000 --- a/docs/coder_config-ssh.md +++ /dev/null @@ -1,30 +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 override the default path of your ssh config file (default "~/.ssh/config") - -h, --help help for config-ssh - --remove remove the auto-generated Coder ssh config -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_envs.md b/docs/coder_envs.md deleted file mode 100644 index 623c5e83..00000000 --- a/docs/coder_envs.md +++ /dev/null @@ -1,32 +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 -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder envs create](coder_envs_create.md) - create a new environment. -* [coder envs create-from-config](coder_envs_create-from-config.md) - create a new environment from a template -* [coder envs edit](coder_envs_edit.md) - edit an existing environment and initiate a rebuild. -* [coder envs ls](coder_envs_ls.md) - list all environments owned by the active user -* [coder envs rebuild](coder_envs_rebuild.md) - rebuild a Coder environment -* [coder envs rm](coder_envs_rm.md) - remove Coder environments by name -* [coder envs stop](coder_envs_stop.md) - stop Coder environments by name -* [coder envs watch-build](coder_envs_watch-build.md) - trail the build log of a Coder environment - diff --git a/docs/coder_envs_create-from-config.md b/docs/coder_envs_create-from-config.md deleted file mode 100644 index e90e7bfe..00000000 --- a/docs/coder_envs_create-from-config.md +++ /dev/null @@ -1,43 +0,0 @@ -## coder envs create-from-config - -create a new environment from a template - -### Synopsis - -Create a new Coder environment using a Workspaces As Code template. - -``` -coder envs create-from-config [flags] -``` - -### Examples - -``` -# create a new environment from git repository -coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch -coder envs create-from-config --name="dev-env" -f coder.yaml -``` - -### Options - -``` - -f, --filepath string path to local template file. - --follow follow buildlog after initiating rebuild - -h, --help help for create-from-config - --name string name of the environment to be created - -o, --org string name of the organization the environment should be created under. - --provider string name of Workspace Provider with which to create the environment - --ref string git reference to pull template from. May be a branch, tag, or commit hash. (default "master") - -r, --repo-url string URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'. -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_create.md b/docs/coder_envs_create.md deleted file mode 100644 index bc1f6517..00000000 --- a/docs/coder_envs_create.md +++ /dev/null @@ -1,47 +0,0 @@ -## coder envs create - -create a new environment. - -### Synopsis - -Create a new Coder environment. - -``` -coder envs create [environment_name] [flags] -``` - -### Examples - -``` -# create a new environment using default resource amounts -coder envs create my-new-env --image ubuntu -coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ubuntu -``` - -### Options - -``` - --container-based-vm deploy the environment as a Container-based VM - -c, --cpu float32 number of cpu cores the environment should be provisioned with. - -d, --disk int GB of disk storage an environment should be provisioned with. - --enable-autostart automatically start this environment at your preferred time. - --follow follow buildlog after initiating rebuild - -g, --gpus int number GPUs an environment should be provisioned with. - -h, --help help for create - -i, --image string name of the image to base the environment off of. - -m, --memory float32 GB of RAM an environment should be provisioned with. - -o, --org string name of the organization the environment should be created under. - --provider string name of Workspace Provider with which to create the environment - -t, --tag string tag of the image the environment will be based off of. (default "latest") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_edit.md b/docs/coder_envs_edit.md deleted file mode 100644 index a8cc2551..00000000 --- a/docs/coder_envs_edit.md +++ /dev/null @@ -1,46 +0,0 @@ -## coder envs edit - -edit an existing environment and initiate a rebuild. - -### Synopsis - -Edit an existing environment and initate a rebuild. - -``` -coder envs edit [flags] -``` - -### Examples - -``` -coder envs edit back-end-env --cpu 4 - -coder envs edit back-end-env --disk 20 -``` - -### Options - -``` - -c, --cpu float32 The number of cpu cores the environment should be provisioned with. - -d, --disk int The amount of disk storage an environment should be provisioned with. - --follow follow buildlog after initiating rebuild - --force force rebuild without showing a confirmation prompt - -g, --gpu int The amount of disk storage to provision the environment with. - -h, --help help for edit - -i, --image string name of the image you want the environment to be based off of. - -m, --memory float32 The amount of RAM an environment should be provisioned with. - -o, --org string name of the organization the environment should be created under. - -t, --tag string image tag of the image you want to base the environment off of. (default "latest") - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_ls.md b/docs/coder_envs_ls.md deleted file mode 100644 index e43aec24..00000000 --- a/docs/coder_envs_ls.md +++ /dev/null @@ -1,31 +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") - -p, --provider string Filter environments by a particular workspace provider name. - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_rebuild.md b/docs/coder_envs_rebuild.md deleted file mode 100644 index a18d279e..00000000 --- a/docs/coder_envs_rebuild.md +++ /dev/null @@ -1,34 +0,0 @@ -## coder envs rebuild - -rebuild a Coder environment - -``` -coder envs rebuild [environment_name] [flags] -``` - -### Examples - -``` -coder envs rebuild front-end-env --follow -coder envs rebuild backend-env --force -``` - -### Options - -``` - --follow follow build log after initiating rebuild - --force force rebuild without showing a confirmation prompt - -h, --help help for rebuild - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_rm.md b/docs/coder_envs_rm.md deleted file mode 100644 index e39acffd..00000000 --- a/docs/coder_envs_rm.md +++ /dev/null @@ -1,26 +0,0 @@ -## coder envs rm - -remove Coder environments by name - -``` -coder envs rm [...environment_names] [flags] -``` - -### Options - -``` - -f, --force force remove the specified environments without prompting first - -h, --help help for rm - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### 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 4fd38db2..00000000 --- a/docs/coder_envs_stop.md +++ /dev/null @@ -1,44 +0,0 @@ -## coder envs stop - -stop Coder environments by name - -### Synopsis - -Stop Coder environments by name - -``` -coder envs stop [...environment_names] [flags] -``` - -### Examples - -``` -coder envs stop front-end-env -coder envs stop front-end-env backend-env - -# stop all of your environments -coder envs ls -o json | jq -c '.[].name' | xargs coder envs stop - -# stop all environments for a given user -coder envs --user charlie@coder.com ls -o json \ - | jq -c '.[].name' \ - | xargs coder envs --user charlie@coder.com stop -``` - -### Options - -``` - -h, --help help for stop - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_envs_watch-build.md b/docs/coder_envs_watch-build.md deleted file mode 100644 index 19901fc5..00000000 --- a/docs/coder_envs_watch-build.md +++ /dev/null @@ -1,31 +0,0 @@ -## coder envs watch-build - -trail the build log of a Coder environment - -``` -coder envs watch-build [environment_name] [flags] -``` - -### Examples - -``` -coder envs watch-build front-end-env -``` - -### Options - -``` - -h, --help help for watch-build - --user string Specify the user whose resources to target (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder envs](coder_envs.md) - Interact with Coder environments - diff --git a/docs/coder_images.md b/docs/coder_images.md deleted file mode 100644 index 78c875ed..00000000 --- a/docs/coder_images.md +++ /dev/null @@ -1,26 +0,0 @@ -## coder images - -Manage Coder images - -### Synopsis - -Manage existing images and/or import new ones. - -### Options - -``` - -h, --help help for images - --user string Specifies the user by email (default "me") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder images ls](coder_images_ls.md) - list all images available to the active user - diff --git a/docs/coder_images_ls.md b/docs/coder_images_ls.md deleted file mode 100644 index bfb646d5..00000000 --- a/docs/coder_images_ls.md +++ /dev/null @@ -1,31 +0,0 @@ -## coder images ls - -list all images available to the active user - -### Synopsis - -List all Coder images available to the active user. - -``` -coder images ls [flags] -``` - -### Options - -``` - -h, --help help for ls - --org string organization name - --output string human | json (default "human") -``` - -### Options inherited from parent commands - -``` - --user string Specifies the user by email (default "me") - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder images](coder_images.md) - Manage Coder images - diff --git a/docs/coder_login.md b/docs/coder_login.md deleted file mode 100644 index ff20bf7e..00000000 --- a/docs/coder_login.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder login - -Authenticate this client for future operations - -``` -coder login [Coder URL eg. https://my.coder.domain/] [flags] -``` - -### Options - -``` - -h, --help help for login -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_logout.md b/docs/coder_logout.md deleted file mode 100644 index cfb1f4c4..00000000 --- a/docs/coder_logout.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder logout - -Remove local authentication credentials if any exist - -``` -coder logout [flags] -``` - -### Options - -``` - -h, --help help for logout -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_ssh.md b/docs/coder_ssh.md deleted file mode 100644 index 656b66fd..00000000 --- a/docs/coder_ssh.md +++ /dev/null @@ -1,31 +0,0 @@ -## coder ssh - -Enter a shell of execute a command over SSH into a Coder environment - -``` -coder ssh [environment_name] [] -``` - -### Examples - -``` -coder ssh my-dev -coder ssh my-dev pwd -``` - -### Options - -``` - -h, --help help for ssh -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_sync.md b/docs/coder_sync.md deleted file mode 100644 index b872fab4..00000000 --- a/docs/coder_sync.md +++ /dev/null @@ -1,25 +0,0 @@ -## coder sync - -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 -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_tokens.md b/docs/coder_tokens.md deleted file mode 100644 index 62bf7511..00000000 --- a/docs/coder_tokens.md +++ /dev/null @@ -1,29 +0,0 @@ -## coder tokens - -manage Coder API tokens for the active user - -### Synopsis - -Create and manage API Tokens for authenticating the CLI. -Statically authenticate using the token value with the `CODER_TOKEN` and `CODER_URL` environment variables. - -### Options - -``` - -h, --help help for tokens -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation -* [coder tokens create](coder_tokens_create.md) - create generates a new API token and prints it to stdout -* [coder tokens ls](coder_tokens_ls.md) - show the user's active API tokens -* [coder tokens regen](coder_tokens_regen.md) - regenerate an API token by its unique ID and print the new token to stdout -* [coder tokens rm](coder_tokens_rm.md) - remove an API token by its unique ID - diff --git a/docs/coder_tokens_create.md b/docs/coder_tokens_create.md deleted file mode 100644 index a7a89f54..00000000 --- a/docs/coder_tokens_create.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder tokens create - -create generates a new API token and prints it to stdout - -``` -coder tokens create [token_name] [flags] -``` - -### Options - -``` - -h, --help help for create -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user - diff --git a/docs/coder_tokens_ls.md b/docs/coder_tokens_ls.md deleted file mode 100644 index 6790700d..00000000 --- a/docs/coder_tokens_ls.md +++ /dev/null @@ -1,25 +0,0 @@ -## coder tokens ls - -show the user's active API tokens - -``` -coder tokens ls [flags] -``` - -### Options - -``` - -h, --help help for ls - -o, --output string human | json (default "human") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user - diff --git a/docs/coder_tokens_regen.md b/docs/coder_tokens_regen.md deleted file mode 100644 index 26832102..00000000 --- a/docs/coder_tokens_regen.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder tokens regen - -regenerate an API token by its unique ID and print the new token to stdout - -``` -coder tokens regen [token_id] [flags] -``` - -### Options - -``` - -h, --help help for regen -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user - diff --git a/docs/coder_tokens_rm.md b/docs/coder_tokens_rm.md deleted file mode 100644 index ca95ee0e..00000000 --- a/docs/coder_tokens_rm.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder tokens rm - -remove an API token by its unique ID - -``` -coder tokens rm [token_id] [flags] -``` - -### Options - -``` - -h, --help help for rm -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder tokens](coder_tokens.md) - manage Coder API tokens for the active user - diff --git a/docs/coder_urls.md b/docs/coder_urls.md deleted file mode 100644 index 39cddf89..00000000 --- a/docs/coder_urls.md +++ /dev/null @@ -1,23 +0,0 @@ -## coder urls - -Interact with environment DevURLs - -### Options - -``` - -h, --help help for urls -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder 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 ac60824d..00000000 --- a/docs/coder_urls_create.md +++ /dev/null @@ -1,27 +0,0 @@ -## coder urls create - -Create a new devurl for an environment - -``` -coder urls create [env_name] [port] [flags] -``` - -### Options - -``` - --access string Set DevURL access to [private | org | authed | public] (default "private") - -h, --help help for create - --name string DevURL name - --scheme string Server scheme (http|https) (default "http") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### 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 67d3fc2c..00000000 --- a/docs/coder_urls_ls.md +++ /dev/null @@ -1,25 +0,0 @@ -## coder urls ls - -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") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### 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 be1f8e3c..00000000 --- a/docs/coder_urls_rm.md +++ /dev/null @@ -1,24 +0,0 @@ -## coder urls rm - -Remove a dev url - -``` -coder urls rm [environment_name] [port] [flags] -``` - -### Options - -``` - -h, --help help for rm -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### 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 2bfadc7c..00000000 --- a/docs/coder_users.md +++ /dev/null @@ -1,21 +0,0 @@ -## coder users - -Interact with Coder user accounts - -### Options - -``` - -h, --help help for users -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder 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 ea7b4d4c..00000000 --- a/docs/coder_users_ls.md +++ /dev/null @@ -1,32 +0,0 @@ -## coder users ls - -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") -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### 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 295f0066..00000000 --- a/go.mod +++ /dev/null @@ -1,26 +0,0 @@ -module cdr.dev/coder-cli - -go 1.14 - -require ( - cdr.dev/slog v1.4.0 - cdr.dev/wsep v0.0.0-20200728013649-82316a09813f - github.com/briandowns/spinner v1.12.0 - github.com/fatih/color v1.10.0 - github.com/google/go-cmp v0.5.5 - github.com/gorilla/websocket v1.4.2 - github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f - github.com/klauspost/compress v1.10.8 // indirect - github.com/manifoldco/promptui v0.8.0 - github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 - github.com/rjeczalik/notify v0.9.2 - github.com/spf13/cobra v1.1.3 - github.com/stretchr/testify v1.6.1 // indirect - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect - golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 - golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 - nhooyr.io/websocket v1.8.6 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 4cdbb21a..00000000 --- a/go.sum +++ /dev/null @@ -1,465 +0,0 @@ -cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= -cdr.dev/slog v1.4.0 h1:tLXQJ/hZ5Q051h0MBHSd2Ha8xzdXj7CjtzmG/8dUvUk= -cdr.dev/slog v1.4.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/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -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/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/briandowns/spinner v1.12.0 h1:72O0PzqGJb6G3KgrcIOtL/JAGGZ5ptOMCn9cUHmqsmw= -github.com/briandowns/spinner v1.12.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= -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.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.3.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/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -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.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/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/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -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/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.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= -github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -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/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -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.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -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.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -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/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/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -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= -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-20181029021203-45a5f77698d3/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-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/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-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/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-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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -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-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -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-20181026203630-95b1ffbd15a5/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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34= -golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/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-20190328211700-ab21143f2384/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-20191112195655-aa38f8e97acc/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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -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-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -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.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/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -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 f59630a1..00000000 --- a/internal/activity/pusher.go +++ /dev/null @@ -1,47 +0,0 @@ -package activity - -import ( - "context" - "fmt" - "time" - - "golang.org/x/time/rate" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" -) - -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 { - clog.Log(clog.Error(fmt.Sprintf("push activity: %s", err))) - } -} diff --git a/internal/activity/writer.go b/internal/activity/writer.go deleted file mode 100644 index 02d9d1b8..00000000 --- a/internal/activity/writer.go +++ /dev/null @@ -1,24 +0,0 @@ -// Package activity defines the logic for tracking usage activity metrics. -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 a6e73d16..00000000 --- a/internal/cmd/auth.go +++ /dev/null @@ -1,82 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "net/http" - "net/url" - "os" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/internal/version" - "cdr.dev/coder-cli/pkg/clog" -) - -var errNeedLogin = clog.Fatal( - "failed to read session credentials", - clog.Hintf(`did you run "coder login [https://coder.domain.com]"?`), -) - -const tokenEnv = "CODER_TOKEN" -const urlEnv = "CODER_URL" - -func newClient(ctx context.Context) (coder.Client, error) { - var ( - err error - sessionToken = os.Getenv(tokenEnv) - rawURL = os.Getenv(urlEnv) - ) - - if sessionToken == "" || rawURL == "" { - 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 malformed: %w try running \"coder login\" with a valid URL", err) - } - - c, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Token: sessionToken, - }) - if err != nil { - return nil, xerrors.Errorf("failed to create new coder.Client: %w", err) - } - - apiVersion, err := c.APIVersion(ctx) - if apiVersion != "" && !version.VersionsMatch(apiVersion) { - logVersionMismatchError(apiVersion) - } - if err != nil { - var he *coder.HTTPError - if xerrors.As(err, &he) { - if he.StatusCode == http.StatusUnauthorized { - return nil, xerrors.Errorf("not authenticated: try running \"coder login`\"") - } - } - return nil, err - } - - return c, nil -} - -func logVersionMismatchError(apiVersion string) { - clog.LogWarn( - "version mismatch detected", - fmt.Sprintf("Coder CLI version: %s", version.Version), - fmt.Sprintf("Coder API version: %s", apiVersion), clog.BlankLine, - clog.Tipf("download the appropriate version here: https://github.com/cdr/coder-cli/releases"), - ) -} diff --git a/internal/cmd/ceapi.go b/internal/cmd/ceapi.go deleted file mode 100644 index 2e7b031d..00000000 --- a/internal/cmd/ceapi.go +++ /dev/null @@ -1,238 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "strings" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/pkg/clog" -) - -// Helpers for working with the Coder API. - -// lookupUserOrgs gets a list of orgs the user is apart of. -func lookupUserOrgs(user *coder.User, orgs []coder.Organization) []coder.Organization { - // NOTE: We don't know in advance how many orgs the user is in so we can't pre-alloc. - var userOrgs []coder.Organization - - 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.Organizations(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.UserEnvironmentsByOrganization(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 -} - -// searchForEnv searches a user's environments to find the specified envName. If none is found, the haystack of -// environment names is returned. -func searchForEnv(ctx context.Context, client coder.Client, envName, userEmail string) (_ *coder.Environment, haystack []string, _ error) { - envs, err := getEnvs(ctx, client, userEmail) - if err != nil { - return nil, 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. - for _, env := range envs { - if env.Name == envName { - return &env, nil, nil - } - // Keep track of what we found for the logs. - haystack = append(haystack, env.Name) - } - return nil, haystack, coder.ErrNotFound -} - -// findEnv returns a single environment by name (if it exists.). -func findEnv(ctx context.Context, client coder.Client, envName, userEmail string) (*coder.Environment, error) { - env, haystack, err := searchForEnv(ctx, client, envName, userEmail) - if err != nil { - return nil, clog.Fatal( - "failed to find environment", - fmt.Sprintf("environment %q not found in %q", envName, haystack), - clog.BlankLine, - clog.Tipf("run \"coder envs ls\" to view your environments"), - ) - } - return env, nil -} - -type findImgConf struct { - email string - imgName string - orgName string -} - -func findImg(ctx context.Context, client coder.Client, conf findImgConf) (*coder.Image, error) { - switch { - case conf.email == "": - return nil, xerrors.New("user email unset") - case conf.imgName == "": - return nil, xerrors.New("image name unset") - } - - imgs, err := getImgs(ctx, client, getImgsConf{ - email: conf.email, - orgName: conf.orgName, - }) - if err != nil { - return nil, err - } - - var possibleMatches []coder.Image - - // The user may provide an image thats not an exact match - // to one of their imported images but they may be close. - // We can assist the user by collecting images that contain - // the user provided image flag value as a substring. - for _, img := range imgs { - // If it's an exact match we can just return and exit. - if img.Repository == conf.imgName { - return &img, nil - } - if strings.Contains(img.Repository, conf.imgName) { - possibleMatches = append(possibleMatches, img) - } - } - - if len(possibleMatches) == 0 { - return nil, xerrors.New("image not found - did you forget to import this image?") - } - - lines := []string{clog.Hintf("Did you mean?")} - - for _, img := range possibleMatches { - lines = append(lines, fmt.Sprintf(" %s", img.Repository)) - } - return nil, clog.Fatal( - fmt.Sprintf("image %s not found", conf.imgName), - lines..., - ) -} - -type getImgsConf struct { - email string - orgName string -} - -func getImgs(ctx context.Context, client coder.Client, conf getImgsConf) ([]coder.Image, error) { - u, err := client.UserByEmail(ctx, conf.email) - if err != nil { - return nil, err - } - - orgs, err := client.Organizations(ctx) - if err != nil { - return nil, err - } - - orgs = lookupUserOrgs(u, orgs) - - for _, org := range orgs { - imgs, err := client.OrganizationImages(ctx, org.ID) - if err != nil { - return nil, err - } - // If orgName is set we know the user is a multi-org member - // so we should only return the imported images that beong to the org they specified. - if conf.orgName != "" && conf.orgName == org.Name { - return imgs, nil - } - - if conf.orgName == "" { - // if orgName is unset we know the user is only part of one org. - return imgs, nil - } - } - return nil, xerrors.Errorf("org name %q not found", conf.orgName) -} - -func isMultiOrgMember(ctx context.Context, client coder.Client, email string) (bool, error) { - orgs, err := getUserOrgs(ctx, client, email) - if err != nil { - return false, err - } - return len(orgs) > 1, nil -} - -func getUserOrgs(ctx context.Context, client coder.Client, email string) ([]coder.Organization, error) { - u, err := client.UserByEmail(ctx, email) - if err != nil { - return nil, xerrors.New("email not found") - } - - orgs, err := client.Organizations(ctx) - if err != nil { - return nil, xerrors.New("no organizations found") - } - return lookupUserOrgs(u, orgs), nil -} - -func getEnvsByProvider(ctx context.Context, client coder.Client, wpName, userEmail string) ([]coder.Environment, error) { - wp, err := coderutil.ProviderByName(ctx, client, wpName) - if err != nil { - return nil, err - } - - envs, err := client.EnvironmentsByWorkspaceProvider(ctx, wp.ID) - if err != nil { - return nil, err - } - - envs, err = filterEnvsByUser(ctx, client, userEmail, envs) - if err != nil { - return nil, err - } - return envs, nil -} - -func filterEnvsByUser(ctx context.Context, client coder.Client, userEmail string, envs []coder.Environment) ([]coder.Environment, error) { - user, err := client.UserByEmail(ctx, userEmail) - if err != nil { - return nil, xerrors.Errorf("get user: %w", err) - } - - var filteredEnvs []coder.Environment - for _, env := range envs { - if env.UserID == user.ID { - filteredEnvs = append(filteredEnvs, env) - } - } - return filteredEnvs, nil -} diff --git a/internal/cmd/cli_test.go b/internal/cmd/cli_test.go deleted file mode 100644 index ca145a9d..00000000 --- a/internal/cmd/cli_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package cmd - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/url" - "os" - "strings" - "testing" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - "cdr.dev/slog/sloggers/slogtest/assert" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/pkg/clog" -) - -var ( - shouldSkipAuthedTests bool = false - testCoderClient coder.Client -) - -func isCI() bool { _, ok := os.LookupEnv("CI"); return ok } - -func skipIfNoAuth(t *testing.T) { - if shouldSkipAuthedTests { - t.Skip("no authentication provided and not in CI, skipping") - } -} - -func init() { - tmpDir, err := ioutil.TempDir("", "coder-cli-config-dir") - if err != nil { - panic(err) - } - config.SetRoot(tmpDir) - - email := os.Getenv("CODER_EMAIL") - password := os.Getenv("CODER_PASSWORD") - rawURL := os.Getenv("CODER_URL") - if email == "" || password == "" || rawURL == "" { - if isCI() { - panic("when run in CI, CODER_EMAIL, CODER_PASSWORD, and CODER_URL are required environment variables") - } - shouldSkipAuthedTests = true - return - } - u, err := url.Parse(rawURL) - if err != nil { - panic("invalid CODER_URL: " + err.Error()) - } - client, err := coder.NewClient(coder.ClientOptions{ - BaseURL: u, - Email: email, - Password: password, - }) - if err != nil { - panic("new client: " + err.Error()) - } - testCoderClient = client - if err := config.URL.Write(rawURL); err != nil { - panic("write config url: " + err.Error()) - } - if err := config.Session.Write(client.Token()); err != nil { - panic("write config token: " + err.Error()) - } -} - -type result struct { - outBuffer *bytes.Buffer - errBuffer *bytes.Buffer - exitErr error -} - -func (r result) success(t *testing.T) { - t.Helper() - assert.Success(t, "execute command", r.exitErr) -} - -func (r result) error(t *testing.T) { - t.Helper() - assert.Error(t, "execute command", r.exitErr) -} - -//nolint -func (r result) stdoutContains(t *testing.T, substring string) { - t.Helper() - if !strings.Contains(r.outBuffer.String(), substring) { - slogtest.Fatal(t, "stdout contains substring", slog.F("substring", substring), slog.F("stdout", r.outBuffer.String())) - } -} - -func (r result) stdoutUnmarshals(t *testing.T, target interface{}) { - t.Helper() - err := json.Unmarshal(r.outBuffer.Bytes(), target) - assert.Success(t, "unmarshal json", err) -} - -//nolint -func (r result) stdoutEmpty(t *testing.T) { - t.Helper() - assert.Equal(t, "stdout empty", "", r.outBuffer.String()) -} - -//nolint -func (r result) stderrEmpty(t *testing.T) { - t.Helper() - assert.Equal(t, "stderr empty", "", r.errBuffer.String()) -} - -//nolint -func (r result) stderrContains(t *testing.T, substring string) { - t.Helper() - if !strings.Contains(r.errBuffer.String(), substring) { - slogtest.Fatal(t, "stderr contains substring", slog.F("substring", substring), slog.F("stderr", r.errBuffer.String())) - } -} - -//nolint -func (r result) clogError(t *testing.T) clog.CLIError { - t.Helper() - var cliErr clog.CLIError - if !xerrors.As(r.exitErr, &cliErr) { - slogtest.Fatal(t, "expected clog error, none found", slog.Error(r.exitErr), slog.F("type", fmt.Sprintf("%T", r.exitErr))) - } - slogtest.Debug(t, "clog error", slog.F("message", cliErr.String())) - return cliErr -} - -//nolint -func execute(t *testing.T, in io.Reader, args ...string) result { - cmd := Make() - - var outStream bytes.Buffer - var errStream bytes.Buffer - - cmd.SetArgs(args) - - cmd.SetIn(in) - cmd.SetOut(&outStream) - cmd.SetErr(&errStream) - - // TODO: this *needs* to be moved to function scoped writer arg. As is, - // this prevents tests from running in parallel. - clog.SetOutput(&errStream) - - err := cmd.Execute() - - slogtest.Debug(t, "execute command", - slog.F("out_buffer", outStream.String()), - slog.F("err_buffer", errStream.String()), - slog.F("args", args), - slog.F("execute_error", err), - ) - if err != nil { - clog.Log(err) - } - return result{ - outBuffer: &outStream, - errBuffer: &errStream, - exitErr: err, - } -} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go deleted file mode 100644 index 888fd84c..00000000 --- a/internal/cmd/cmd.go +++ /dev/null @@ -1,117 +0,0 @@ -// Package cmd constructs all subcommands for coder-cli. -package cmd - -import ( - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" - - "cdr.dev/coder-cli/internal/x/xcobra" -) - -// verbose is a global flag for specifying that a command should give verbose output. -var verbose bool = false - -// 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 installation", - SilenceErrors: true, - SilenceUsage: true, - DisableAutoGenTag: true, - } - - app.AddCommand( - loginCmd(), - logoutCmd(), - sshCmd(), - usersCmd(), - tagsCmd(), - configSSHCmd(), - envsCmd(), - syncCmd(), - urlCmd(), - tokensCmd(), - resourceCmd(), - completionCmd(), - imgsCmd(), - providersCmd(), - genDocsCmd(app), - ) - app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") - return app -} - -func genDocsCmd(rootCmd *cobra.Command) *cobra.Command { - return &cobra.Command{ - Use: "gen-docs [dir_path]", - Short: "Generate a markdown documentation tree for the root command.", - Args: xcobra.ExactArgs(1), - Example: "coder gen-docs ./docs", - 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 -func completionCmd() *cobra.Command { - return &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(cmd.OutOrStdout()) // Best effort. - case "zsh": - _ = cmd.Root().GenZshCompletion(cmd.OutOrStdout()) // Best effort. - case "fish": - _ = cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true) // Best effort. - case "powershell": - _ = cmd.Root().GenPowerShellCompletion(cmd.OutOrStdout()) // Best effort. - } - }, - } -} diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go deleted file mode 100644 index 9bfb0cd4..00000000 --- a/internal/cmd/configssh.go +++ /dev/null @@ -1,241 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "io/ioutil" - "net/url" - "os" - "os/user" - "path/filepath" - "sort" - "strings" - - "cdr.dev/coder-cli/pkg/clog" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" -) - -const sshStartToken = "# ------------START-CODER-ENTERPRISE-----------" -const sshStartMessage = `# The following has been auto-generated by "coder config-ssh" -# to make accessing your Coder environments easier. -# -# To remove this blob, run: -# -# coder config-ssh --remove -# -# You should not hand-edit this section, unless you are deleting it.` -const sshEndToken = "# ------------END-CODER-ENTERPRISE------------" - -func configSSHCmd() *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"), "override the default path of your ssh config file") - cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder ssh config") - - return cmd -} - -func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []string) error { - return func(cmd *cobra.Command, _ []string) error { - ctx := cmd.Context() - 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) - } - - currentConfig, didRemoveConfig := removeOldConfig(currentConfig) - if *remove { - if !didRemoveConfig { - return xerrors.Errorf("the Coder ssh configuration section could not be safely deleted or does not exist") - } - - err = writeStr(*configpath, currentConfig) - if err != nil { - return xerrors.Errorf("write to ssh config file %q: %s", *configpath, err) - } - _ = os.Remove(privateKeyFilepath) - - return nil - } - - client, err := newClient(ctx) - if err != nil { - return err - } - - user, err := client.Me(ctx) - if err != nil { - return xerrors.Errorf("fetch username: %w", err) - } - - envs, err := getEnvs(ctx, client, coder.Me) - if err != nil { - return err - } - if len(envs) < 1 { - return xerrors.New("no environments found") - } - - envsWithProviders, err := coderutil.EnvsWithProvider(ctx, client, envs) - if err != nil { - return xerrors.Errorf("resolve env workspace providers: %w", err) - } - - if !sshAvailable(envsWithProviders) { - return xerrors.New("SSH is disabled or not available for any environments in your Coder deployment.") - } - - newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath) - - 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 { - if !xerrors.Is(err, os.ErrPermission) { - return xerrors.Errorf("write ssh key: %w", err) - } - fmt.Printf("Your private ssh key already exists at \"%s\"\nYou may need to remove the existing private key file and re-run this command\n\n", privateKeyFilepath) - } else { - fmt.Printf("Your private ssh key was written to \"%s\"\n", privateKeyFilepath) - } - - writeSSHUXState(ctx, client, user.ID, envs) - fmt.Printf("An auto-generated ssh config was written to \"%s\"\n", *configpath) - 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 - } -} - -// removeOldConfig removes the old ssh configuration from the user's sshconfig. -// Returns true if the config was modified. -func removeOldConfig(config string) (string, bool) { - startIndex := strings.Index(config, sshStartToken) - endIndex := strings.Index(config, sshEndToken) - - if startIndex == -1 || endIndex == -1 { - return config, false - } - if startIndex == 0 { - return config[endIndex+len(sshEndToken)+1:], true - } - return config[:startIndex-1] + config[endIndex+len(sshEndToken)+1:], true -} - -// sshAvailable returns true if SSH is available for at least one environment. -func sshAvailable(envs []coderutil.EnvWithWorkspaceProvider) bool { - for _, env := range envs { - if env.WorkspaceProvider.SSHEnabled { - return true - } - } - return false -} - -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), 0600) -} - -func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, privateKeyFilepath string) string { - newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage) - - sort.Slice(envs, func(i, j int) bool { return envs[i].Env.Name < envs[j].Env.Name }) - - for _, env := range envs { - if !env.WorkspaceProvider.SSHEnabled { - clog.LogWarn(fmt.Sprintf("SSH is not enabled for workspace provider %q", env.WorkspaceProvider.Name), - clog.BlankLine, - clog.Tipf("ask an infrastructure administrator to enable SSH for this workspace provider"), - ) - continue - } - u, err := url.Parse(env.WorkspaceProvider.EnvproxyAccessURL) - if err != nil { - clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", env.WorkspaceProvider.EnvproxyAccessURL)) - continue - } - newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath) - } - newConfig += fmt.Sprintf("\n%s\n", sshEndToken) - - return newConfig -} - -func makeSSHConfig(host, userName, envName, privateKeyFilepath string) string { - return fmt.Sprintf( - `Host coder.%s - HostName %s - User %s-%s - StrictHostKeyChecking no - ConnectTimeout=0 - IdentitiesOnly yes - IdentityFile="%s" - ServerAliveInterval 60 - ServerAliveCountMax 3 -`, envName, host, userName, envName, privateKeyFilepath) -} - -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 -} - -func writeSSHUXState(ctx context.Context, client coder.Client, userID string, envs []coder.Environment) { - // Create a map of env.ID -> true to indicate to the web client that all - // current environments have SSH configured - cliSSHConfigured := make(map[string]bool) - for _, env := range envs { - cliSSHConfigured[env.ID] = true - } - // Update UXState that coder config-ssh has been run by the currently - // authenticated user - err := client.UpdateUXState(ctx, userID, map[string]interface{}{"cliSSHConfigured": cliSSHConfigured}) - if err != nil { - clog.LogWarn("The Coder web client may not recognize that you've configured SSH.") - } -} diff --git a/internal/cmd/devurls_test.go b/internal/cmd/devurls_test.go deleted file mode 100644 index 3e3c2bd4..00000000 --- a/internal/cmd/devurls_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package cmd - -import ( - "testing" -) - -func Test_devurls(t *testing.T) { - skipIfNoAuth(t) - res := execute(t, nil, "urls", "ls") - res.error(t) -} diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go deleted file mode 100644 index 4a0c0101..00000000 --- a/internal/cmd/envs.go +++ /dev/null @@ -1,701 +0,0 @@ -package cmd - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "io/ioutil" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" - - "github.com/manifoldco/promptui" - "github.com/spf13/cobra" - "golang.org/x/xerrors" -) - -const defaultImgTag = "latest" - -func envsCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "envs", - Short: "Interact with Coder environments", - Long: "Perform operations on the Coder environments owned by the active user.", - } - - cmd.AddCommand( - lsEnvsCommand(), - stopEnvsCmd(), - rmEnvsCmd(), - watchBuildLogCommand(), - rebuildEnvCommand(), - createEnvCmd(), - createEnvFromConfigCmd(), - editEnvCmd(), - ) - return cmd -} - -const ( - humanOutput = "human" - jsonOutput = "json" -) - -func lsEnvsCommand() *cobra.Command { - var ( - outputFmt string - user string - provider string - ) - - cmd := &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 { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - envs, err := getEnvs(ctx, client, user) - if err != nil { - return err - } - if provider != "" { - envs, err = getEnvsByProvider(ctx, client, provider, user) - if err != nil { - return err - } - } - if len(envs) < 1 { - clog.LogInfo("no environments found") - envs = []coder.Environment{} // ensures that json output still marshals - } - - switch outputFmt { - case humanOutput: - envs, err := coderutil.EnvsHumanTable(ctx, client, envs) - if err != nil { - return err - } - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(envs), func(i int) interface{} { - return envs[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - case jsonOutput: - err := json.NewEncoder(cmd.OutOrStdout()).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 - }, - } - - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - cmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human | json") - cmd.Flags().StringVarP(&provider, "provider", "p", "", "Filter environments by a particular workspace provider name.") - - return cmd -} - -func stopEnvsCmd() *cobra.Command { - var user string - cmd := &cobra.Command{ - Use: "stop [...environment_names]", - Short: "stop Coder environments by name", - Long: "Stop Coder environments by name", - Example: `coder envs stop front-end-env -coder envs stop front-end-env backend-env - -# stop all of your environments -coder envs ls -o json | jq -c '.[].name' | xargs coder envs stop - -# stop all environments for a given user -coder envs --user charlie@coder.com ls -o json \ - | jq -c '.[].name' \ - | xargs coder envs --user charlie@coder.com stop`, - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return xerrors.Errorf("new client: %w", err) - } - - egroup := clog.LoggedErrGroup() - for _, envName := range args { - envName := envName - egroup.Go(func() error { - env, err := findEnv(ctx, client, envName, user) - if err != nil { - return err - } - - if err = client.StopEnvironment(ctx, env.ID); err != nil { - return clog.Error(fmt.Sprintf("stop environment %q", env.Name), - clog.Causef(err.Error()), clog.BlankLine, - clog.Hintf("current environment status is %q", env.LatestStat.ContainerStatus), - ) - } - clog.LogSuccess(fmt.Sprintf("successfully stopped environment %q", envName)) - return nil - }) - } - - return egroup.Wait() - }, - } - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - return cmd -} - -func createEnvCmd() *cobra.Command { - var ( - org string - cpu float32 - memory float32 - disk int - gpus int - img string - tag string - follow bool - useCVM bool - providerName string - enableAutostart bool - ) - - cmd := &cobra.Command{ - Use: "create [environment_name]", - Short: "create a new environment.", - Args: xcobra.ExactArgs(1), - Long: "Create a new Coder environment.", - Example: `# create a new environment using default resource amounts -coder envs create my-new-env --image ubuntu -coder envs create my-new-powerful-env --cpu 12 --disk 100 --memory 16 --image ubuntu`, - PreRun: func(cmd *cobra.Command, args []string) { - autoStartInfo() - }, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - if img == "" { - return xerrors.New("image unset") - } - - client, err := newClient(ctx) - if err != nil { - return err - } - - multiOrgMember, err := isMultiOrgMember(ctx, client, coder.Me) - if err != nil { - return err - } - - if multiOrgMember && org == "" { - return xerrors.New("org is required for multi-org members") - } - importedImg, err := findImg(ctx, client, findImgConf{ - email: coder.Me, - imgName: img, - orgName: org, - }) - if err != nil { - return err - } - - var provider *coder.KubernetesProvider - if providerName == "" { - provider, err = coderutil.DefaultWorkspaceProvider(ctx, client) - if err != nil { - return xerrors.Errorf("default workspace provider: %w", err) - } - } else { - provider, err = coderutil.ProviderByName(ctx, client, providerName) - if err != nil { - return xerrors.Errorf("provider by name: %w", err) - } - } - - // ExactArgs(1) ensures our name value can't panic on an out of bounds. - createReq := &coder.CreateEnvironmentRequest{ - Name: args[0], - ImageID: importedImg.ID, - OrgID: importedImg.OrganizationID, - ImageTag: tag, - CPUCores: cpu, - MemoryGB: memory, - DiskGB: disk, - GPUs: gpus, - UseContainerVM: useCVM, - ResourcePoolID: provider.ID, - Namespace: provider.DefaultNamespace, - EnableAutoStart: enableAutostart, - } - - // if any of these defaulted to their zero value we provision - // the create request with the imported image defaults instead. - if createReq.CPUCores == 0 { - createReq.CPUCores = importedImg.DefaultCPUCores - } - if createReq.MemoryGB == 0 { - createReq.MemoryGB = importedImg.DefaultMemoryGB - } - if createReq.DiskGB == 0 { - createReq.DiskGB = importedImg.DefaultDiskGB - } - - env, err := client.CreateEnvironment(ctx, *createReq) - if err != nil { - return xerrors.Errorf("create environment: %w", err) - } - - if follow { - clog.LogSuccess("creating environment...") - if err := trailBuildLogs(ctx, client, env.ID); err != nil { - return err - } - return nil - } - - clog.LogSuccess("creating environment...", - clog.BlankLine, - clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, env.Name), - ) - return nil - }, - } - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the environment should be created under.") - cmd.Flags().StringVarP(&tag, "tag", "t", defaultImgTag, "tag of the image the environment will be based off of.") - cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "number of cpu cores the environment should be provisioned with.") - cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "GB of RAM an environment should be provisioned with.") - cmd.Flags().IntVarP(&disk, "disk", "d", 0, "GB of disk storage an environment should be provisioned with.") - cmd.Flags().IntVarP(&gpus, "gpus", "g", 0, "number GPUs an environment should be provisioned with.") - cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image to base the environment off of.") - cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the environment") - cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") - cmd.Flags().BoolVar(&useCVM, "container-based-vm", false, "deploy the environment as a Container-based VM") - cmd.Flags().BoolVar(&enableAutostart, "enable-autostart", false, "automatically start this environment at your preferred time.") - _ = cmd.MarkFlagRequired("image") - return cmd -} - -func createEnvFromConfigCmd() *cobra.Command { - var ( - ref string - repo string - follow bool - filepath string - org string - providerName string - envName string - ) - - cmd := &cobra.Command{ - Use: "create-from-config", - Short: "create a new environment from a template", - Long: "Create a new Coder environment using a Workspaces As Code template.", - Example: `# create a new environment from git repository -coder envs create-from-config --name="dev-env" --repo-url https://github.com/cdr/m --ref my-branch -coder envs create-from-config --name="dev-env" -f coder.yaml`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - if envName == "" { - return clog.Error("Must provide a environment name.", - clog.BlankLine, - clog.Tipf("Use --name= to name your environment"), - ) - } - - client, err := newClient(ctx) - if err != nil { - return err - } - - orgs, err := getUserOrgs(ctx, client, coder.Me) - if err != nil { - return err - } - - multiOrgMember := len(orgs) > 1 - if multiOrgMember && org == "" { - return xerrors.New("org is required for multi-org members") - } - - var userOrg *coder.Organization - for i := range orgs { - // Look for org by name - if orgs[i].Name == org { - userOrg = &orgs[i] - break - } - // Or use default if the provided is blank - if org == "" && orgs[i].Default { - userOrg = &orgs[i] - break - } - } - - if userOrg == nil { - if org != "" { - return xerrors.Errorf("Unable to locate org '%s'", org) - } - return xerrors.Errorf("Unable to locate a default organization for the user") - } - - var rd io.Reader - if filepath != "" { - b, err := ioutil.ReadFile(filepath) - if err != nil { - return xerrors.Errorf("read local file: %w", err) - } - rd = bytes.NewReader(b) - } - - req := coder.ParseTemplateRequest{ - RepoURL: repo, - Ref: ref, - Local: rd, - OrgID: userOrg.ID, - Filepath: ".coder/coder.yaml", - } - - version, err := client.ParseTemplate(ctx, req) - if err != nil { - return handleAPIError(err) - } - - provider, err := coderutil.DefaultWorkspaceProvider(ctx, client) - if err != nil { - return xerrors.Errorf("default workspace provider: %w", err) - } - - env, err := client.CreateEnvironment(ctx, coder.CreateEnvironmentRequest{ - OrgID: userOrg.ID, - TemplateID: version.TemplateID, - ResourcePoolID: provider.ID, - Namespace: provider.DefaultNamespace, - Name: envName, - }) - if err != nil { - return handleAPIError(err) - } - - if follow { - clog.LogSuccess("creating environment...") - if err := trailBuildLogs(ctx, client, env.ID); err != nil { - return err - } - return nil - } - - clog.LogSuccess("creating environment...", - clog.BlankLine, - clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, env.Name), - ) - return nil - }, - } - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the environment should be created under.") - cmd.Flags().StringVarP(&filepath, "filepath", "f", "", "path to local template file.") - cmd.Flags().StringVarP(&ref, "ref", "", "master", "git reference to pull template from. May be a branch, tag, or commit hash.") - cmd.Flags().StringVarP(&repo, "repo-url", "r", "", "URL of the git repository to pull the config from. Config file must live in '.coder/coder.yaml'.") - cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") - cmd.Flags().StringVar(&providerName, "provider", "", "name of Workspace Provider with which to create the environment") - cmd.Flags().StringVar(&envName, "name", "", "name of the environment to be created") - return cmd -} - -func editEnvCmd() *cobra.Command { - var ( - org string - img string - tag string - cpu float32 - memory float32 - disk int - gpus int - follow bool - user string - force bool - ) - - cmd := &cobra.Command{ - Use: "edit", - Short: "edit an existing environment and initiate a rebuild.", - Args: xcobra.ExactArgs(1), - Long: "Edit an existing environment and initate a rebuild.", - Example: `coder envs edit back-end-env --cpu 4 - -coder envs edit back-end-env --disk 20`, - PreRun: func(cmd *cobra.Command, args []string) { - autoStartInfo() - }, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - - envName := args[0] - - env, err := findEnv(ctx, client, envName, user) - if err != nil { - return err - } - - multiOrgMember, err := isMultiOrgMember(ctx, client, user) - if err != nil { - return err - } - - // if the user belongs to multiple organizations we need them to specify which one. - if multiOrgMember && org == "" { - return xerrors.New("org is required for multi-org members") - } - - req, err := buildUpdateReq(ctx, client, updateConf{ - cpu: cpu, - memGB: memory, - diskGB: disk, - gpus: gpus, - environment: env, - user: user, - image: img, - imageTag: tag, - orgName: org, - }) - if err != nil { - return err - } - - if !force && env.LatestStat.ContainerStatus == coder.EnvironmentOn { - _, err = (&promptui.Prompt{ - Label: fmt.Sprintf("Rebuild environment %q? (will destroy any work outside of your home directory)", env.Name), - IsConfirm: true, - }).Run() - if err != nil { - return clog.Fatal( - "failed to confirm prompt", clog.BlankLine, - clog.Tipf(`use "--force" to rebuild without a confirmation prompt`), - ) - } - } - - if err := client.EditEnvironment(ctx, env.ID, *req); err != nil { - return xerrors.Errorf("failed to apply changes to environment %q: %w", envName, err) - } - - if follow { - clog.LogSuccess("applied changes to the environment, rebuilding...") - if err := trailBuildLogs(ctx, client, env.ID); err != nil { - return err - } - return nil - } - - clog.LogSuccess("applied changes to the environment, rebuilding...", - clog.BlankLine, - clog.Tipf(`run "coder envs watch-build %s" to trail the build logs`, envName), - ) - return nil - }, - } - cmd.Flags().StringVarP(&org, "org", "o", "", "name of the organization the environment should be created under.") - cmd.Flags().StringVarP(&img, "image", "i", "", "name of the image you want the environment to be based off of.") - cmd.Flags().StringVarP(&tag, "tag", "t", "latest", "image tag of the image you want to base the environment off of.") - cmd.Flags().Float32VarP(&cpu, "cpu", "c", 0, "The number of cpu cores the environment should be provisioned with.") - cmd.Flags().Float32VarP(&memory, "memory", "m", 0, "The amount of RAM an environment should be provisioned with.") - cmd.Flags().IntVarP(&disk, "disk", "d", 0, "The amount of disk storage an environment should be provisioned with.") - cmd.Flags().IntVarP(&gpus, "gpu", "g", 0, "The amount of disk storage to provision the environment with.") - cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild") - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - cmd.Flags().BoolVar(&force, "force", false, "force rebuild without showing a confirmation prompt") - return cmd -} - -func rmEnvsCmd() *cobra.Command { - var ( - force bool - user string - ) - - cmd := &cobra.Command{ - Use: "rm [...environment_names]", - Short: "remove Coder environments by name", - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - if !force { - confirm := promptui.Prompt{ - Label: fmt.Sprintf("Delete environments %q? (all data will be lost)", args), - IsConfirm: true, - } - if _, err := confirm.Run(); err != nil { - return clog.Fatal( - "failed to confirm deletion", clog.BlankLine, - clog.Tipf(`use "--force" to rebuild without a confirmation prompt`), - ) - } - } - - egroup := clog.LoggedErrGroup() - for _, envName := range args { - envName := envName - egroup.Go(func() error { - env, err := findEnv(ctx, client, envName, user) - if err != nil { - return err - } - if err = client.DeleteEnvironment(ctx, env.ID); err != nil { - return clog.Error( - fmt.Sprintf(`failed to delete environment "%s"`, env.Name), - clog.Causef(err.Error()), - ) - } - clog.LogSuccess(fmt.Sprintf("deleted environment %q", env.Name)) - return nil - }) - } - return egroup.Wait() - }, - } - cmd.Flags().BoolVarP(&force, "force", "f", false, "force remove the specified environments without prompting first") - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - return cmd -} - -type updateConf struct { - cpu float32 - memGB float32 - diskGB int - gpus int - environment *coder.Environment - user string - image string - imageTag string - orgName string -} - -func buildUpdateReq(ctx context.Context, client coder.Client, conf updateConf) (*coder.UpdateEnvironmentReq, error) { - var ( - updateReq coder.UpdateEnvironmentReq - defaultCPUCores float32 - defaultMemGB float32 - defaultDiskGB int - ) - - // If this is not empty it means the user is requesting to change the environment image. - if conf.image != "" { - importedImg, err := findImg(ctx, client, findImgConf{ - email: conf.user, - imgName: conf.image, - orgName: conf.orgName, - }) - if err != nil { - return nil, err - } - - // If the user passes an image arg of the image that - // the environment is already using, it was most likely a mistake. - if conf.image != importedImg.Repository { - return nil, xerrors.Errorf("environment is already using image %q", conf.image) - } - - // Since the environment image is being changed, - // the resource amount defaults should be changed to - // reflect that of the default resource amounts of the new image. - defaultCPUCores = importedImg.DefaultCPUCores - defaultMemGB = importedImg.DefaultMemoryGB - defaultDiskGB = importedImg.DefaultDiskGB - updateReq.ImageID = &importedImg.ID - } else { - // if the environment image is not being changed, the default - // resource amounts should reflect the default resource amounts - // of the image the environment is already using. - defaultCPUCores = conf.environment.CPUCores - defaultMemGB = conf.environment.MemoryGB - defaultDiskGB = conf.environment.DiskGB - updateReq.ImageID = &conf.environment.ImageID - } - - // The following logic checks to see if the user specified - // any resource amounts for the environment that need to be changed. - // If they did not, then we will get the zero value back - // and should set the resource amount to the default. - - if conf.cpu == 0 { - updateReq.CPUCores = &defaultCPUCores - } else { - updateReq.CPUCores = &conf.cpu - } - - if conf.memGB == 0 { - updateReq.MemoryGB = &defaultMemGB - } else { - updateReq.MemoryGB = &conf.memGB - } - - if conf.diskGB == 0 { - updateReq.DiskGB = &defaultDiskGB - } else { - updateReq.DiskGB = &conf.diskGB - } - - // Environment disks can not be shrink so we have to overwrite this - // if the user accidentally requests it or if the default diskGB value for a - // newly requested image is smaller than the current amount the environment is using. - if *updateReq.DiskGB < conf.environment.DiskGB { - clog.LogWarn("disk can not be shrunk", - fmt.Sprintf("keeping environment disk at %d GB", conf.environment.DiskGB), - ) - updateReq.DiskGB = &conf.environment.DiskGB - } - - if conf.gpus != 0 { - updateReq.GPUs = &conf.gpus - } - - if conf.imageTag == "" { - // We're forced to make an alloc here because untyped string consts are not addressable. - // i.e. updateReq.ImageTag = &defaultImgTag results in : - // invalid operation: cannot take address of defaultImgTag (untyped string constant "latest") - imgTag := defaultImgTag - updateReq.ImageTag = &imgTag - } else { - updateReq.ImageTag = &conf.imageTag - } - return &updateReq, nil -} - -// TODO (Grey): Remove education in a future non-patch release. -func autoStartInfo() { - var preferencesURI string - - accessURI, err := config.URL.Read() - if err != nil { - // Error is fairly benign in this case, fallback to relative URI - preferencesURI = "/preferences" - } else { - preferencesURI = fmt.Sprintf("%s%s", accessURI, "/preferences?tab=autostart") - } - - clog.LogInfo("⚡NEW: Automate daily environment startup", "Visit "+preferencesURI+" to configure your preferred time") -} diff --git a/internal/cmd/envs_test.go b/internal/cmd/envs_test.go deleted file mode 100644 index b28ea6c7..00000000 --- a/internal/cmd/envs_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "math" - "math/rand" - "os" - "testing" - "time" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - "cdr.dev/slog/sloggers/slogtest/assert" - "github.com/google/go-cmp/cmp" - - "cdr.dev/coder-cli/coder-sdk" -) - -func Test_envs_ls(t *testing.T) { - skipIfNoAuth(t) - res := execute(t, nil, "envs", "ls") - res.success(t) - - res = execute(t, nil, "envs", "ls", "--output=json") - res.success(t) - - var envs []coder.Environment - res.stdoutUnmarshals(t, &envs) -} - -func Test_envs_ls_by_provider(t *testing.T) { - skipIfNoAuth(t) - for _, test := range []struct { - name string - command []string - assert func(r result) - }{ - { - name: "simple list", - command: []string{"envs", "ls", "--provider", "built-in"}, - assert: func(r result) { r.success(t) }, - }, - { - name: "list as json", - command: []string{"envs", "ls", "--provider", "built-in", "--output", "json"}, - assert: func(r result) { - var envs []coder.Environment - r.stdoutUnmarshals(t, &envs) - }, - }, - } { - test := test - t.Run(test.name, func(t *testing.T) { - test.assert(execute(t, nil, test.command...)) - }) - } -} - -func Test_env_create(t *testing.T) { - skipIfNoAuth(t) - ctx := context.Background() - - // Minimum args not received. - res := execute(t, nil, "envs", "create") - res.error(t) - res.stderrContains(t, "accepts 1 arg(s), received 0") - - // Successfully output help. - res = execute(t, nil, "envs", "create", "--help") - res.success(t) - res.stdoutContains(t, "Create a new Coder environment.") - - // Image unset - res = execute(t, nil, "envs", "create", "test-env") - res.error(t) - res.stderrContains(t, "fatal: required flag(s) \"image\" not set") - - // Image not imported - res = execute(t, nil, "envs", "create", "test-env", "--image=doestexist") - res.error(t) - res.stderrContains(t, "fatal: image not found - did you forget to import this image?") - - ensureImageImported(ctx, t, testCoderClient, "ubuntu") - - name := randString(10) - cpu := 2.3 - - // attempt to remove the environment on cleanup - t.Cleanup(func() { _ = execute(t, nil, "envs", "rm", name, "--force") }) - - res = execute(t, nil, "envs", "create", name, "--image=ubuntu", fmt.Sprintf("--cpu=%f", cpu)) - res.success(t) - - res = execute(t, nil, "envs", "ls") - res.success(t) - res.stdoutContains(t, name) - - var envs []coder.Environment - res = execute(t, nil, "envs", "ls", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &envs) - env := assertEnv(t, name, envs) - assert.Equal(t, "env cpu", cpu, float64(env.CPUCores), floatComparer) - - res = execute(t, nil, "envs", "watch-build", name) - res.success(t) - - // edit the CPU of the environment - cpu = 2.1 - res = execute(t, nil, "envs", "edit", name, fmt.Sprintf("--cpu=%f", cpu), "--follow", "--force") - res.success(t) - - // assert that the CPU actually did change after edit - res = execute(t, nil, "envs", "ls", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &envs) - env = assertEnv(t, name, envs) - assert.Equal(t, "env cpu", cpu, float64(env.CPUCores), floatComparer) - - res = execute(t, nil, "envs", "rm", name, "--force") - res.success(t) -} - -func assertEnv(t *testing.T, name string, envs []coder.Environment) *coder.Environment { - for _, e := range envs { - if name == e.Name { - return &e - } - } - slogtest.Fatal(t, "env not found", slog.F("name", name), slog.F("envs", envs)) - return nil -} - -var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) - -//nolint:unparam -func randString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyz" - b := make([]byte, length) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] - } - return string(b) -} - -var floatComparer = cmp.Comparer(func(x, y float64) bool { - delta := math.Abs(x - y) - mean := math.Abs(x+y) / 2.0 - return delta/mean < 0.001 -}) - -// this is a stopgap until we have support for a `coder images` subcommand -// until then, we can use the coder.Client to ensure our integration tests -// work on fresh deployments. -func ensureImageImported(ctx context.Context, t *testing.T, client coder.Client, img string) { - orgs, err := client.Organizations(ctx) - assert.Success(t, "get orgs", err) - - var org *coder.Organization -search: - for _, o := range orgs { - for _, m := range o.Members { - if m.Email == os.Getenv("CODER_EMAIL") { - o := o - org = &o - break search - } - } - } - if org == nil { - slogtest.Fatal(t, "failed to find org of current user") - return // help the linter out a bit - } - - registries, err := client.Registries(ctx, org.ID) - assert.Success(t, "get registries", err) - - var dockerhubID string - for _, r := range registries { - if r.Registry == "index.docker.io" { - dockerhubID = r.ID - } - } - assert.True(t, "docker hub registry found", dockerhubID != "") - - imgs, err := client.OrganizationImages(ctx, org.ID) - assert.Success(t, "get org images", err) - found := false - for _, i := range imgs { - if i.Repository == img { - found = true - } - } - if !found { - // ignore this error for now as it causes a race with other parallel tests - _, _ = client.ImportImage(ctx, coder.ImportImageReq{ - RegistryID: &dockerhubID, - OrgID: org.ID, - Repository: img, - Tag: "latest", - DefaultCPUCores: 2.5, - DefaultDiskGB: 22, - DefaultMemoryGB: 3, - }) - } -} diff --git a/internal/cmd/errors.go b/internal/cmd/errors.go deleted file mode 100644 index e202037e..00000000 --- a/internal/cmd/errors.go +++ /dev/null @@ -1,71 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" -) - -// handleAPIError attempts to convert an api error into a more detailed clog error. -// If it cannot, it will return the original error. -func handleAPIError(origError error) error { - var httpError *coder.HTTPError - if !xerrors.As(origError, &httpError) { - return origError // Return the original - } - - ae, err := httpError.Payload() - if err != nil { - return origError // Return the original - } - - switch ae.Err.Code { - case "wac_template": // template parse errors - type templatePayload struct { - ErrorType string `json:"error_type"` - Msgs []string `json:"messages"` - } - - var p templatePayload - err := json.Unmarshal(ae.Err.Details, &p) - if err != nil { - return origError - } - - return clog.Error(p.ErrorType, p.Msgs...) - case "verbose": - type verbosePayload struct { - Verbose string `json:"verbose"` - } - var p verbosePayload - err := json.Unmarshal(ae.Err.Details, &p) - if err != nil { - return origError - } - - return clog.Error(origError.Error(), p.Verbose) - case "precondition": - type preconditionPayload struct { - Error string `json:"error"` - Message string `json:"message"` - Solution string `json:"solution"` - } - - var p preconditionPayload - err := json.Unmarshal(ae.Err.Details, &p) - if err != nil { - return origError - } - - return clog.Error(fmt.Sprintf("Precondition Error : Status Code=%d", httpError.StatusCode), - p.Message, - clog.BlankLine, - clog.Tipf(p.Solution)) - } - - return origError // Return the original -} diff --git a/internal/cmd/images.go b/internal/cmd/images.go deleted file mode 100644 index 70364a59..00000000 --- a/internal/cmd/images.go +++ /dev/null @@ -1,88 +0,0 @@ -package cmd - -import ( - "encoding/json" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func imgsCmd() *cobra.Command { - var user string - - cmd := &cobra.Command{ - Use: "images", - Short: "Manage Coder images", - Long: "Manage existing images and/or import new ones.", - } - - cmd.PersistentFlags().StringVar(&user, "user", coder.Me, "Specifies the user by email") - cmd.AddCommand(lsImgsCommand(&user)) - return cmd -} - -func lsImgsCommand(user *string) *cobra.Command { - var ( - orgName string - outputFmt string - ) - - cmd := &cobra.Command{ - Use: "ls", - Short: "list all images available to the active user", - Long: "List all Coder images available to the active user.", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - client, err := newClient(ctx) - if err != nil { - return err - } - - imgs, err := getImgs(ctx, client, - getImgsConf{ - email: *user, - orgName: orgName, - }, - ) - - if err != nil { - return err - } - - if len(imgs) < 1 { - clog.LogInfo("no images found") - imgs = []coder.Image{} // ensures that json output still marshals - } - - switch outputFmt { - case jsonOutput: - enc := json.NewEncoder(cmd.OutOrStdout()) - // pretty print the json - enc.SetIndent("", "\t") - - if err := enc.Encode(imgs); err != nil { - return xerrors.Errorf("write images as JSON: %w", err) - } - return nil - case humanOutput: - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(imgs), func(i int) interface{} { - return imgs[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - return nil - default: - return xerrors.Errorf("%q is not a supported value for --output", outputFmt) - } - }, - } - cmd.Flags().StringVar(&orgName, "org", "", "organization name") - cmd.Flags().StringVar(&outputFmt, "output", humanOutput, "human | json") - return cmd -} diff --git a/internal/cmd/images_test.go b/internal/cmd/images_test.go deleted file mode 100644 index b5823ff6..00000000 --- a/internal/cmd/images_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package cmd - -import ( - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func Test_images(t *testing.T) { - res := execute(t, nil, "images", "--help") - res.success(t) - - res = execute(t, nil, "images", "ls") - res.success(t) - - var images []coder.Image - res = execute(t, nil, "images", "ls", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &images) - assert.True(t, "more than 0 images", len(images) > 0) - - res = execute(t, nil, "images", "ls", "--org=doesntexist") - res.error(t) - res.stderrContains(t, "org name \"doesntexist\" not found") -} diff --git a/internal/cmd/login.go b/internal/cmd/login.go deleted file mode 100644 index 27f4584c..00000000 --- a/internal/cmd/login.go +++ /dev/null @@ -1,115 +0,0 @@ -package cmd - -import ( - "bufio" - "context" - "fmt" - "io" - "net/url" - "strings" - - "github.com/pkg/browser" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/internal/version" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" -) - -func loginCmd() *cobra.Command { - return &cobra.Command{ - Use: "login [Coder URL eg. https://my.coder.domain/]", - Short: "Authenticate this client for future operations", - Args: xcobra.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); err != nil { - return xerrors.Errorf("login error: %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) error { - authURL := *envURL - authURL.Path = envURL.Path + "/internal-auth" - q := authURL.Query() - q.Add("show_token", "true") - authURL.RawQuery = q.Encode() - - if err := browser.OpenURL(authURL.String()); err != nil { - fmt.Printf("Open the following in your browser:\n\n\t%s\n\n", authURL.String()) - } else { - fmt.Printf("Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String()) - } - - token := readLine("Paste token here: ", cmd.InOrStdin()) - if err := pingAPI(cmd.Context(), envURL, token); err != nil { - return xerrors.Errorf("ping API with credentials: %w", err) - } - if err := storeConfig(envURL, token, config.URL, config.Session); err != nil { - return xerrors.Errorf("store auth: %w", err) - } - clog.LogSuccess("logged in") - return nil -} - -func readLine(prompt string, r io.Reader) string { - reader := bufio.NewReader(r) - fmt.Print(prompt) - text, _ := reader.ReadString('\n') - return strings.TrimSuffix(text, "\n") -} - -// 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, err := coder.NewClient(coder.ClientOptions{ - BaseURL: envURL, - Token: token, - }) - if err != nil { - return xerrors.Errorf("failed to create coder.Client: %w", err) - } - - if apiVersion, err := client.APIVersion(ctx); err == nil { - if apiVersion != "" && !version.VersionsMatch(apiVersion) { - logVersionMismatchError(apiVersion) - } - } - _, err = client.Me(ctx) - if err != nil { - return xerrors.Errorf("call api: %w", err) - } - return nil -} diff --git a/internal/cmd/logout.go b/internal/cmd/logout.go deleted file mode 100644 index fd864aa1..00000000 --- a/internal/cmd/logout.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "os" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/pkg/clog" -) - -func logoutCmd() *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) { - clog.LogInfo("no active session") - return nil - } - return xerrors.Errorf("delete session: %w", err) - } - clog.LogSuccess("logged out") - return nil -} diff --git a/internal/cmd/providers.go b/internal/cmd/providers.go deleted file mode 100644 index 678327d3..00000000 --- a/internal/cmd/providers.go +++ /dev/null @@ -1,286 +0,0 @@ -package cmd - -import ( - "fmt" - "net/url" - - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/internal/x/xcobra" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func providersCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "providers", - Short: "Interact with Coder workspace providers", - Long: "Perform operations on the Coder Workspace Providers for the platform.", - Hidden: true, - } - - cmd.AddCommand( - createProviderCmd(), - listProviderCmd(), - deleteProviderCmd(), - cordonProviderCmd(), - unCordonProviderCmd(), - ) - return cmd -} - -func createProviderCmd() *cobra.Command { - var ( - hostname string - clusterAddress string - ) - cmd := &cobra.Command{ - Use: "create [name] --hostname=[hostname] --cluster-address=[clusterAddress]", - Args: xcobra.ExactArgs(1), - Short: "create a new workspace provider.", - Long: "Create a new Coder workspace provider.", - Example: `# create a new workspace provider in a pending state - -coder providers create my-provider --hostname=https://provider.example.com --cluster-address=https://255.255.255.255`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - client, err := newClient(ctx) - if err != nil { - return err - } - - version, err := client.APIVersion(ctx) - if err != nil { - return xerrors.Errorf("get application version: %w", err) - } - - cemanagerURL := client.BaseURL() - ingressHost, err := url.Parse(hostname) - if err != nil { - return xerrors.Errorf("parse hostname: %w", err) - } - - if cemanagerURL.Scheme != ingressHost.Scheme { - return xerrors.Errorf("Coder access url and hostname must have matching protocols: coder access url: %s, workspace provider hostname: %s", cemanagerURL.String(), ingressHost.String()) - } - - // ExactArgs(1) ensures our name value can't panic on an out of bounds. - createReq := &coder.CreateWorkspaceProviderReq{ - Name: args[0], - Type: coder.WorkspaceProviderKubernetes, - Hostname: hostname, - ClusterAddress: clusterAddress, - } - - wp, err := client.CreateWorkspaceProvider(ctx, *createReq) - if err != nil { - return xerrors.Errorf("create workspace provider: %w", err) - } - - var sslNote string - if ingressHost.Scheme == "https" { - sslNote = ` -NOTE: Since the hostname provided is using https you must ensure the deployment -has a valid SSL certificate. See https://coder.com/docs/guides/ssl-certificates -for more information.` - } - - clog.LogSuccess(fmt.Sprintf(` -Created workspace provider "%s" -`, createReq.Name)) - _ = tablewriter.WriteTable(cmd.OutOrStdout(), 1, func(i int) interface{} { - return *wp - }) - _, _ = fmt.Fprint(cmd.OutOrStdout(), ` -Now that the workspace provider is provisioned, it must be deployed into the cluster. To learn more, -visit https://coder.com/docs/workspace-providers/deployment - -When connected to the cluster you wish to deploy onto, use the following helm command: - -helm upgrade coder-workspace-provider coder/workspace-provider \ - --version=`+version+` \ - --atomic \ - --install \ - --force \ - --set envproxy.token=`+wp.EnvproxyToken+` \ - --set envproxy.accessURL=`+ingressHost.String()+` \ - --set ingress.host=`+ingressHost.Hostname()+` \ - --set envproxy.clusterAddress=`+clusterAddress+` \ - --set cemanager.accessURL=`+cemanagerURL.String()+` -`+sslNote+` - -WARNING: The 'envproxy.token' is a secret value that authenticates the workspace provider, -make sure not to share this token or make it public. - -Other values can be set on the helm chart to further customize the deployment, see -https://github.com/cdr/enterprise-helm/blob/workspace-providers-envproxy-only/README.md -`) - - return nil - }, - } - - cmd.Flags().StringVar(&hostname, "hostname", "", "workspace provider hostname") - cmd.Flags().StringVar(&clusterAddress, "cluster-address", "", "kubernetes cluster apiserver endpoint") - _ = cmd.MarkFlagRequired("hostname") - _ = cmd.MarkFlagRequired("cluster-address") - return cmd -} - -func listProviderCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "ls", - Short: "list workspace providers.", - Long: "List all Coder workspace providers.", - Example: `# list workspace providers -coder providers ls`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - client, err := newClient(ctx) - if err != nil { - return err - } - - wps, err := client.WorkspaceProviders(ctx) - if err != nil { - return xerrors.Errorf("list workspace providers: %w", err) - } - - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(wps.Kubernetes), func(i int) interface{} { - return wps.Kubernetes[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - return nil - }, - } - return cmd -} - -func deleteProviderCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "rm [workspace_provider_name]", - Short: "remove a workspace provider.", - Long: "Remove an existing Coder workspace provider by name.", - Example: `# remove an existing workspace provider by name -coder providers rm my-workspace-provider`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - - wps, err := client.WorkspaceProviders(ctx) - if err != nil { - return xerrors.Errorf("listing workspace providers: %w", err) - } - - egroup := clog.LoggedErrGroup() - for _, wpName := range args { - name := wpName - egroup.Go(func() error { - var id string - for _, wp := range wps.Kubernetes { - if wp.Name == name { - id = wp.ID - } - } - if id == "" { - return clog.Error( - fmt.Sprintf(`failed to remove workspace provider "%s"`, name), - clog.Causef(`no workspace provider found by name "%s"`, name), - ) - } - - err = client.DeleteWorkspaceProviderByID(ctx, id) - if err != nil { - return clog.Error( - fmt.Sprintf(`failed to remove workspace provider "%s"`, name), - clog.Causef(err.Error()), - ) - } - - clog.LogSuccess(fmt.Sprintf(`removed workspace provider with name "%s"`, name)) - - return nil - }) - } - return egroup.Wait() - }, - } - return cmd -} - -func cordonProviderCmd() *cobra.Command { - var reason string - - cmd := &cobra.Command{ - Use: "cordon [workspace_provider_name]", - Args: xcobra.ExactArgs(1), - Short: "cordon a workspace provider.", - Long: "Prevent an existing Coder workspace provider from supporting any additional workspaces.", - Example: `# cordon an existing workspace provider by name -coder providers cordon my-workspace-provider --reason "limit cloud clost"`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - - wpName := args[0] - provider, err := coderutil.ProviderByName(ctx, client, wpName) - if err != nil { - return err - } - - if err := client.CordonWorkspaceProvider(ctx, provider.ID, reason); err != nil { - return err - } - clog.LogSuccess(fmt.Sprintf("provider %q successfully cordoned - you can no longer create workspaces on this provider without uncordoning first", wpName)) - return nil - }, - } - cmd.Flags().StringVar(&reason, "reason", "", "reason for cordoning the provider") - _ = cmd.MarkFlagRequired("reason") - return cmd -} - -func unCordonProviderCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "uncordon [workspace_provider_name]", - Args: xcobra.ExactArgs(1), - Short: "uncordon a workspace provider.", - Long: "Set a currently cordoned provider as ready; enabling it to continue provisioning resources for new workspaces.", - Example: `# uncordon an existing workspace provider by name -coder providers uncordon my-workspace-provider`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - - wpName := args[0] - provider, err := coderutil.ProviderByName(ctx, client, wpName) - if err != nil { - return err - } - - if err := client.UnCordonWorkspaceProvider(ctx, provider.ID); err != nil { - return err - } - clog.LogSuccess(fmt.Sprintf("provider %q successfully uncordoned - you can now create workspaces on this provider", wpName)) - return nil - }, - } - return cmd -} diff --git a/internal/cmd/providers_test.go b/internal/cmd/providers_test.go deleted file mode 100644 index 685e129b..00000000 --- a/internal/cmd/providers_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package cmd - -import ( - "testing" -) - -func Test_providers_ls(t *testing.T) { - skipIfNoAuth(t) - res := execute(t, nil, "providers", "ls") - res.success(t) -} diff --git a/internal/cmd/rebuild.go b/internal/cmd/rebuild.go deleted file mode 100644 index 8cd6ca2e..00000000 --- a/internal/cmd/rebuild.go +++ /dev/null @@ -1,189 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/briandowns/spinner" - "github.com/fatih/color" - "github.com/manifoldco/promptui" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" -) - -func rebuildEnvCommand() *cobra.Command { - var follow bool - var force bool - var user string - cmd := &cobra.Command{ - Use: "rebuild [environment_name]", - Short: "rebuild a Coder environment", - Args: xcobra.ExactArgs(1), - Example: `coder envs rebuild front-end-env --follow -coder envs rebuild backend-env --force`, - PreRun: func(cmd *cobra.Command, args []string) { - autoStartInfo() - }, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - env, err := findEnv(ctx, client, args[0], user) - if err != nil { - return err - } - - if !force && env.LatestStat.ContainerStatus == coder.EnvironmentOn { - _, err = (&promptui.Prompt{ - Label: fmt.Sprintf("Rebuild environment %q? (will destroy any work outside of your home directory)", env.Name), - IsConfirm: true, - }).Run() - if err != nil { - return clog.Fatal( - "failed to confirm prompt", clog.BlankLine, - clog.Tipf(`use "--force" to rebuild without a confirmation prompt`), - ) - } - } - - if err = client.RebuildEnvironment(ctx, env.ID); err != nil { - return err - } - if follow { - if err = trailBuildLogs(ctx, client, env.ID); err != nil { - return err - } - } else { - clog.LogSuccess( - "successfully started rebuild", - clog.Tipf("run \"coder envs watch-build %s\" to follow the build logs", env.Name), - ) - } - return nil - }, - } - - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - cmd.Flags().BoolVar(&follow, "follow", false, "follow build log after initiating rebuild") - cmd.Flags().BoolVar(&force, "force", false, "force rebuild without showing a confirmation prompt") - return cmd -} - -// trailBuildLogs follows the build log for a given environment and prints the staged -// output with loaders and success/failure indicators for each stage. -func trailBuildLogs(ctx context.Context, client coder.Client, envID string) error { - const check = "✅" - const failure = "❌" - - newSpinner := func() *spinner.Spinner { return spinner.New(spinner.CharSets[11], 100*time.Millisecond) } - - // this tells us whether to show dynamic loaders when printing output - isTerminal := showInteractiveOutput - - logs, err := client.FollowEnvironmentBuildLog(ctx, envID) - if err != nil { - return err - } - - var s *spinner.Spinner - for l := range logs { - if l.Err != nil { - return l.Err - } - - logTime := l.BuildLog.Time.Local() - msg := fmt.Sprintf("%s %s", logTime.Format(time.RFC3339), l.BuildLog.Msg) - - switch l.BuildLog.Type { - case coder.BuildLogTypeStart: - // the FE uses this to reset the UI - // the CLI doesn't need to do anything here given that we only append to the trail - - case coder.BuildLogTypeStage: - if !isTerminal { - fmt.Println(msg) - continue - } - - if s != nil { - s.Stop() - fmt.Print("\n") - } - - s = newSpinner() - s.Suffix = fmt.Sprintf(" -- %s", msg) - s.FinalMSG = fmt.Sprintf("%s -- %s", check, msg) - s.Start() - - case coder.BuildLogTypeSubstage: - // TODO(@f0ssel) add verbose substage printing - if !verbose { - continue - } - - case coder.BuildLogTypeError: - if !isTerminal { - fmt.Println(msg) - continue - } - - if s != nil { - s.FinalMSG = fmt.Sprintf("%s %s", failure, strings.TrimPrefix(s.Suffix, " ")) - s.Stop() - fmt.Print("\n") - } - - s = newSpinner() - s.Suffix = color.RedString(" -- %s", msg) - s.FinalMSG = color.RedString("%s -- %s", failure, msg) - s.Start() - - case coder.BuildLogTypeDone: - if s != nil { - s.Stop() - fmt.Print("\n") - } - - return nil - default: - return xerrors.Errorf("unknown buildlog type: %s", l.BuildLog.Type) - } - } - return nil -} - -func watchBuildLogCommand() *cobra.Command { - var user string - cmd := &cobra.Command{ - Use: "watch-build [environment_name]", - Example: "coder envs watch-build front-end-env", - Short: "trail the build log of a Coder environment", - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - env, err := findEnv(ctx, client, args[0], user) - if err != nil { - return err - } - - if err = trailBuildLogs(ctx, client, env.ID); err != nil { - return err - } - return nil - }, - } - cmd.Flags().StringVar(&user, "user", coder.Me, "Specify the user whose resources to target") - return cmd -} diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go deleted file mode 100644 index 5611594f..00000000 --- a/internal/cmd/resourcemanager.go +++ /dev/null @@ -1,433 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "sort" - "strings" - "text/tabwriter" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" -) - -func resourceCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "resources", - Short: "manage Coder resources with platform-level context (users, organizations, environments)", - Hidden: true, - } - cmd.AddCommand(resourceTop()) - return cmd -} - -type resourceTopOptions struct { - group string - user string - org string - sortBy string - provider string - showEmptyGroups bool -} - -func resourceTop() *cobra.Command { - var options resourceTopOptions - - cmd := &cobra.Command{ - Use: "top", - Short: "resource viewer with Coder platform annotations", - RunE: runResourceTop(&options), - Args: xcobra.ExactArgs(0), - Example: `coder resources top --group org -coder resources top --group org --verbose --org DevOps -coder resources top --group user --verbose --user name@example.com -coder resources top --group provider --verbose --provider myprovider -coder resources top --sort-by memory --show-empty`, - } - cmd.Flags().StringVar(&options.group, "group", "user", "the grouping parameter (user|org|provider)") - cmd.Flags().StringVar(&options.user, "user", "", "filter by a user email") - cmd.Flags().StringVar(&options.org, "org", "", "filter by the name of an organization") - cmd.Flags().StringVar(&options.provider, "provider", "", "filter by the name of a workspace provider") - cmd.Flags().StringVar(&options.sortBy, "sort-by", "cpu", "field to sort aggregate groups and environments by (cpu|memory)") - cmd.Flags().BoolVar(&options.showEmptyGroups, "show-empty", false, "show groups with zero active environments") - - return cmd -} - -func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - - // NOTE: it's not worth parrallelizing these calls yet given that this specific endpoint - // takes about 20x times longer than the other two - allEnvs, err := client.Environments(ctx) - if err != nil { - return xerrors.Errorf("get environments %w", err) - } - // only include environments whose last status was "ON" - envs := make([]coder.Environment, 0) - for _, e := range allEnvs { - if e.LatestStat.ContainerStatus == coder.EnvironmentOn { - envs = append(envs, e) - } - } - - users, err := client.Users(ctx) - if err != nil { - return xerrors.Errorf("get users: %w", err) - } - images, err := coderutil.MakeImageMap(ctx, client, envs) - if err != nil { - return xerrors.Errorf("get images: %w", err) - } - - orgs, err := client.Organizations(ctx) - if err != nil { - return xerrors.Errorf("get organizations: %w", err) - } - - providers, err := client.WorkspaceProviders(ctx) - if err != nil { - return xerrors.Errorf("get workspace providers: %w", err) - } - data := entities{ - providers: providers.Kubernetes, - users: users, - orgs: orgs, - envs: envs, - images: images, - } - return presentEntites(cmd.OutOrStdout(), data, *options) - } -} - -func presentEntites(w io.Writer, data entities, options resourceTopOptions) error { - var ( - groups []groupable - labeler envLabeler - ) - switch options.group { - case "user": - groups, labeler = aggregateByUser(data, options) - case "org": - groups, labeler = aggregateByOrg(data, options) - case "provider": - groups, labeler = aggregateByProvider(data, options) - default: - return xerrors.Errorf("unknown --group %q", options.group) - } - - return printResourceTop(w, groups, labeler, options.showEmptyGroups, options.sortBy) -} - -type entities struct { - providers []coder.KubernetesProvider - users []coder.User - orgs []coder.Organization - envs []coder.Environment - images map[string]*coder.Image -} - -func aggregateByUser(data entities, options resourceTopOptions) ([]groupable, envLabeler) { - var groups []groupable - providerIDMap := providerIDs(data.providers) - orgIDMap := make(map[string]coder.Organization) - for _, o := range data.orgs { - orgIDMap[o.ID] = o - } - userEnvs := make(map[string][]coder.Environment, len(data.users)) - for _, e := range data.envs { - if options.org != "" && orgIDMap[e.OrganizationID].Name != options.org { - continue - } - userEnvs[e.UserID] = append(userEnvs[e.UserID], e) - } - for _, u := range data.users { - if options.user != "" && u.Email != options.user { - continue - } - groups = append(groups, userGrouping{user: u, envs: userEnvs[u.ID]}) - } - return groups, labelAll(imgLabeler(data.images), providerLabeler(providerIDMap), orgLabeler(orgIDMap)) -} - -func userIDs(users []coder.User) map[string]coder.User { - userIDMap := make(map[string]coder.User) - for _, u := range users { - userIDMap[u.ID] = u - } - return userIDMap -} - -func aggregateByOrg(data entities, options resourceTopOptions) ([]groupable, envLabeler) { - var groups []groupable - providerIDMap := providerIDs(data.providers) - orgEnvs := make(map[string][]coder.Environment, len(data.orgs)) - userIDMap := userIDs(data.users) - for _, e := range data.envs { - if options.user != "" && userIDMap[e.UserID].Email != options.user { - continue - } - orgEnvs[e.OrganizationID] = append(orgEnvs[e.OrganizationID], e) - } - for _, o := range data.orgs { - if options.org != "" && o.Name != options.org { - continue - } - groups = append(groups, orgGrouping{org: o, envs: orgEnvs[o.ID]}) - } - return groups, labelAll(imgLabeler(data.images), userLabeler(userIDMap), providerLabeler(providerIDMap)) -} - -func providerIDs(providers []coder.KubernetesProvider) map[string]coder.KubernetesProvider { - providerIDMap := make(map[string]coder.KubernetesProvider) - for _, p := range providers { - providerIDMap[p.ID] = p - } - return providerIDMap -} - -func aggregateByProvider(data entities, options resourceTopOptions) ([]groupable, envLabeler) { - var groups []groupable - providerIDMap := providerIDs(data.providers) - userIDMap := userIDs(data.users) - providerEnvs := make(map[string][]coder.Environment, len(data.providers)) - for _, e := range data.envs { - if options.provider != "" && providerIDMap[e.ResourcePoolID].Name != options.provider { - continue - } - providerEnvs[e.ResourcePoolID] = append(providerEnvs[e.ResourcePoolID], e) - } - for _, p := range data.providers { - if options.provider != "" && p.Name != options.provider { - continue - } - groups = append(groups, providerGrouping{provider: p, envs: providerEnvs[p.ID]}) - } - return groups, labelAll(imgLabeler(data.images), userLabeler(userIDMap)) // TODO: consider adding an org label here -} - -// groupable specifies a structure capable of being an aggregation group of environments (user, org, all). -type groupable interface { - header() string - environments() []coder.Environment -} - -type userGrouping struct { - user coder.User - envs []coder.Environment -} - -func (u userGrouping) environments() []coder.Environment { - return u.envs -} - -func (u userGrouping) header() string { - return fmt.Sprintf("%s\t(%s)", truncate(u.user.Name, 20, "..."), u.user.Email) -} - -type orgGrouping struct { - org coder.Organization - envs []coder.Environment -} - -func (o orgGrouping) environments() []coder.Environment { - return o.envs -} - -func (o orgGrouping) header() string { - plural := "s" - if len(o.org.Members) == 1 { - plural = "" - } - return fmt.Sprintf("%s\t(%v member%s)", truncate(o.org.Name, 20, "..."), len(o.org.Members), plural) -} - -type providerGrouping struct { - provider coder.KubernetesProvider - envs []coder.Environment -} - -func (p providerGrouping) environments() []coder.Environment { - return p.envs -} - -func (p providerGrouping) header() string { - return fmt.Sprintf("%s\t", truncate(p.provider.Name, 20, "...")) -} - -func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler, showEmptyGroups bool, sortBy string) error { - tabwriter := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) - defer func() { _ = tabwriter.Flush() }() - - var userResources []aggregatedResources - for _, group := range groups { - if !showEmptyGroups && len(group.environments()) < 1 { - continue - } - userResources = append(userResources, aggregatedResources{ - groupable: group, resources: aggregateEnvResources(group.environments()), - }) - } - - err := sortAggregatedResources(userResources, sortBy) - if err != nil { - return err - } - - for _, u := range userResources { - _, _ = fmt.Fprintf(tabwriter, "%s\t%s", u.header(), u.resources) - if verbose { - if len(u.environments()) > 0 { - _, _ = fmt.Fprintf(tabwriter, "\f") - } - for _, env := range u.environments() { - _, _ = fmt.Fprintf(tabwriter, "\t") - _, _ = fmt.Fprintln(tabwriter, fmtEnvResources(env, labeler)) - } - } - _, _ = fmt.Fprint(tabwriter, "\n") - } - if len(userResources) == 0 { - clog.LogInfo( - "no groups for the given filters exist with active environments", - clog.Tipf("run \"--show-empty\" to see groups with no resources."), - ) - } - return nil -} - -func sortAggregatedResources(resources []aggregatedResources, sortBy string) error { - const cpu = "cpu" - const memory = "memory" - switch sortBy { - case cpu: - sort.Slice(resources, func(i, j int) bool { - return resources[i].cpuAllocation > resources[j].cpuAllocation - }) - case memory: - sort.Slice(resources, func(i, j int) bool { - return resources[i].memAllocation > resources[j].memAllocation - }) - default: - return xerrors.Errorf("unknown --sort-by value of \"%s\"", sortBy) - } - for _, group := range resources { - envs := group.environments() - switch sortBy { - case cpu: - sort.Slice(envs, func(i, j int) bool { return envs[i].CPUCores > envs[j].CPUCores }) - case memory: - sort.Slice(envs, func(i, j int) bool { return envs[i].MemoryGB > envs[j].MemoryGB }) - default: - return xerrors.Errorf("unknown --sort-by value of \"%s\"", sortBy) - } - } - return nil -} - -type aggregatedResources struct { - groupable - resources -} - -func resourcesFromEnv(env coder.Environment) resources { - return resources{ - cpuAllocation: env.CPUCores, - cpuUtilization: env.LatestStat.CPUUsage, - memAllocation: env.MemoryGB, - memUtilization: env.LatestStat.MemoryUsage, - } -} - -func fmtEnvResources(env coder.Environment, labeler envLabeler) string { - return fmt.Sprintf("%s\t%s\t%s", truncate(env.Name, 20, "..."), resourcesFromEnv(env), labeler.label(env)) -} - -type envLabeler interface { - label(coder.Environment) string -} - -func labelAll(labels ...envLabeler) envLabeler { return multiLabeler(labels) } - -type multiLabeler []envLabeler - -func (m multiLabeler) label(e coder.Environment) string { - var str strings.Builder - for i, labeler := range m { - if i != 0 { - str.WriteString("\t") - } - str.WriteString(labeler.label(e)) - } - return str.String() -} - -type orgLabeler map[string]coder.Organization - -func (o orgLabeler) label(e coder.Environment) string { - return fmt.Sprintf("[org: %s]", o[e.OrganizationID].Name) -} - -type imgLabeler map[string]*coder.Image - -func (i imgLabeler) label(e coder.Environment) string { - return fmt.Sprintf("[img: %s:%s]", i[e.ImageID].Repository, e.ImageTag) -} - -type userLabeler map[string]coder.User - -func (u userLabeler) label(e coder.Environment) string { - return fmt.Sprintf("[user: %s]", u[e.UserID].Email) -} - -type providerLabeler map[string]coder.KubernetesProvider - -func (p providerLabeler) label(e coder.Environment) string { - return fmt.Sprintf("[provider: %s]", p[e.ResourcePoolID].Name) -} - -func aggregateEnvResources(envs []coder.Environment) resources { - var aggregate resources - for _, e := range envs { - aggregate.cpuAllocation += e.CPUCores - aggregate.cpuUtilization += e.LatestStat.CPUUsage - aggregate.memAllocation += e.MemoryGB - aggregate.memUtilization += e.LatestStat.MemoryUsage - } - return aggregate -} - -type resources struct { - cpuAllocation float32 - memAllocation float32 - - // TODO: consider using these - cpuUtilization float32 - memUtilization float32 -} - -func (a resources) String() string { - return fmt.Sprintf( - "[cpu: %.1f]\t[mem: %.1f GB]", - a.cpuAllocation, a.memAllocation, - ) -} - -//nolint:unparam -// truncate the given string and replace the removed chars with some replacement (ex: "..."). -func truncate(str string, max int, replace string) string { - if len(str) <= max { - return str - } - return str[:max+1] + replace -} diff --git a/internal/cmd/resourcemanager_test.go b/internal/cmd/resourcemanager_test.go deleted file mode 100644 index 50c9156c..00000000 --- a/internal/cmd/resourcemanager_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package cmd - -import ( - "bytes" - "flag" - "fmt" - "io/ioutil" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -var write = flag.Bool("write", false, "write to the golden files") - -func Test_resourceManager(t *testing.T) { - // TODO: cleanup - verbose = true - - const goldenFile = "resourcemanager_test.golden" - var buff bytes.Buffer - data := mockResourceTopEntities() - tests := []struct { - header string - data entities - options resourceTopOptions - }{ - { - header: "By User", - data: data, - options: resourceTopOptions{ - group: "user", - sortBy: "cpu", - }, - }, - { - header: "By Org", - data: data, - options: resourceTopOptions{ - group: "org", - sortBy: "cpu", - }, - }, - { - header: "By Provider", - data: data, - options: resourceTopOptions{ - group: "provider", - sortBy: "cpu", - }, - }, - { - header: "Sort By Memory", - data: data, - options: resourceTopOptions{ - group: "user", - sortBy: "memory", - }, - }, - } - - for _, tcase := range tests { - buff.WriteString(fmt.Sprintf("=== TEST: %s\n", tcase.header)) - err := presentEntites(&buff, tcase.data, tcase.options) - assert.Success(t, "present entities", err) - } - - assertGolden(t, goldenFile, buff.Bytes()) -} - -func assertGolden(t *testing.T, path string, output []byte) { - if *write { - err := ioutil.WriteFile(path, output, 0777) - assert.Success(t, "write file", err) - return - } - goldenContent, err := ioutil.ReadFile(path) - assert.Success(t, "read golden file", err) - assert.Equal(t, "golden content matches", string(goldenContent), string(output)) -} - -func mockResourceTopEntities() entities { - orgIDs := [...]string{randString(10), randString(10), randString(10)} - imageIDs := [...]string{randString(10), randString(10), randString(10)} - providerIDs := [...]string{randString(10), randString(10), randString(10)} - userIDs := [...]string{randString(10), randString(10), randString(10)} - envIDs := [...]string{randString(10), randString(10), randString(10), randString(10)} - - return entities{ - providers: []coder.KubernetesProvider{ - { - ID: providerIDs[0], - Name: "mars", - }, - { - ID: providerIDs[1], - Name: "underground", - }, - }, - users: []coder.User{ - { - ID: userIDs[0], - Name: "Random", - Email: "random@coder.com", - }, - { - ID: userIDs[1], - Name: "Second Random", - Email: "second-random@coder.com", - }, - }, - orgs: []coder.Organization{ - { - ID: orgIDs[0], - Name: "SpecialOrg", - - //! these should probably be fixed, but for now they are just for the count - Members: []coder.OrganizationUser{{}, {}}, - }, - { - ID: orgIDs[1], - Name: "NotSoSpecialOrg", - - //! these should probably be fixed, but for now they are just for the count - Members: []coder.OrganizationUser{{}, {}}, - }, - }, - envs: []coder.Environment{ - { - ID: envIDs[0], - ResourcePoolID: providerIDs[0], - ImageID: imageIDs[0], - OrganizationID: orgIDs[0], - UserID: userIDs[0], - Name: "dev-env", - ImageTag: "20.04", - CPUCores: 12.2, - MemoryGB: 64.4, - LatestStat: coder.EnvironmentStat{ - ContainerStatus: coder.EnvironmentOn, - }, - }, - { - ID: envIDs[1], - ResourcePoolID: providerIDs[1], - ImageID: imageIDs[1], - OrganizationID: orgIDs[1], - UserID: userIDs[1], - Name: "another-env", - ImageTag: "10.2", - CPUCores: 4, - MemoryGB: 16, - LatestStat: coder.EnvironmentStat{ - ContainerStatus: coder.EnvironmentOn, - }, - }, - { - ID: envIDs[2], - ResourcePoolID: providerIDs[1], - ImageID: imageIDs[1], - OrganizationID: orgIDs[1], - UserID: userIDs[1], - Name: "yet-another-env", - ImageTag: "10.2", - CPUCores: 100, - MemoryGB: 2, - LatestStat: coder.EnvironmentStat{ - ContainerStatus: coder.EnvironmentOn, - }, - }, - }, - images: map[string]*coder.Image{ - imageIDs[0]: { - Repository: "ubuntu", - OrganizationID: orgIDs[0], - }, - imageIDs[1]: { - Repository: "archlinux", - OrganizationID: orgIDs[0], - }, - }, - } -} diff --git a/internal/cmd/resourcemanager_test.golden b/internal/cmd/resourcemanager_test.golden deleted file mode 100755 index 0707bd1a..00000000 --- a/internal/cmd/resourcemanager_test.golden +++ /dev/null @@ -1,32 +0,0 @@ -=== TEST: By User -Second Random (second-random@coder.com) [cpu: 104.0] [mem: 18.0 GB] - yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - -Random (random@coder.com) [cpu: 12.2] [mem: 64.4 GB] - dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] - -=== TEST: By Org -NotSoSpecialOrg (2 members) [cpu: 104.0] [mem: 18.0 GB] - yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] - another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] [provider: underground] - -SpecialOrg (2 members) [cpu: 12.2] [mem: 64.4 GB] - dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] [provider: mars] - -=== TEST: By Provider -underground [cpu: 104.0] [mem: 18.0 GB] - yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] - another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [user: second-random@coder.com] - -mars [cpu: 12.2] [mem: 64.4 GB] - dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [user: random@coder.com] - -=== TEST: Sort By Memory -Random (random@coder.com) [cpu: 12.2] [mem: 64.4 GB] - dev-env [cpu: 12.2] [mem: 64.4 GB] [img: ubuntu:20.04] [provider: mars] [org: SpecialOrg] - -Second Random (second-random@coder.com) [cpu: 104.0] [mem: 18.0 GB] - another-env [cpu: 4.0] [mem: 16.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - yet-another-env [cpu: 100.0] [mem: 2.0 GB] [img: archlinux:10.2] [provider: underground] [org: NotSoSpecialOrg] - diff --git a/internal/cmd/ssh.go b/internal/cmd/ssh.go deleted file mode 100644 index 18131717..00000000 --- a/internal/cmd/ssh.go +++ /dev/null @@ -1,117 +0,0 @@ -package cmd - -import ( - "fmt" - "net/url" - "os" - "os/exec" - "os/user" - "path/filepath" - - "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" -) - -var ( - showInteractiveOutput = terminal.IsTerminal(int(os.Stdout.Fd())) -) - -func sshCmd() *cobra.Command { - cmd := cobra.Command{ - Use: "ssh [environment_name] []", - Short: "Enter a shell of execute a command over SSH into a Coder environment", - Args: shValidArgs, - Example: `coder ssh my-dev -coder ssh my-dev pwd`, - Aliases: []string{"sh"}, - DisableFlagParsing: true, - DisableFlagsInUseLine: true, - RunE: shell, - } - return &cmd -} - -func shell(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - me, err := client.Me(ctx) - if err != nil { - return err - } - env, err := findEnv(ctx, client, args[0], coder.Me) - if err != nil { - return err - } - if env.LatestStat.ContainerStatus != coder.EnvironmentOn { - return clog.Error("environment not available", - fmt.Sprintf("current status: \"%s\"", env.LatestStat.ContainerStatus), - clog.BlankLine, - clog.Tipf("use \"coder envs rebuild %s\" to rebuild this environment", env.Name), - ) - } - wp, err := client.WorkspaceProviderByID(ctx, env.ResourcePoolID) - if err != nil { - return err - } - u, err := url.Parse(wp.EnvproxyAccessURL) - if err != nil { - return err - } - - usr, err := user.Current() - if err != nil { - return xerrors.Errorf("get user home directory: %w", err) - } - privateKeyFilepath := filepath.Join(usr.HomeDir, ".ssh", "coder_enterprise") - - err = writeSSHKey(ctx, client, privateKeyFilepath) - if err != nil { - return err - } - ssh := exec.CommandContext(ctx, - "ssh", "-i"+privateKeyFilepath, - fmt.Sprintf("%s-%s@%s", me.Username, env.Name, u.Hostname()), - ) - if len(args) > 1 { - ssh.Args = append(ssh.Args, args[1:]...) - } - ssh.Stderr = os.Stderr - ssh.Stdout = os.Stdout - ssh.Stdin = os.Stdin - err = ssh.Run() - var exitErr *exec.ExitError - if xerrors.As(err, &exitErr) { - os.Exit(exitErr.ExitCode()) - return xerrors.New("unreachable") - } - return err -} - -// special handling for the common case of "coder sh" input without a positional argument. -func shValidArgs(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - err := cobra.MinimumNArgs(1)(cmd, args) - if err != nil { - client, err := newClient(ctx) - if err != nil { - return clog.Error("missing [environment_name] argument") - } - _, haystack, err := searchForEnv(ctx, client, "", coder.Me) - if err != nil { - return clog.Error("missing [environment_name] argument", - fmt.Sprintf("specify one of %q", haystack), - clog.BlankLine, - clog.Tipf("run \"coder envs ls\" to view your environments"), - ) - } - return clog.Error("missing [environment_name] argument") - } - return nil -} diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go deleted file mode 100644 index fc059ac0..00000000 --- a/internal/cmd/sync.go +++ /dev/null @@ -1,121 +0,0 @@ -package cmd - -import ( - "bytes" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/sync" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" -) - -func syncCmd() *cobra.Command { - var init bool - cmd := &cobra.Command{ - Use: "sync [local directory] [:]", - Short: "Establish a one way directory sync to a Coder environment", - Args: xcobra.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 ( - ctx = cmd.Context() - local = args[0] - remote = args[1] - ) - - client, err := newClient(ctx) - if err != nil { - return err - } - - remoteTokens := strings.SplitN(remote, ":", 2) - if len(remoteTokens) != 2 { - return xerrors.New("remote malformatted") - } - var ( - envName = remoteTokens[0] - remoteDir = remoteTokens[1] - ) - - env, err := findEnv(ctx, client, envName, coder.Me) - if err != nil { - return err - } - - info, err := os.Stat(local) - if err != nil { - return err - } - if info.Mode().IsRegular() { - return sync.SingleFile(ctx, local, remoteDir, env, client) - } - if !info.IsDir() { - return xerrors.Errorf("local path must lead to a regular file or directory: %w", 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, - OutW: cmd.OutOrStdout(), - ErrW: cmd.ErrOrStderr(), - InputReader: cmd.InOrStdin(), - IsInteractiveOutput: showInteractiveOutput, - } - - localVersion := rsyncVersion() - remoteVersion, rsyncErr := s.Version() - - if rsyncErr != nil { - clog.LogInfo("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/tags.go b/internal/cmd/tags.go deleted file mode 100644 index 91d7ba19..00000000 --- a/internal/cmd/tags.go +++ /dev/null @@ -1,173 +0,0 @@ -package cmd - -import ( - "encoding/json" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func tagsCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "tags", - Hidden: true, - Short: "operate on Coder image tags", - } - - cmd.AddCommand( - tagsLsCmd(), - tagsCreateCmd(), - tagsRmCmd(), - ) - return cmd -} - -func tagsCreateCmd() *cobra.Command { - var ( - orgName string - imageName string - defaultTag bool - ) - cmd := &cobra.Command{ - Use: "create [tag]", - Short: "add an image tag", - Long: "allow users to create environments with this image tag", - Example: `coder tags create latest --image ubuntu --org default`, - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - img, err := findImg(ctx, client, findImgConf{ - orgName: orgName, - imgName: imageName, - email: coder.Me, - }) - if err != nil { - return xerrors.Errorf("find image: %w", err) - } - - _, err = client.CreateImageTag(ctx, img.ID, coder.CreateImageTagReq{ - Tag: args[0], - Default: defaultTag, - }) - if err != nil { - return xerrors.Errorf("create image tag: %w", err) - } - clog.LogSuccess("created new tag") - - return nil - }, - } - - cmd.Flags().StringVarP(&imageName, "image", "i", "", "image name") - cmd.Flags().StringVarP(&orgName, "org", "o", "", "organization name") - cmd.Flags().BoolVar(&defaultTag, "default", false, "make this tag the default for its image") - _ = cmd.MarkFlagRequired("org") - _ = cmd.MarkFlagRequired("image") - return cmd -} - -func tagsLsCmd() *cobra.Command { - var ( - orgName string - imageName string - outputFmt string - ) - cmd := &cobra.Command{ - Use: "ls", - Example: `coder tags ls --image ubuntu --org default --output json`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - - img, err := findImg(ctx, client, findImgConf{ - email: coder.Me, - orgName: orgName, - imgName: imageName, - }) - if err != nil { - return err - } - - tags, err := client.ImageTags(ctx, img.ID) - if err != nil { - return err - } - - switch outputFmt { - case humanOutput: - err = tablewriter.WriteTable(cmd.OutOrStdout(), len(tags), func(i int) interface{} { return tags[i] }) - if err != nil { - return err - } - case jsonOutput: - err := json.NewEncoder(cmd.OutOrStdout()).Encode(tags) - if err != nil { - return err - } - default: - return clog.Error("unknown --output value") - } - - return nil - }, - } - cmd.Flags().StringVar(&orgName, "org", "", "organization by name") - cmd.Flags().StringVarP(&imageName, "image", "i", "", "image by name") - cmd.Flags().StringVar(&outputFmt, "output", humanOutput, "output format (human|json)") - _ = cmd.MarkFlagRequired("image") - _ = cmd.MarkFlagRequired("org") - return cmd -} - -func tagsRmCmd() *cobra.Command { - var ( - imageName string - orgName string - ) - cmd := &cobra.Command{ - Use: "rm [tag]", - Short: "remove an image tag", - Example: `coder tags rm latest --image ubuntu --org default`, - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - - img, err := findImg(ctx, client, findImgConf{ - email: coder.Me, - imgName: imageName, - orgName: orgName, - }) - if err != nil { - return err - } - - if err = client.DeleteImageTag(ctx, img.ID, args[0]); err != nil { - return err - } - clog.LogSuccess("removed tag") - - return nil - }, - } - cmd.Flags().StringVarP(&orgName, "org", "o", "", "organization by name") - cmd.Flags().StringVarP(&imageName, "image", "i", "", "image by name") - _ = cmd.MarkFlagRequired("image") - _ = cmd.MarkFlagRequired("org") - return cmd -} diff --git a/internal/cmd/tags_test.go b/internal/cmd/tags_test.go deleted file mode 100644 index d074f43b..00000000 --- a/internal/cmd/tags_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package cmd - -import ( - "context" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/coder-sdk" -) - -func Test_tags(t *testing.T) { - t.Skip("TODO: wait for dedicated test API server / DB so we can create an org") - ctx := context.Background() - - skipIfNoAuth(t) - - res := execute(t, nil, "tags", "ls") - res.error(t) - - ensureImageImported(ctx, t, testCoderClient, "ubuntu") - - res = execute(t, nil, "tags", "ls", "--image=ubuntu", "--org=default") - res.success(t) - - var tags []coder.ImageTag - res = execute(t, nil, "tags", "ls", "--image=ubuntu", "--org=default", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &tags) - assert.True(t, "> 0 tags", len(tags) > 0) -} diff --git a/internal/cmd/tokens.go b/internal/cmd/tokens.go deleted file mode 100644 index 21fd478f..00000000 --- a/internal/cmd/tokens.go +++ /dev/null @@ -1,136 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func tokensCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "tokens", - Short: "manage Coder API tokens for the active user", - Long: "Create and manage API Tokens for authenticating the CLI.\n" + - "Statically authenticate using the token value with the " + "`" + "CODER_TOKEN" + "`" + " and " + "`" + "CODER_URL" + "`" + " environment variables.", - } - cmd.AddCommand( - lsTokensCmd(), - createTokensCmd(), - rmTokenCmd(), - regenTokenCmd(), - ) - return cmd -} - -func lsTokensCmd() *cobra.Command { - var outputFmt string - - cmd := &cobra.Command{ - Use: "ls", - Short: "show the user's active API tokens", - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - - tokens, err := client.APITokens(ctx, coder.Me) - if err != nil { - return err - } - - switch outputFmt { - case humanOutput: - err := tablewriter.WriteTable(cmd.OutOrStdout(), len(tokens), func(i int) interface{} { - return tokens[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - case jsonOutput: - err := json.NewEncoder(cmd.OutOrStdout()).Encode(tokens) - if err != nil { - return xerrors.Errorf("write tokens as JSON: %w", err) - } - default: - return xerrors.Errorf("unknown --output value %q", outputFmt) - } - - return nil - }, - } - - cmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "human | json") - - return cmd -} - -func createTokensCmd() *cobra.Command { - return &cobra.Command{ - Use: "create [token_name]", - Short: "create generates a new API token and prints it to stdout", - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - token, err := client.CreateAPIToken(ctx, coder.Me, coder.CreateAPITokenReq{ - Name: args[0], - }) - if err != nil { - return err - } - fmt.Println(token) - return nil - }, - } -} - -func rmTokenCmd() *cobra.Command { - return &cobra.Command{ - Use: "rm [token_id]", - Short: "remove an API token by its unique ID", - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - if err = client.DeleteAPIToken(ctx, coder.Me, args[0]); err != nil { - return err - } - return nil - }, - } -} - -func regenTokenCmd() *cobra.Command { - return &cobra.Command{ - Use: "regen [token_id]", - Short: "regenerate an API token by its unique ID and print the new token to stdout", - Args: xcobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - token, err := client.RegenerateAPIToken(ctx, coder.Me, args[0]) - if err != nil { - return nil - } - fmt.Println(token) - return nil - }, - } -} diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go deleted file mode 100644 index 94f90357..00000000 --- a/internal/cmd/urls.go +++ /dev/null @@ -1,267 +0,0 @@ -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "regexp" - "strconv" - "strings" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func urlCmd() *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: xcobra.ExactArgs(1), - RunE: listDevURLsCmd(&outputFmt), - } - lsCmd.Flags().StringVarP(&outputFmt, "output", "o", humanOutput, "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, - createDevURLCmd(), - ) - - return cmd -} - -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 { - clog.Log(clog.Error("invalid port")) - return 0, err - } - if p < 1 { - // Port 0 means 'any free port', which we don't support. - return 0, xerrors.New("Port must be > 0") - } - return int(p), nil -} - -func accessLevelIsValid(level string) bool { - _, ok := urlAccessLevel[level] - if !ok { - clog.Log(clog.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 listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - envName := args[0] - - devURLs, err := urlList(ctx, client, envName) - if err != nil { - return err - } - - switch *outputFmt { - case humanOutput: - if len(devURLs) < 1 { - clog.LogInfo(fmt.Sprintf("no devURLs found for environment %q", envName)) - return nil - } - err := tablewriter.WriteTable(cmd.OutOrStdout(), len(devURLs), func(i int) interface{} { - return devURLs[i] - }) - if err != nil { - return xerrors.Errorf("write table: %w", err) - } - case jsonOutput: - if err := json.NewEncoder(cmd.OutOrStdout()).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 createDevURLCmd() *cobra.Command { - var ( - access string - urlname string - scheme string - ) - cmd := &cobra.Command{ - Use: "create [env_name] [port]", - Short: "Create a new devurl for an environment", - Aliases: []string{"edit"}, - Args: xcobra.ExactArgs(2), - // Run creates or updates a devURL - RunE: func(cmd *cobra.Command, args []string) error { - var ( - envName = args[0] - port = args[1] - ctx = cmd.Context() - ) - - 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(ctx) - if err != nil { - return err - } - - env, err := findEnv(ctx, client, envName, coder.Me) - if err != nil { - return err - } - - urls, err := urlList(ctx, client, envName) - if err != nil { - return err - } - - urlID, found := devURLID(portNum, urls) - if found { - err := client.PutDevURL(ctx, env.ID, urlID, coder.PutDevURLReq{ - Port: portNum, - Name: urlname, - Access: access, - EnvID: env.ID, - Scheme: scheme, - }) - if err != nil { - return xerrors.Errorf("update DevURL: %w", err) - } - clog.LogSuccess(fmt.Sprintf("patched devurl for port %s", port)) - } else { - err := client.CreateDevURL(ctx, env.ID, coder.CreateDevURLReq{ - Port: portNum, - Name: urlname, - Access: access, - EnvID: env.ID, - Scheme: scheme, - }) - if err != nil { - return xerrors.Errorf("insert DevURL: %w", err) - } - clog.LogSuccess(fmt.Sprintf("created devurl for port %s", port)) - } - return nil - }, - } - - cmd.Flags().StringVar(&access, "access", "private", "Set DevURL access to [private | org | authed | public]") - cmd.Flags().StringVar(&urlname, "name", "", "DevURL name") - cmd.Flags().StringVar(&scheme, "scheme", "http", "Server scheme (http|https)") - _ = 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 []coder.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] - ctx = cmd.Context() - ) - - portNum, err := validatePort(port) - if err != nil { - return xerrors.Errorf("validate port: %w", err) - } - - client, err := newClient(ctx) - if err != nil { - return err - } - env, err := findEnv(ctx, client, envName, coder.Me) - if err != nil { - return err - } - - urls, err := urlList(ctx, client, envName) - if err != nil { - return err - } - - urlID, found := devURLID(portNum, urls) - if found { - clog.LogInfo(fmt.Sprintf("deleting devurl for port %v", port)) - } else { - return xerrors.Errorf("No devurl found for port %v", port) - } - - if err := client.DeleteDevURL(ctx, 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, client coder.Client, envName string) ([]coder.DevURL, error) { - env, err := findEnv(ctx, client, envName, coder.Me) - if err != nil { - return nil, err - } - return client.DevURLs(ctx, env.ID) -} diff --git a/internal/cmd/users.go b/internal/cmd/users.go deleted file mode 100644 index 03929366..00000000 --- a/internal/cmd/users.go +++ /dev/null @@ -1,61 +0,0 @@ -package cmd - -import ( - "encoding/json" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/pkg/tablewriter" -) - -func usersCmd() *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", humanOutput, "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 { - ctx := cmd.Context() - client, err := newClient(ctx) - if err != nil { - return err - } - - users, err := client.Users(ctx) - if err != nil { - return xerrors.Errorf("get users: %w", err) - } - - switch *outputFmt { - case humanOutput: - // For each element, return the user. - each := func(i int) interface{} { return users[i] } - if err := tablewriter.WriteTable(cmd.OutOrStdout(), len(users), each); err != nil { - return xerrors.Errorf("write table: %w", err) - } - case "json": - if err := json.NewEncoder(cmd.OutOrStdout()).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/cmd/users_test.go b/internal/cmd/users_test.go deleted file mode 100644 index a82f4607..00000000 --- a/internal/cmd/users_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "testing" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - - "cdr.dev/coder-cli/coder-sdk" -) - -func Test_users(t *testing.T) { - skipIfNoAuth(t) - - var users []coder.User - res := execute(t, nil, "users", "ls", "--output=json") - res.success(t) - res.stdoutUnmarshals(t, &users) - assertAdmin(t, users) - - res = execute(t, nil, "users", "ls", "--output=human") - res.success(t) -} - -func assertAdmin(t *testing.T, users []coder.User) { - for _, u := range users { - if u.Username == "admin" { - return - } - } - slogtest.Fatal(t, "did not find admin user", slog.F("users", users)) -} diff --git a/internal/coderutil/doc.go b/internal/coderutil/doc.go deleted file mode 100644 index 5a7d8e14..00000000 --- a/internal/coderutil/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package coderutil providers utilities for high-level operations on coder-sdk entities. -package coderutil diff --git a/internal/coderutil/env.go b/internal/coderutil/env.go deleted file mode 100644 index 9485347f..00000000 --- a/internal/coderutil/env.go +++ /dev/null @@ -1,162 +0,0 @@ -package coderutil - -import ( - "context" - "fmt" - "net/url" - "sync" - - "golang.org/x/xerrors" - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" -) - -// DialEnvWsep dials the executor endpoint using the https://github.com/cdr/wsep message protocol. -// The proper workspace provider envproxy access URL is used. -func DialEnvWsep(ctx context.Context, client coder.Client, env *coder.Environment) (*websocket.Conn, error) { - workspaceProvider, err := client.WorkspaceProviderByID(ctx, env.ResourcePoolID) - if err != nil { - return nil, xerrors.Errorf("get env workspace provider: %w", err) - } - accessURL, err := url.Parse(workspaceProvider.EnvproxyAccessURL) - if err != nil { - return nil, xerrors.Errorf("invalid workspace provider envproxy access url: %w", err) - } - - conn, err := client.DialWsep(ctx, accessURL, env.ID) - if err != nil { - return nil, xerrors.Errorf("dial websocket: %w", err) - } - return conn, nil -} - -// EnvWithWorkspaceProvider composes an Environment entity with its associated WorkspaceProvider. -type EnvWithWorkspaceProvider struct { - Env coder.Environment - WorkspaceProvider coder.KubernetesProvider -} - -// EnvsWithProvider performs the composition of each Environment with its associated WorkspaceProvider. -func EnvsWithProvider(ctx context.Context, client coder.Client, envs []coder.Environment) ([]EnvWithWorkspaceProvider, error) { - pooledEnvs := make([]EnvWithWorkspaceProvider, 0, len(envs)) - providers, err := client.WorkspaceProviders(ctx) - if err != nil { - return nil, err - } - providerMap := make(map[string]coder.KubernetesProvider, len(providers.Kubernetes)) - for _, p := range providers.Kubernetes { - providerMap[p.ID] = p - } - for _, e := range envs { - envProvider, ok := providerMap[e.ResourcePoolID] - if !ok { - return nil, xerrors.Errorf("fetch env workspace provider: %w", coder.ErrNotFound) - } - pooledEnvs = append(pooledEnvs, EnvWithWorkspaceProvider{ - Env: e, - WorkspaceProvider: envProvider, - }) - } - return pooledEnvs, nil -} - -// DefaultWorkspaceProvider returns the default provider with which to create environments. -func DefaultWorkspaceProvider(ctx context.Context, c coder.Client) (*coder.KubernetesProvider, error) { - provider, err := c.WorkspaceProviders(ctx) - if err != nil { - return nil, err - } - for _, p := range provider.Kubernetes { - if p.BuiltIn { - return &p, nil - } - } - return nil, coder.ErrNotFound -} - -// EnvTable defines an Environment-like structure with associated entities composed in a human -// readable form. -type EnvTable struct { - Name string `table:"Name"` - Image string `table:"Image"` - CPU float32 `table:"vCPU"` - MemoryGB float32 `table:"MemoryGB"` - DiskGB int `table:"DiskGB"` - Status string `table:"Status"` - Provider string `table:"Provider"` - CVM bool `table:"CVM"` -} - -// EnvsHumanTable performs the composition of each Environment with its associated ProviderName and ImageRepo. -func EnvsHumanTable(ctx context.Context, client coder.Client, envs []coder.Environment) ([]EnvTable, error) { - imageMap, err := MakeImageMap(ctx, client, envs) - if err != nil { - return nil, err - } - - pooledEnvs := make([]EnvTable, 0, len(envs)) - providers, err := client.WorkspaceProviders(ctx) - if err != nil { - return nil, err - } - providerMap := make(map[string]coder.KubernetesProvider, len(providers.Kubernetes)) - for _, p := range providers.Kubernetes { - providerMap[p.ID] = p - } - for _, e := range envs { - envProvider, ok := providerMap[e.ResourcePoolID] - if !ok { - return nil, xerrors.Errorf("fetch env workspace provider: %w", coder.ErrNotFound) - } - pooledEnvs = append(pooledEnvs, EnvTable{ - Name: e.Name, - Image: fmt.Sprintf("%s:%s", imageMap[e.ImageID].Repository, e.ImageTag), - CPU: e.CPUCores, - MemoryGB: e.MemoryGB, - DiskGB: e.DiskGB, - Status: string(e.LatestStat.ContainerStatus), - Provider: envProvider.Name, - CVM: e.UseContainerVM, - }) - } - return pooledEnvs, nil -} - -// MakeImageMap fetches all image entities specified in the slice of environments, then places them into an ID map. -func MakeImageMap(ctx context.Context, client coder.Client, envs []coder.Environment) (map[string]*coder.Image, error) { - var ( - mu sync.Mutex - egroup = clog.LoggedErrGroup() - ) - imageMap := make(map[string]*coder.Image) - for _, e := range envs { - // put all the image IDs into a map to remove duplicates - imageMap[e.ImageID] = nil - } - ids := make([]string, 0, len(imageMap)) - for id := range imageMap { - // put the deduplicated back into a slice - // so we can write to the map while iterating - ids = append(ids, id) - } - for _, id := range ids { - id := id - egroup.Go(func() error { - img, err := client.ImageByID(ctx, id) - if err != nil { - return err - } - mu.Lock() - defer mu.Unlock() - imageMap[id] = img - - return nil - }) - } - if err := egroup.Wait(); err != nil { - return nil, err - } - return imageMap, nil -} diff --git a/internal/coderutil/provider.go b/internal/coderutil/provider.go deleted file mode 100644 index 5364add8..00000000 --- a/internal/coderutil/provider.go +++ /dev/null @@ -1,21 +0,0 @@ -package coderutil - -import ( - "context" - - "cdr.dev/coder-cli/coder-sdk" -) - -// ProviderByName searches linearly for a workspace provider by its name. -func ProviderByName(ctx context.Context, client coder.Client, name string) (*coder.KubernetesProvider, error) { - providers, err := client.WorkspaceProviders(ctx) - if err != nil { - return nil, err - } - for _, p := range providers.Kubernetes { - if p.Name == name { - return &p, nil - } - } - return nil, coder.ErrNotFound -} diff --git a/internal/config/dir.go b/internal/config/dir.go deleted file mode 100644 index aff69fca..00000000 --- a/internal/config/dir.go +++ /dev/null @@ -1,52 +0,0 @@ -package config - -import ( - "io/ioutil" - "os" - "path/filepath" - - "github.com/kirsle/configdir" -) - -var configRoot = configdir.LocalConfig("coder") - -// SetRoot overrides the package-level config root configuration. -func SetRoot(root string) { - configRoot = root -} - -// 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(configRoot, 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(configRoot, 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/sync/eventcache.go b/internal/sync/eventcache.go deleted file mode 100644 index 1073b123..00000000 --- a/internal/sync/eventcache.go +++ /dev/null @@ -1,61 +0,0 @@ -package sync - -import ( - "os" - "time" - - "github.com/rjeczalik/notify" -) - -type timedEvent struct { - CreatedAt time.Time - notify.EventInfo -} - -type eventCache map[string]timedEvent - -func (cache eventCache) Add(ev timedEvent) { - lastEvent, ok := cache[ev.Path()] - if ok { - // If the file was quickly created and then destroyed, pretend nothing ever happened. - if lastEvent.Event() == notify.Create && ev.Event() == notify.Remove { - delete(cache, ev.Path()) - return - } - } - // 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/singlefile.go b/internal/sync/singlefile.go deleted file mode 100644 index 0e15e354..00000000 --- a/internal/sync/singlefile.go +++ /dev/null @@ -1,60 +0,0 @@ -package sync - -import ( - "context" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "cdr.dev/wsep" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/coderutil" -) - -// SingleFile copies the given file into the remote dir or remote path of the given coder.Environment. -func SingleFile(ctx context.Context, local, remoteDir string, env *coder.Environment, client coder.Client) error { - conn, err := coderutil.DialEnvWsep(ctx, client, env) - if err != nil { - return xerrors.Errorf("dial remote execer: %w", err) - } - defer func() { _ = conn.Close(websocket.StatusNormalClosure, "normal closure") }() - - if strings.HasSuffix(remoteDir, string(filepath.Separator)) { - remoteDir += filepath.Base(local) - } - - execer := wsep.RemoteExecer(conn) - cmd := fmt.Sprintf(`[ -d %s ] && cat > %s/%s || cat > %s`, remoteDir, remoteDir, filepath.Base(local), remoteDir) - process, err := execer.Start(ctx, wsep.Command{ - Command: "sh", - Args: []string{"-c", cmd}, - Stdin: true, - }) - if err != nil { - return xerrors.Errorf("start sync command: %w", err) - } - - sourceFile, err := os.Open(local) - if err != nil { - return xerrors.Errorf("open source file: %w", err) - } - - go func() { _, _ = io.Copy(ioutil.Discard, process.Stdout()) }() - go func() { _, _ = io.Copy(ioutil.Discard, process.Stderr()) }() - go func() { - stdin := process.Stdin() - defer stdin.Close() - _, _ = io.Copy(stdin, sourceFile) - }() - - if err := process.Wait(); err != nil { - return xerrors.Errorf("copy process: %w", err) - } - return nil -} diff --git a/internal/sync/sync.go b/internal/sync/sync.go deleted file mode 100644 index 1d356f0f..00000000 --- a/internal/sync/sync.go +++ /dev/null @@ -1,393 +0,0 @@ -// Package sync contains logic for establishing a file sync between a local machine and a Coder environment. -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" - - "cdr.dev/wsep" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/activity" - "cdr.dev/coder-cli/internal/coderutil" - "cdr.dev/coder-cli/pkg/clog" -) - -// 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 - OutW io.Writer - ErrW io.Writer - InputReader io.Reader - IsInteractiveOutput bool -} - -// 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 = s.OutW - cmd.Stderr = ioutil.Discard - cmd.Stdin = s.InputReader - - if err := cmd.Run(); err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - switch { - case exitError.ExitCode() == rsyncExitCodeIncompat: - return xerrors.Errorf("no compatible rsync on remote machine: rsync: %w", err) - case exitError.ExitCode() == rsyncExitCodeDataStream: - return xerrors.Errorf("protocol datastream error or no remote rsync found: %w", err) - } - 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 := coderutil.DialEnvWsep(ctx, s.Client, &s.Env) - if err != nil { - return xerrors.Errorf("dial executor: %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(s.OutW, process.Stdout()) }() // Best effort. - go func() { _, _ = io.Copy(s.ErrW, 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 { - clog.LogInfo(fmt.Sprintf("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 - } - clog.LogSuccess( - fmt.Sprintf("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: - clog.LogInfo(fmt.Sprintf("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 { - clog.Log(clog.Error(fmt.Sprintf("%s: %s", log, err))) - } else { - clog.LogSuccess(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()), s.IsInteractiveOutput) - - 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 := coderutil.DialEnvWsep(ctx, s.Client, &s.Env) - if err != nil { - return "", xerrors.Errorf("dial env executor: %w", 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", s.IsInteractiveOutput) - if err := s.initSync(); err != nil { - return err - } - - if s.Init { - return nil - } - - clog.LogInfo(fmt.Sprintf("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 { - clog.LogInfo("dropped event, sync should restart soon") - } - } - } - }() - - var eventGroup []timedEvent - - dispatchEventGroup := time.NewTicker(maxAcceptableDispatch) - defer dispatchEventGroup.Stop() - for { - const watchingFilesystemTitle = "🛰 watching filesystem" - setConsoleTitle(watchingFilesystemTitle, s.IsInteractiveOutput) - - 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 ae7630d8..00000000 --- a/internal/sync/title.go +++ /dev/null @@ -1,17 +0,0 @@ -package sync - -import ( - "fmt" - "path/filepath" -) - -func setConsoleTitle(title string, isInteractiveOutput bool) { - if !isInteractiveOutput { - return - } - fmt.Printf("\033]0;%s\007", title) -} - -func fmtUpdateTitle(path string) string { - return "🚀 updating " + filepath.Base(path) -} diff --git a/internal/version/version.go b/internal/version/version.go deleted file mode 100644 index ce1d5de9..00000000 --- a/internal/version/version.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package version contains the compile-time injected version string and -// related utiliy methods. -package version - -import ( - "strings" -) - -// Version is populated at compile-time with the current coder-cli version. -var Version string = "unknown" - -// VersionsMatch compares the given APIVersion to the compile-time injected coder-cli version. -func VersionsMatch(apiVersion string) bool { - withoutPatchRelease := strings.Split(Version, ".") - if len(withoutPatchRelease) < 3 { - return false - } - majorMinor := strings.Join(withoutPatchRelease[:2], ".") - return strings.HasPrefix(strings.TrimPrefix(apiVersion, "v"), strings.TrimPrefix(majorMinor, "v")) -} diff --git a/internal/version/version_test.go b/internal/version/version_test.go deleted file mode 100644 index e51b64dd..00000000 --- a/internal/version/version_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package version - -import ( - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" -) - -func TestVersion(t *testing.T) { - Version = "1.12.1" - match := VersionsMatch("1.12.2") - assert.True(t, "versions match", match) - - Version = "v1.14.1" - match = VersionsMatch("1.15.2") - assert.True(t, "versions do not match", !match) - - Version = "v1.15.4" - match = VersionsMatch("1.15.2") - assert.True(t, "versions do match", match) - - Version = "1.15.4" - match = VersionsMatch("v1.15.2") - assert.True(t, "versions do match", match) - - Version = "1.15.4" - match = VersionsMatch("v2.15.2") - assert.True(t, "versions do not match", !match) - - Version = "1.12.2+cli.rc1" - match = VersionsMatch("v1.12.9") - assert.True(t, "versions do match", match) -} diff --git a/internal/x/xcobra/cobra.go b/internal/x/xcobra/cobra.go deleted file mode 100644 index 7ddfd9e2..00000000 --- a/internal/x/xcobra/cobra.go +++ /dev/null @@ -1,25 +0,0 @@ -// Package xcobra wraps the cobra package to provide richer functionality. -package xcobra - -import ( - "fmt" - - "github.com/spf13/cobra" - - "cdr.dev/coder-cli/pkg/clog" -) - -// ExactArgs returns an error if there are not exactly n args. -func ExactArgs(n int) cobra.PositionalArgs { - return func(cmd *cobra.Command, args []string) error { - if len(args) != n { - return clog.Error( - fmt.Sprintf("accepts %d arg(s), received %d", n, len(args)), - clog.Bold("usage: ")+cmd.UseLine(), - clog.BlankLine, - clog.Tipf("use \"--help\" for more info"), - ) - } - return nil - } -} diff --git a/internal/x/xsync/doc.go b/internal/x/xsync/doc.go deleted file mode 100644 index fb23bcce..00000000 --- a/internal/x/xsync/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package xsync provides utilities for concurrency. -package xsync diff --git a/internal/x/xsync/syncwriter.go b/internal/x/xsync/syncwriter.go deleted file mode 100644 index 3a26f262..00000000 --- a/internal/x/xsync/syncwriter.go +++ /dev/null @@ -1,24 +0,0 @@ -package xsync - -import ( - "io" - "sync" -) - -// Writer synchronizes concurrent writes to an underlying writer. -func Writer(w io.Writer) io.Writer { - return &writer{ - w: w, - } -} - -type writer struct { - mu sync.Mutex - w io.Writer -} - -func (sw *writer) Write(b []byte) (int, error) { - sw.mu.Lock() - defer sw.mu.Unlock() - return sw.w.Write(b) -} 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 2d420bf1..00000000 --- a/internal/x/xterminal/terminal.go +++ /dev/null @@ -1,24 +0,0 @@ -// +build !windows - -package xterminal - -import ( - "golang.org/x/crypto/ssh/terminal" -) - -// State differs per-platform. -type State struct { - s *terminal.State -} - -// 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) -} diff --git a/internal/x/xterminal/terminal_windows.go b/internal/x/xterminal/terminal_windows.go deleted file mode 100644 index a016e5a7..00000000 --- a/internal/x/xterminal/terminal_windows.go +++ /dev/null @@ -1,48 +0,0 @@ -// +build windows - -package xterminal - -import ( - "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 -} - -// 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) -} diff --git a/pkg/clog/clog.go b/pkg/clog/clog.go deleted file mode 100644 index 0a523e1f..00000000 --- a/pkg/clog/clog.go +++ /dev/null @@ -1,136 +0,0 @@ -package clog - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - - "github.com/fatih/color" - "golang.org/x/xerrors" -) - -var writer io.Writer = os.Stderr - -// SetOutput sets the package-level writer target for log functions. -func SetOutput(w io.Writer) { - writer = w -} - -// CLIMessage provides a human-readable message for CLI errors and messages. -type CLIMessage struct { - Level string - Color color.Attribute - Header string - Lines []string -} - -// CLIError wraps a CLIMessage and allows consumers to treat it as a normal error. -type CLIError struct { - CLIMessage - error -} - -// String formats the CLI message for consumption by a human. -func (m CLIMessage) String() string { - var str strings.Builder - str.WriteString(fmt.Sprintf("%s: %s\n", - color.New(m.Color).Sprint(m.Level), - color.New(color.Bold).Sprint(m.Header)), - ) - for _, line := range m.Lines { - str.WriteString(fmt.Sprintf(" %s %s\n", color.New(m.Color).Sprint("|"), line)) - } - return str.String() -} - -// Log logs the given error to stderr, defaulting to "fatal" if the error is not a CLIError. -// If the error is a CLIError, the plain error chain is ignored and the CLIError -// is logged on its own. -func Log(err error) { - var cliErr CLIError - if !xerrors.As(err, &cliErr) { - cliErr = Fatal(err.Error()) - } - fmt.Fprintln(writer, cliErr.String()) -} - -// LogInfo prints the given info message to stderr. -func LogInfo(header string, lines ...string) { - fmt.Fprint(writer, CLIMessage{ - Level: "info", - Color: color.FgBlue, - Header: header, - Lines: lines, - }.String()) -} - -// LogSuccess prints the given info message to stderr. -func LogSuccess(header string, lines ...string) { - fmt.Fprint(writer, CLIMessage{ - Level: "success", - Color: color.FgGreen, - Header: header, - Lines: lines, - }.String()) -} - -// LogWarn prints the given warn message to stderr. -func LogWarn(header string, lines ...string) { - fmt.Fprint(writer, CLIMessage{ - Level: "warning", - Color: color.FgYellow, - Header: header, - Lines: lines, - }.String()) -} - -// Error creates an error with the level "error". -func Error(header string, lines ...string) CLIError { - return CLIError{ - CLIMessage: CLIMessage{ - Color: color.FgRed, - Level: "error", - Header: header, - Lines: lines, - }, - error: errors.New(header), - } -} - -// Fatal creates an error with the level "fatal". -func Fatal(header string, lines ...string) CLIError { - return CLIError{ - CLIMessage: CLIMessage{ - Color: color.FgRed, - Level: "fatal", - Header: header, - Lines: lines, - }, - error: errors.New(header), - } -} - -// Bold provides a convenience wrapper around color.New for brevity when logging. -func Bold(a string) string { - return color.New(color.Bold).Sprint(a) -} - -// Tipf formats according to the given format specifier and prepends a bolded "tip: " header. -func Tipf(format string, a ...interface{}) string { - return fmt.Sprintf("%s %s", Bold("tip:"), fmt.Sprintf(format, a...)) -} - -// Hintf formats according to the given format specifier and prepends a bolded "hint: " header. -func Hintf(format string, a ...interface{}) string { - return fmt.Sprintf("%s %s", Bold("hint:"), fmt.Sprintf(format, a...)) -} - -// Causef formats according to the given format specifier and prepends a bolded "cause: " header. -func Causef(format string, a ...interface{}) string { - return fmt.Sprintf("%s %s", Bold("cause:"), fmt.Sprintf(format, a...)) -} - -// BlankLine is an empty string meant to be used in CLIMessage and CLIError construction. -const BlankLine = "" diff --git a/pkg/clog/clog_test.go b/pkg/clog/clog_test.go deleted file mode 100644 index 51eab07e..00000000 --- a/pkg/clog/clog_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package clog - -import ( - "bytes" - "fmt" - "io/ioutil" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - "golang.org/x/xerrors" -) - -func TestError(t *testing.T) { - t.Run("oneline", func(t *testing.T) { - var mockErr error = Error("fake error") - mockErr = xerrors.Errorf("wrap 1: %w", mockErr) - mockErr = fmt.Errorf("wrap 2: %w", mockErr) - - var buf bytes.Buffer - //! clearly not concurrent safe - SetOutput(&buf) - - Log(mockErr) - - output, err := ioutil.ReadAll(&buf) - assert.Success(t, "read all stderr output", err) - - assert.Equal(t, "output is as expected", "error: fake error\n\n", string(output)) - }) - - t.Run("plain-error", func(t *testing.T) { - mockErr := xerrors.Errorf("base error") - mockErr = fmt.Errorf("wrap 1: %w", mockErr) - - var buf bytes.Buffer - //! clearly not concurrent safe - SetOutput(&buf) - - Log(mockErr) - - output, err := ioutil.ReadAll(&buf) - assert.Success(t, "read all stderr output", err) - - assert.Equal(t, "output is as expected", "fatal: wrap 1: base error\n\n", string(output)) - }) - - t.Run("message", func(t *testing.T) { - for _, f := range []struct { - f func(string, ...string) - level string - }{{LogInfo, "info"}, {LogSuccess, "success"}, {LogWarn, "warning"}} { - var buf bytes.Buffer - //! clearly not concurrent safe - SetOutput(&buf) - - f.f("testing", Hintf("maybe do %q", "this"), BlankLine, Causef("what happened was %q", "this")) - - output, err := ioutil.ReadAll(&buf) - assert.Success(t, "read all stderr output", err) - - assert.Equal(t, "output is as expected", f.level+": testing\n | hint: maybe do \"this\"\n | \n | cause: what happened was \"this\"\n", string(output)) - } - }) - - t.Run("multi-line", func(t *testing.T) { - var mockErr error = Error("fake header", "next line", BlankLine, Tipf("content of fake tip")) - mockErr = xerrors.Errorf("wrap 1: %w", mockErr) - mockErr = fmt.Errorf("wrap 1: %w", mockErr) - - var buf bytes.Buffer - //! clearly not concurrent safe - SetOutput(&buf) - - Log(mockErr) - - output, err := ioutil.ReadAll(&buf) - assert.Success(t, "read all stderr output", err) - - assert.Equal(t, - "output is as expected", - "error: fake header\n | next line\n | \n | tip: content of fake tip\n\n", - string(output), - ) - }) -} diff --git a/pkg/clog/doc.go b/pkg/clog/doc.go deleted file mode 100644 index 9e5717bb..00000000 --- a/pkg/clog/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package clog provides rich error types and logging helpers for coder-cli. -// -// clog encourages returning error types rather than -// logging them and failing with os.Exit as they happen. -// Error, Fatal, and Warn allow downstream functions to return errors with rich formatting information -// while preserving the original, single-line error chain. -package clog diff --git a/pkg/clog/errgroup.go b/pkg/clog/errgroup.go deleted file mode 100644 index e3a16fd4..00000000 --- a/pkg/clog/errgroup.go +++ /dev/null @@ -1,58 +0,0 @@ -package clog - -import ( - "fmt" - "sync/atomic" - - "golang.org/x/sync/errgroup" - "golang.org/x/xerrors" -) - -// ErrGroup wraps the /x/sync/errgroup.(Group) and adds clog logging and rich error propagation. -// -// Take for example, a case in which we are concurrently stopping a slice of environments. -// In this case, we want to log errors as they happen, not pass them through the callstack as errors. -// When the operations complete, we want to log how many, if any, failed. The caller is still expected -// to handle success and info logging. -type ErrGroup interface { - Go(f func() error) - Wait() error -} - -type group struct { - egroup errgroup.Group - failures int32 -} - -// LoggedErrGroup gives an error group with error logging and error propagation handled automatically. -func LoggedErrGroup() ErrGroup { - return &group{ - egroup: errgroup.Group{}, - failures: 0, - } -} - -func (g *group) Go(f func() error) { - g.egroup.Go(func() error { - if err := f(); err != nil { - atomic.AddInt32(&g.failures, 1) - Log(err) - - // this error does not matter because we discard it in Wait. - return xerrors.New("") - } - return nil - }) -} - -func (g *group) Wait() error { - _ = g.egroup.Wait() // ignore this error because we are already tracking failures manually - if g.failures == 0 { - return nil - } - failureWord := "failure" - if g.failures > 1 { - failureWord += "s" - } - return Fatal(fmt.Sprintf("%d %s emitted", g.failures, failureWord)) -} diff --git a/pkg/clog/errgroup_test.go b/pkg/clog/errgroup_test.go deleted file mode 100644 index b632921d..00000000 --- a/pkg/clog/errgroup_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package clog - -import ( - "bytes" - "errors" - "strings" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/internal/x/xsync" -) - -func TestErrGroup(t *testing.T) { - t.Run("success", func(t *testing.T) { - egroup := LoggedErrGroup() - - var buf bytes.Buffer - SetOutput(xsync.Writer(&buf)) - - egroup.Go(func() error { return nil }) - egroup.Go(func() error { return nil }) - egroup.Go(func() error { return nil }) - - err := egroup.Wait() - assert.Success(t, "error group wait", err) - assert.Equal(t, "empty log buffer", "", buf.String()) - }) - t.Run("failure_count", func(t *testing.T) { - egroup := LoggedErrGroup() - - var buf bytes.Buffer - SetOutput(xsync.Writer(&buf)) - - egroup.Go(func() error { return errors.New("whoops") }) - egroup.Go(func() error { return Error("rich error", "second line") }) - - err := egroup.Wait() - assert.ErrorContains(t, "error group wait", err, "2 failures emitted") - assert.True(t, "log buf contains", strings.Contains(buf.String(), "fatal: whoops\n\n")) - assert.True(t, "log buf contains", strings.Contains(buf.String(), "error: rich error\n | second line\n\n")) - }) -} diff --git a/pkg/tablewriter/doc.go b/pkg/tablewriter/doc.go deleted file mode 100644 index 366a7b9e..00000000 --- a/pkg/tablewriter/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package tablewriter provides helpers for printing human-readable tabular data from slices of structs. -package tablewriter diff --git a/pkg/tablewriter/table_output.golden b/pkg/tablewriter/table_output.golden deleted file mode 100755 index dfe3299c..00000000 --- a/pkg/tablewriter/table_output.golden +++ /dev/null @@ -1,3 +0,0 @@ -Name birthday month first_nested second_nested Age -Tom 12 234-0934 2340-234234 28.12 -Jerry 3 aflfafe-afjlk falj-fjlkjlkadf 36.22 diff --git a/pkg/tablewriter/tablewriter.go b/pkg/tablewriter/tablewriter.go deleted file mode 100644 index 9f12becd..00000000 --- a/pkg/tablewriter/tablewriter.go +++ /dev/null @@ -1,96 +0,0 @@ -package tablewriter - -import ( - "fmt" - "io" - "reflect" - "strings" - "text/tabwriter" -) - -const structFieldTagKey = "table" - -// StructValues tab delimits the values of a given struct. -// -// Tag a field `table:"-"` to hide it from output. -// Tag a field `table:"_"` to flatten its subfields. -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 - } - if shouldFlatten(v.Type().Field(i)) { - fmt.Fprintf(s, "%v", StructValues(v.Field(i).Interface())) - 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 `table:"-"` to hide it from output. -// Tag a field `table:"_"` to flatten its subfields. -func StructFieldNames(data interface{}) string { - v := reflect.ValueOf(data) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - s := &strings.Builder{} - for i := 0; i < v.NumField(); i++ { - field := v.Type().Field(i) - if shouldHideField(field) { - continue - } - if shouldFlatten(field) { - fmt.Fprintf(s, "%s", StructFieldNames(reflect.New(field.Type).Interface())) - 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 `table` struct tag. -// -// `table:"-"` omits the field and no tag defaults to the Go identifier. -// `table:"_"` flattens a fields subfields. -func WriteTable(writer io.Writer, length int, each func(i int) interface{}) error { - if length < 1 { - return nil - } - w := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) - 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 shouldFlatten(f reflect.StructField) bool { - return f.Tag.Get(structFieldTagKey) == "_" -} - -func shouldHideField(f reflect.StructField) bool { - return f.Tag.Get(structFieldTagKey) == "-" -} diff --git a/pkg/tablewriter/tablewriter_test.go b/pkg/tablewriter/tablewriter_test.go deleted file mode 100644 index e611e52c..00000000 --- a/pkg/tablewriter/tablewriter_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package tablewriter - -import ( - "bytes" - "flag" - "io/ioutil" - "testing" - - "cdr.dev/slog/sloggers/slogtest/assert" -) - -var write = flag.Bool("write", false, "write to the golden files") - -func TestTableWriter(t *testing.T) { - type NestedRow struct { - NestedOne string `table:"first_nested"` - NestedTwo string `table:"second_nested"` - } - - type Row struct { - ID string `table:"-"` - Name string - BirthdayMonth int `table:"birthday month"` - Nested NestedRow `table:"_"` - Age float32 - } - - items := []Row{ - { - ID: "13123lkjqlkj-2f323l--f23f", - Name: "Tom", - BirthdayMonth: 12, - Age: 28.12, - Nested: NestedRow{ - NestedOne: "234-0934", - NestedTwo: "2340-234234", - }, - }, - { - ID: "afwaflkj23kl-2f323l--f23f", - Name: "Jerry", - BirthdayMonth: 3, - Age: 36.22, - Nested: NestedRow{ - NestedOne: "aflfafe-afjlk", - NestedTwo: "falj-fjlkjlkadf", - }, - }, - } - - buf := bytes.NewBuffer(nil) - err := WriteTable(buf, len(items), func(i int) interface{} { return items[i] }) - assert.Success(t, "write table", err) - - assertGolden(t, "table_output.golden", buf.Bytes()) -} - -func assertGolden(t *testing.T, path string, output []byte) { - if *write { - err := ioutil.WriteFile(path, output, 0777) - assert.Success(t, "write file", err) - return - } - goldenContent, err := ioutil.ReadFile(path) - assert.Success(t, "read golden file", err) - assert.Equal(t, "golden content matches", string(goldenContent), string(output)) -} diff --git a/pkg/tcli/doc.go b/pkg/tcli/doc.go deleted file mode 100644 index 561dc480..00000000 --- a/pkg/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/pkg/tcli/tcli.go b/pkg/tcli/tcli.go deleted file mode 100644 index b09f4885..00000000 --- a/pkg/tcli/tcli.go +++ /dev/null @@ -1,352 +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", - "--rm", "-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 err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - result.ExitCode = exitErr.ExitCode() - } else { - 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/pkg/tcli/tcli_test.go b/pkg/tcli/tcli_test.go deleted file mode 100644 index 178e702c..00000000 --- a/pkg/tcli/tcli_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package tcli_test - -import ( - "context" - "os" - "os/exec" - "strings" - "testing" - "time" - - "cdr.dev/slog/sloggers/slogtest/assert" - - "cdr.dev/coder-cli/pkg/tcli" -) - -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), - ) -}