diff --git a/.github/actions/setup-go/action.yaml b/.github/actions/setup-go/action.yaml
index 4d696ef298b12..968f1f1ab8d27 100644
--- a/.github/actions/setup-go/action.yaml
+++ b/.github/actions/setup-go/action.yaml
@@ -4,7 +4,7 @@ description: |
inputs:
version:
description: "The Go version to use."
- default: "1.20.6"
+ default: "1.20.7"
runs:
using: "composite"
steps:
diff --git a/.github/actions/setup-sqlc/action.yaml b/.github/actions/setup-sqlc/action.yaml
index 354e55e8213f6..d109a50f52f75 100644
--- a/.github/actions/setup-sqlc/action.yaml
+++ b/.github/actions/setup-sqlc/action.yaml
@@ -7,4 +7,4 @@ runs:
- name: Setup sqlc
uses: sqlc-dev/setup-sqlc@v3
with:
- sqlc-version: "1.19.1"
+ sqlc-version: "1.20.0"
diff --git a/.github/actions/setup-tf/action.yaml b/.github/actions/setup-tf/action.yaml
index 16472c9fafd6e..63a539a3fd922 100644
--- a/.github/actions/setup-tf/action.yaml
+++ b/.github/actions/setup-tf/action.yaml
@@ -7,5 +7,5 @@ runs:
- name: Install Terraform
uses: hashicorp/setup-terraform@v2
with:
- terraform_version: ~1.5
+ terraform_version: 1.5.5
terraform_wrapper: false
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
index 5b9f7a9c6597a..76048b9fe398d 100644
--- a/.github/dependabot.yaml
+++ b/.github/dependabot.yaml
@@ -8,7 +8,7 @@ updates:
timezone: "America/Chicago"
labels: []
commit-message:
- prefix: "chore"
+ prefix: "ci"
ignore:
# These actions deliver the latest versions by updating the major
# release tag, so ignore minor and patch versions
@@ -117,11 +117,6 @@ updates:
- "@eslint*"
- "@typescript-eslint/eslint-plugin"
- "@typescript-eslint/parser"
- jest:
- patterns:
- - "jest*"
- - "@swc/jest"
- - "@types/jest"
- package-ecosystem: "npm"
directory: "/offlinedocs/"
@@ -146,20 +141,6 @@ updates:
- version-update:semver-major
# Update dogfood.
- - package-ecosystem: "docker"
- directory: "/dogfood/"
- schedule:
- interval: "weekly"
- time: "06:00"
- timezone: "America/Chicago"
- commit-message:
- prefix: "chore"
- labels: []
- groups:
- dogfood-docker:
- patterns:
- - "*"
-
- package-ecosystem: "terraform"
directory: "/dogfood/"
schedule:
diff --git a/.github/pr-deployments/certificate.yaml b/.github/pr-deployments/certificate.yaml
new file mode 100644
index 0000000000000..cf441a98bbc88
--- /dev/null
+++ b/.github/pr-deployments/certificate.yaml
@@ -0,0 +1,13 @@
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ name: pr${PR_NUMBER}-tls
+ namespace: pr-deployment-certs
+spec:
+ secretName: pr${PR_NUMBER}-tls
+ issuerRef:
+ name: letsencrypt
+ kind: ClusterIssuer
+ dnsNames:
+ - "${PR_HOSTNAME}"
+ - "*.${PR_HOSTNAME}"
diff --git a/.github/pr-deployments/rbac.yaml b/.github/pr-deployments/rbac.yaml
new file mode 100644
index 0000000000000..0d37cae7daebe
--- /dev/null
+++ b/.github/pr-deployments/rbac.yaml
@@ -0,0 +1,31 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: coder-workspace-pr${PR_NUMBER}
+ namespace: pr${PR_NUMBER}
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-workspace-pr${PR_NUMBER}
+ namespace: pr${PR_NUMBER}
+rules:
+ - apiGroups: ["*"]
+ resources: ["*"]
+ verbs: ["*"]
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: coder-workspace-pr${PR_NUMBER}
+ namespace: pr${PR_NUMBER}
+subjects:
+ - kind: ServiceAccount
+ name: coder-workspace-pr${PR_NUMBER}
+ namespace: pr${PR_NUMBER}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-workspace-pr${PR_NUMBER}
diff --git a/.github/pr-deployments/template/main.tf b/.github/pr-deployments/template/main.tf
new file mode 100644
index 0000000000000..bef767547b2a0
--- /dev/null
+++ b/.github/pr-deployments/template/main.tf
@@ -0,0 +1,313 @@
+terraform {
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = "~> 0.11.0"
+ }
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = "~> 2.22"
+ }
+ }
+}
+
+provider "coder" {
+}
+
+variable "namespace" {
+ type = string
+ description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces)"
+}
+
+data "coder_parameter" "cpu" {
+ name = "cpu"
+ display_name = "CPU"
+ description = "The number of CPU cores"
+ default = "2"
+ icon = "/icon/memory.svg"
+ mutable = true
+ option {
+ name = "2 Cores"
+ value = "2"
+ }
+ option {
+ name = "4 Cores"
+ value = "4"
+ }
+ option {
+ name = "6 Cores"
+ value = "6"
+ }
+ option {
+ name = "8 Cores"
+ value = "8"
+ }
+}
+
+data "coder_parameter" "memory" {
+ name = "memory"
+ display_name = "Memory"
+ description = "The amount of memory in GB"
+ default = "2"
+ icon = "/icon/memory.svg"
+ mutable = true
+ option {
+ name = "2 GB"
+ value = "2"
+ }
+ option {
+ name = "4 GB"
+ value = "4"
+ }
+ option {
+ name = "6 GB"
+ value = "6"
+ }
+ option {
+ name = "8 GB"
+ value = "8"
+ }
+}
+
+data "coder_parameter" "home_disk_size" {
+ name = "home_disk_size"
+ display_name = "Home disk size"
+ description = "The size of the home disk in GB"
+ default = "10"
+ type = "number"
+ icon = "/emojis/1f4be.png"
+ mutable = false
+ validation {
+ min = 1
+ max = 99999
+ }
+}
+
+provider "kubernetes" {
+ config_path = null
+}
+
+data "coder_workspace" "me" {}
+
+resource "coder_agent" "main" {
+ os = "linux"
+ arch = "amd64"
+ startup_script_timeout = 180
+ startup_script = <<-EOT
+ set -e
+
+ # install and start code-server
+ curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server
+ /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &
+
+ EOT
+
+ # The following metadata blocks are optional. They are used to display
+ # information about your workspace in the dashboard. You can remove them
+ # if you don't want to display any information.
+ # For basic resources, you can use the `coder stat` command.
+ # If you need more control, you can write your own script.
+ metadata {
+ display_name = "CPU Usage"
+ key = "0_cpu_usage"
+ script = "coder stat cpu"
+ interval = 10
+ timeout = 1
+ }
+
+ metadata {
+ display_name = "RAM Usage"
+ key = "1_ram_usage"
+ script = "coder stat mem"
+ interval = 10
+ timeout = 1
+ }
+
+ metadata {
+ display_name = "Home Disk"
+ key = "3_home_disk"
+ script = "coder stat disk --path $${HOME}"
+ interval = 60
+ timeout = 1
+ }
+
+ metadata {
+ display_name = "CPU Usage (Host)"
+ key = "4_cpu_usage_host"
+ script = "coder stat cpu --host"
+ interval = 10
+ timeout = 1
+ }
+
+ metadata {
+ display_name = "Memory Usage (Host)"
+ key = "5_mem_usage_host"
+ script = "coder stat mem --host"
+ interval = 10
+ timeout = 1
+ }
+
+ metadata {
+ display_name = "Load Average (Host)"
+ key = "6_load_host"
+ # get load avg scaled by number of cores
+ script = < $protoc_path
- chmod +x $protoc_path
- protoc --version
+ mkdir -p /tmp/proto
+ pushd /tmp/proto
+ curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip
+ unzip protoc.zip
+ cp -r ./bin/* /usr/local/bin
+ cp -r ./include /usr/local/bin/include
+ popd
- name: make gen
run: "make --output-sync -j -B gen"
@@ -224,7 +221,7 @@ jobs:
with:
# This doesn't need caching. It's super fast anyways!
cache: false
- go-version: 1.20.6
+ go-version: 1.20.7
- name: Install shfmt
run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.5.0
@@ -532,6 +529,24 @@ jobs:
- name: Setup Terraform
uses: ./.github/actions/setup-tf
+ - name: go install tools
+ run: |
+ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
+ go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33
+ go install golang.org/x/tools/cmd/goimports@latest
+ go install github.com/mikefarah/yq/v4@v4.30.6
+ go install github.com/golang/mock/mockgen@v1.6.0
+
+ - name: Install Protoc
+ run: |
+ mkdir -p /tmp/proto
+ pushd /tmp/proto
+ curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip
+ unzip protoc.zip
+ cp -r ./bin/* /usr/local/bin
+ cp -r ./include /usr/local/bin/include
+ popd
+
- name: Build
run: |
make -B site/out/index.html
@@ -586,6 +601,7 @@ jobs:
# https://www.chromatic.com/docs/github-actions#forked-repositories
projectToken: 695c25b6cb65
workingDir: "./site"
+ storybookBaseDir: "./site"
# Prevent excessive build runs on minor version changes
skip: "@(renovate/**|dependabot/**)"
# Run TurboSnap to trace file dependencies to related stories
@@ -611,6 +627,7 @@ jobs:
buildScriptName: "storybook:build"
projectToken: 695c25b6cb65
workingDir: "./site"
+ storybookBaseDir: "./site"
# Run TurboSnap to trace file dependencies to related stories
# and tell chromatic to only take snapshots of relevent stories
onlyChanged: true
@@ -640,9 +657,7 @@ jobs:
go install github.com/golang/mock/mockgen@v1.6.0
- name: Setup sqlc
- uses: sqlc-dev/setup-sqlc@v3
- with:
- sqlc-version: "1.19.1"
+ uses: ./.github/actions/setup-sqlc
- name: Format
run: |
@@ -668,6 +683,7 @@ jobs:
- test-go-pg
- test-go-race
- test-js
+ - test-e2e
- offlinedocs
# Allow this job to run even if the needed jobs fail, are skipped or
# cancelled.
@@ -724,6 +740,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Linux amd64 Docker image
+ id: build_and_push
run: |
set -euxo pipefail
go mod download
@@ -738,3 +755,19 @@ jobs:
--version $version \
--push \
build/coder_linux_amd64
+
+ # Tag image with new package tag and push
+ tag=$(echo "$version" | sed 's/+/-/g')
+ docker tag ghcr.io/coder/coder-preview:main ghcr.io/coder/coder-preview:main-$tag
+ docker push ghcr.io/coder/coder-preview:main-$tag
+
+ - name: Prune old images
+ uses: vlaurin/action-ghcr-prune@v0.5.0
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ organization: coder
+ container: coder-preview
+ keep-younger-than: 7 # days
+ keep-tags-regexes: ^pr
+ prune-tags-regexes: ^main-
+ prune-untagged: true
diff --git a/.github/workflows/contrib.yaml b/.github/workflows/contrib.yaml
index e3365d1abe24c..9c601b011c03b 100644
--- a/.github/workflows/contrib.yaml
+++ b/.github/workflows/contrib.yaml
@@ -46,7 +46,8 @@ jobs:
path-to-document: "https://github.com/coder/cla/blob/main/README.md"
# branch should not be protected
branch: "main"
- allowlist: dependabot*
+ # Some users have signed a corporate CLA with Coder so are exempt from signing our community one.
+ allowlist: "coryb,aaronlehmann,dependabot*"
release-labels:
runs-on: ubuntu-latest
diff --git a/.github/workflows/dogfood.yaml b/.github/workflows/dogfood.yaml
index f1a6c2e712fd0..bbed89679f7d1 100644
--- a/.github/workflows/dogfood.yaml
+++ b/.github/workflows/dogfood.yaml
@@ -5,11 +5,15 @@ on:
branches:
- main
paths:
+ - "flake.nix"
+ - "flake.lock"
- "dogfood/**"
- ".github/workflows/dogfood.yaml"
# Uncomment these lines when testing with CI.
# pull_request:
# paths:
+ # - "flake.nix"
+ # - "flake.lock"
# - "dogfood/**"
# - ".github/workflows/dogfood.yaml"
workflow_dispatch:
@@ -18,6 +22,9 @@ jobs:
deploy_image:
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
- name: Get branch name
id: branch-name
uses: tj-actions/branch-names@v6.5
@@ -30,11 +37,13 @@ jobs:
tag=${tag//\//--}
echo "tag=${tag}" >> $GITHUB_OUTPUT
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ - name: Install Nix
+ uses: DeterminateSystems/nix-installer-action@v4
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ - name: Run the Magic Nix Cache
+ uses: DeterminateSystems/magic-nix-cache-action@v2
+
+ - run: nix build .#devEnvImage && ./result | docker load
- name: Login to DockerHub
uses: docker/login-action@v2
@@ -42,15 +51,10 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- - name: Build and push
- uses: docker/build-push-action@v4
- with:
- context: "{{defaultContext}}:dogfood"
- pull: true
- push: true
- tags: "codercom/oss-dogfood:${{ steps.docker-tag-name.outputs.tag }},codercom/oss-dogfood:latest"
- cache-from: type=registry,ref=codercom/oss-dogfood:latest
- cache-to: type=inline
+ - name: Tag and Push
+ run: |
+ docker tag codercom/oss-dogfood:latest codercom/oss-dogfood:${{ steps.docker-tag-name.outputs.tag }}
+ docker push codercom/oss-dogfood -a
deploy_template:
needs: deploy_image
diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml
index 510c8f4299361..d32ea2f5d49b7 100644
--- a/.github/workflows/pr-cleanup.yaml
+++ b/.github/workflows/pr-cleanup.yaml
@@ -1,4 +1,4 @@
-name: Cleanup PR deployment and image
+name: pr-cleanup
on:
pull_request:
types: closed
@@ -35,14 +35,14 @@ jobs:
- name: Set up kubeconfig
run: |
- set -euxo pipefail
+ set -euo pipefail
mkdir -p ~/.kube
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
export KUBECONFIG=~/.kube/config
- name: Delete helm release
run: |
- set -euxo pipefail
+ set -euo pipefail
helm delete --namespace "pr${{ steps.pr_number.outputs.PR_NUMBER }}" "pr${{ steps.pr_number.outputs.PR_NUMBER }}" || echo "helm release not found"
- name: "Remove PR namespace"
@@ -51,7 +51,7 @@ jobs:
- name: "Remove DNS records"
run: |
- set -euxo pipefail
+ set -euo pipefail
# Get identifier for the record
record_id=$(curl -X GET "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records?name=%2A.pr${{ steps.pr_number.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" \
-H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \
diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml
index adc0e2d25c376..4dd2446dcc9aa 100644
--- a/.github/workflows/pr-deploy.yaml
+++ b/.github/workflows/pr-deploy.yaml
@@ -4,24 +4,30 @@
# 3. when a PR is updated
name: Deploy PR
on:
- pull_request:
- types: synchronize
+ push:
+ branches-ignore:
+ - main
workflow_dispatch:
inputs:
pr_number:
description: "PR number"
type: number
required: true
- skip_build:
- description: "Skip build job"
- required: false
- type: boolean
- default: false
experiments:
description: "Experiments to enable"
required: false
type: string
default: "*"
+ build:
+ description: "Force new build"
+ required: false
+ type: boolean
+ default: false
+ deploy:
+ description: "Force new deployment"
+ required: false
+ type: boolean
+ default: false
env:
REPO: ghcr.io/coder/coder-preview
@@ -29,43 +35,70 @@ env:
permissions:
contents: read
packages: write
- pull-requests: write
+ pull-requests: write # needed for commenting on PRs
concurrency:
- group: ${{ github.workflow }}-PR-${{ github.event.pull_request.number || github.event.inputs.pr_number }}
+ group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
+ check_pr:
+ runs-on: ubuntu-latest
+ outputs:
+ PR_OPEN: ${{ steps.check_pr.outputs.pr_open }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Check if PR is open
+ id: check_pr
+ run: |
+ set -euo pipefail
+ pr_open=true
+ if [[ "$(gh pr view --json state | jq -r '.state')" != "OPEN" ]]; then
+ echo "PR doesn't exist or is closed."
+ pr_open=false
+ fi
+ echo "pr_open=$pr_open" >> $GITHUB_OUTPUT
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
get_info:
- if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request'
+ needs: check_pr
+ if: ${{ needs.check_pr.outputs.PR_OPEN == 'true' }}
outputs:
PR_NUMBER: ${{ steps.pr_info.outputs.PR_NUMBER }}
PR_TITLE: ${{ steps.pr_info.outputs.PR_TITLE }}
PR_URL: ${{ steps.pr_info.outputs.PR_URL }}
- PR_BRANCH: ${{ steps.pr_info.outputs.PR_BRANCH }}
CODER_BASE_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }}
CODER_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_IMAGE_TAG }}
- NEW: ${{ steps.check_deployment.outputs.new }}
- BUILD: ${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.new }}
+ NEW: ${{ steps.check_deployment.outputs.NEW }}
+ BUILD: ${{ steps.build_conditionals.outputs.first_or_force_build || steps.build_conditionals.outputs.automatic_rebuild }}
runs-on: "ubuntu-latest"
steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
- name: Get PR number, title, and branch name
id: pr_info
run: |
- set -euxo pipefail
- PR_NUMBER=${{ github.event.inputs.pr_number || github.event.pull_request.number }}
- PR_TITLE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/coder/coder/pulls/$PR_NUMBER | jq -r '.title')
- PR_BRANCH=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/coder/coder/pulls/$PR_NUMBER | jq -r '.head.ref')
- echo "PR_URL=https://github.com/coder/coder/pull/$PR_NUMBER" >> $GITHUB_OUTPUT
+ set -euo pipefail
+ PR_NUMBER=$(gh pr view --json number | jq -r '.number')
+ PR_TITLE=$(gh pr view --json title | jq -r '.title')
+ PR_URL=$(gh pr view --json url | jq -r '.url')
+ echo "PR_URL=$PR_URL" >> $GITHUB_OUTPUT
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "PR_TITLE=$PR_TITLE" >> $GITHUB_OUTPUT
- echo "PR_BRANCH=$PR_BRANCH" >> $GITHUB_OUTPUT
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set required tags
id: set_tags
run: |
- set -euxo pipefail
+ set -euo pipefail
echo "CODER_BASE_IMAGE_TAG=$CODER_BASE_IMAGE_TAG" >> $GITHUB_OUTPUT
echo "CODER_IMAGE_TAG=$CODER_IMAGE_TAG" >> $GITHUB_OUTPUT
env:
@@ -74,7 +107,7 @@ jobs:
- name: Set up kubeconfig
run: |
- set -euxo pipefail
+ set -euo pipefail
mkdir -p ~/.kube
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
export KUBECONFIG=~/.kube/config
@@ -82,53 +115,21 @@ jobs:
- name: Check if the helm deployment already exists
id: check_deployment
run: |
- set -euxo pipefail
+ set -euo pipefail
if helm status "pr${{ steps.pr_info.outputs.PR_NUMBER }}" --namespace "pr${{ steps.pr_info.outputs.PR_NUMBER }}" > /dev/null 2>&1; then
echo "Deployment already exists. Skipping deployment."
- new=false
+ NEW=false
else
echo "Deployment doesn't exist."
- new=true
+ NEW=true
fi
- echo "new=$new" >> $GITHUB_OUTPUT
-
- - name: Find Comment
- uses: peter-evans/find-comment@v2
- if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
- id: fc
- with:
- issue-number: ${{ steps.pr_info.outputs.PR_NUMBER }}
- comment-author: "github-actions[bot]"
- body-includes: ":rocket:"
- direction: last
-
- - name: Comment on PR
- id: comment_id
- if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
- uses: peter-evans/create-or-update-comment@v3
- with:
- comment-id: ${{ steps.fc.outputs.comment-id }}
- issue-number: ${{ steps.pr_info.outputs.PR_NUMBER }}
- edit-mode: replace
- body: |
- ---
- :rocket: Deploying PR ${{ steps.pr_info.outputs.PR_NUMBER }} ...
- ---
- reactions: eyes
- reactions-edit-mode: replace
-
- - name: Checkout
- if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
- uses: actions/checkout@v3
- with:
- ref: ${{ steps.pr_info.outputs.PR_BRANCH }}
- fetch-depth: 0
+ echo "NEW=$NEW" >> $GITHUB_OUTPUT
- name: Check changed files
- if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
uses: dorny/paths-filter@v2
id: filter
with:
+ base: ${{ github.ref }}
filters: |
all:
- "**"
@@ -149,47 +150,72 @@ jobs:
- "scripts/**/*[^D][^o][^c][^k][^e][^r][^f][^i][^l][^e][.][b][^a][^s][^e]*"
- name: Print number of changed files
- if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
run: |
- set -euxo pipefail
+ set -euo pipefail
echo "Total number of changed files: ${{ steps.filter.outputs.all_count }}"
echo "Number of ignored files: ${{ steps.filter.outputs.ignored_count }}"
+ - name: Build conditionals
+ id: build_conditionals
+ run: |
+ set -euo pipefail
+ # build if the workflow is manually triggered and the deployment doesn't exist (first build or force rebuild)
+ echo "first_or_force_build=${{ (github.event_name == 'workflow_dispatch' && steps.check_deployment.outputs.NEW == 'true') || github.event.inputs.build == 'true' }}" >> $GITHUB_OUTPUT
+ # build if the deployment alreday exist and there are changes in the files that we care about (automatic updates)
+ echo "automatic_rebuild=${{ steps.check_deployment.outputs.NEW == 'false' && steps.filter.outputs.all_count > steps.filter.outputs.ignored_count }}" >> $GITHUB_OUTPUT
+
+ comment-pr:
+ needs: [check_pr, get_info]
+ if: needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true'
+ runs-on: "ubuntu-latest"
+ steps:
+ - name: Find Comment
+ uses: peter-evans/find-comment@v2
+ id: fc
+ with:
+ issue-number: ${{ needs.get_info.outputs.PR_NUMBER }}
+ comment-author: "github-actions[bot]"
+ body-includes: ":rocket:"
+ direction: last
+
+ - name: Comment on PR
+ id: comment_id
+ uses: peter-evans/create-or-update-comment@v3
+ with:
+ comment-id: ${{ steps.fc.outputs.comment-id }}
+ issue-number: ${{ needs.get_info.outputs.PR_NUMBER }}
+ edit-mode: replace
+ body: |
+ ---
+ :rocket: Deploying PR ${{ needs.get_info.outputs.PR_NUMBER }} ...
+ ---
+ reactions: eyes
+ reactions-edit-mode: replace
+
build:
needs: get_info
- # Skips the build job if the workflow was triggered by a workflow_dispatch event and the skip_build input is set to true
- # or if the workflow was triggered by an issue_comment event and the comment body contains --skip-build
- # always run the build job if a pull_request event triggered the workflow
- if: |
- (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_build == 'false') ||
- (github.event_name == 'pull_request' && needs.get_info.result == 'success' && needs.get_info.outputs.NEW == 'false')
+ # Run build job only if there are changes in the files that we care about or if the workflow is manually triggered with --build flag
+ if: needs.get_info.outputs.BUILD == 'true'
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
- PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }}
- PR_BRANCH: ${{ needs.get_info.outputs.PR_BRANCH }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
- ref: ${{ env.PR_BRANCH }}
fetch-depth: 0
- name: Setup Node
- if: needs.get_info.outputs.BUILD == 'true'
uses: ./.github/actions/setup-node
- name: Setup Go
- if: needs.get_info.outputs.BUILD == 'true'
uses: ./.github/actions/setup-go
- name: Setup sqlc
- if: needs.get_info.outputs.BUILD == 'true'
uses: ./.github/actions/setup-sqlc
- name: GHCR Login
- if: needs.get_info.outputs.BUILD == 'true'
uses: docker/login-action@v2
with:
registry: ghcr.io
@@ -197,9 +223,8 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Linux amd64 Docker image
- if: needs.get_info.outputs.BUILD == 'true'
run: |
- set -euxo pipefail
+ set -euo pipefail
go mod download
make gen/mark-fresh
export DOCKER_IMAGE_NO_PREREQUISITES=true
@@ -217,35 +242,42 @@ jobs:
needs: [build, get_info]
# Run deploy job only if build job was successful or skipped
if: |
- always() && (needs.build.result == 'success' || needs.build.result == 'skipped') &&
- (github.event_name == 'workflow_dispatch' || needs.get_info.outputs.NEW == 'false')
+ always() && (needs.build.result == 'success' || needs.build.result == 'skipped') &&
+ (needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true')
runs-on: "ubuntu-latest"
env:
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }}
PR_TITLE: ${{ needs.get_info.outputs.PR_TITLE }}
PR_URL: ${{ needs.get_info.outputs.PR_URL }}
- PR_BRANCH: ${{ needs.get_info.outputs.PR_BRANCH }}
- PR_DEPLOYMENT_ACCESS_URL: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
+ PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
steps:
- name: Set up kubeconfig
run: |
- set -euxo pipefail
+ set -euo pipefail
mkdir -p ~/.kube
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
export KUBECONFIG=~/.kube/config
- name: Check if image exists
- if: needs.get_info.outputs.NEW == 'true'
run: |
- set -euxo pipefail
- foundTag=$(curl -fsSL https://github.com/coder/coder/pkgs/container/coder-preview | grep -o ${{ env.CODER_IMAGE_TAG }} | head -n 1)
+ set -euo pipefail
+ foundTag=$(
+ gh api /orgs/coder/packages/container/coder-preview/versions |
+ jq -r --arg tag "pr${{ env.PR_NUMBER }}" '.[] |
+ select(.metadata.container.tags == [$tag]) |
+ .metadata.container.tags[0]'
+ )
if [ -z "$foundTag" ]; then
echo "Image not found"
echo "${{ env.CODER_IMAGE_TAG }} not found in ghcr.io/coder/coder-preview"
- echo "Please remove --skip-build from the comment and try again"
exit 1
+ else
+ echo "Image found"
+ echo "$foundTag tag found in ghcr.io/coder/coder-preview"
fi
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Add DNS record to Cloudflare
if: needs.get_info.outputs.NEW == 'true'
@@ -253,43 +285,27 @@ jobs:
curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records" \
-H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type:application/json" \
- --data '{"type":"CNAME","name":"*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}","content":"${{ env.PR_DEPLOYMENT_ACCESS_URL }}","ttl":1,"proxied":false}'
-
- - name: Checkout
- uses: actions/checkout@v3
- with:
- ref: ${{ env.PR_BRANCH }}
+ --data '{"type":"CNAME","name":"*.${{ env.PR_HOSTNAME }}","content":"${{ env.PR_HOSTNAME }}","ttl":1,"proxied":false}'
- name: Create PR namespace
- if: needs.get_info.outputs.NEW == 'true'
+ if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
- set -euxo pipefail
+ set -euo pipefail
# try to delete the namespace, but don't fail if it doesn't exist
kubectl delete namespace "pr${{ env.PR_NUMBER }}" || true
kubectl create namespace "pr${{ env.PR_NUMBER }}"
+ - name: Checkout
+ uses: actions/checkout@v3
+
- name: Check and Create Certificate
- if: needs.get_info.outputs.NEW == 'true'
+ if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
# Using kubectl to check if a Certificate resource already exists
# we are doing this to avoid letsenrypt rate limits
if ! kubectl get certificate pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs > /dev/null 2>&1; then
echo "Certificate doesn't exist. Creating a new one."
- cat < pr-deploy-values.yaml
- coder:
- image:
- repo: ${{ env.REPO }}
- tag: pr${{ env.PR_NUMBER }}
- pullPolicy: Always
- service:
- type: ClusterIP
- ingress:
- enable: true
- className: traefik
- host: ${{ env.PR_DEPLOYMENT_ACCESS_URL }}
- wildcardHost: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
- tls:
- enable: true
- secretName: pr${{ env.PR_NUMBER }}-tls
- wildcardSecretName: pr${{ env.PR_NUMBER }}-tls
- env:
- - name: "CODER_ACCESS_URL"
- value: "https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
- - name: "CODER_WILDCARD_ACCESS_URL"
- value: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
- - name: "CODER_EXPERIMENTS"
- value: "${{ github.event.inputs.experiments }}"
- - name: CODER_PG_CONNECTION_URL
- valueFrom:
- secretKeyRef:
- name: coder-db-url
- key: url
- - name: "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS"
- value: "true"
- - name: "CODER_OAUTH2_GITHUB_CLIENT_ID"
- value: "${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_ID }}"
- - name: "CODER_OAUTH2_GITHUB_CLIENT_SECRET"
- value: "${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_SECRET }}"
- - name: "CODER_OAUTH2_GITHUB_ALLOWED_ORGS"
- value: "coder"
- EOF
+ set -euo pipefail
+ envsubst < ./.github/pr-deployments/values.yaml > ./pr-deploy-values.yaml
- name: Install/Upgrade Helm chart
run: |
- set -euxo pipefail
- if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then
- helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \
- --namespace "pr${{ env.PR_NUMBER }}" \
- --values ./pr-deploy-values.yaml \
- --force
- else
- if [[ ${{ needs.get_info.outputs.BUILD }} == "true" ]]; then
- helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \
- --namespace "pr${{ env.PR_NUMBER }}" \
- --reuse-values \
- --force
- else
- echo "Skipping helm upgrade, as there is no new image to deploy"
- fi
- fi
+ set -euo pipefail
+ helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm/coder \
+ --namespace "pr${{ env.PR_NUMBER }}" \
+ --values ./pr-deploy-values.yaml \
+ --force
- name: Install coder-logstream-kube
- if: needs.get_info.outputs.NEW == 'true'
+ if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
helm repo add coder-logstream-kube https://helm.coder.com/logstream-kube
helm upgrade --install coder-logstream-kube coder-logstream-kube/coder-logstream-kube \
--namespace "pr${{ env.PR_NUMBER }}" \
- --set url="https://pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
+ --set url="https://${{ env.PR_HOSTNAME }}"
- name: Get Coder binary
- if: needs.get_info.outputs.NEW == 'true'
+ if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
- set -euxo pipefail
+ set -euo pipefail
DEST="${HOME}/coder"
- URL="https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}/bin/coder-linux-amd64"
+ URL="https://${{ env.PR_HOSTNAME }}/bin/coder-linux-amd64"
mkdir -p "$(dirname ${DEST})"
@@ -414,10 +393,10 @@ jobs:
mv "${DEST}" /usr/local/bin/coder
- name: Create first user, template and workspace
- if: needs.get_info.outputs.NEW == 'true'
+ if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
id: setup_deployment
run: |
- set -euxo pipefail
+ set -euo pipefail
# Create first user
@@ -429,28 +408,24 @@ jobs:
echo "password=$password" >> $GITHUB_OUTPUT
coder login \
- --first-user-username test \
+ --first-user-username coder \
--first-user-email pr${{ env.PR_NUMBER }}@coder.com \
--first-user-password $password \
--first-user-trial \
--use-token-as-session \
- https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}
+ https://${{ env.PR_HOSTNAME }}
# Create template
- coder templates init --id kubernetes && cd ./kubernetes/ && coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }}
+ cd ./.github/pr-deployments/template
+ terraform init
+ coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes
# Create workspace
- cat < workspace.yaml
- cpu: "2"
- memory: "4"
- home_disk_size: "2"
- EOF
-
- coder create --template="kubernetes" test --rich-parameter-file ./workspace.yaml -y
- coder stop test -y
+ coder create --template="kubernetes" kube --parameter cpu=2 --parameter memory=4 --parameter home_disk_size=2 -y
+ coder stop kube -y
- name: Send Slack notification
- if: needs.get_info.outputs.NEW == 'true'
+ if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
run: |
curl -s -o /dev/null -X POST -H 'Content-type: application/json' \
-d \
@@ -458,7 +433,7 @@ jobs:
"pr_number": "'"${{ env.PR_NUMBER }}"'",
"pr_url": "'"${{ env.PR_URL }}"'",
"pr_title": "'"${{ env.PR_TITLE }}"'",
- "pr_access_url": "'"https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"'",
+ "pr_access_url": "'"https://${{ env.PR_HOSTNAME }}"'",
"pr_username": "'"test"'",
"pr_email": "'"pr${{ env.PR_NUMBER }}@coder.com"'",
"pr_password": "'"${{ steps.setup_deployment.outputs.password }}"'",
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 95f07feda29ca..42d6a2f8620fc 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -28,10 +28,6 @@ env:
# https://github.blog/changelog/2022-06-10-github-actions-inputs-unified-across-manual-and-reusable-workflows/
CODER_RELEASE: ${{ !inputs.dry_run }}
CODER_DRY_RUN: ${{ inputs.dry_run }}
- # For some reason, setup-go won't actually pick up a new patch version if
- # it has an old one cached. We need to manually specify the versions so we
- # can get the latest release. Never use "~1.xx" here!
- CODER_GO_VERSION: "1.20.6"
jobs:
release:
@@ -98,16 +94,8 @@ jobs:
- name: Setup Go
uses: ./.github/actions/setup-go
- - name: Cache Node
- id: cache-node
- uses: buildjet/cache@v3
- with:
- path: |
- **/node_modules
- .eslintcache
- key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
- restore-keys: |
- js-${{ runner.os }}-
+ - name: Setup Node
+ uses: ./.github/actions/setup-node
- name: Install nsis and zstd
run: sudo apt-get install -y nsis zstd
@@ -153,7 +141,8 @@ jobs:
build/coder_"$version"_linux_{amd64,armv7,arm64}.{tar.gz,apk,deb,rpm} \
build/coder_"$version"_{darwin,windows}_{amd64,arm64}.zip \
build/coder_"$version"_windows_amd64_installer.exe \
- build/coder_helm_"$version".tgz
+ build/coder_helm_"$version".tgz \
+ build/provisioner_helm_"$version".tgz
env:
CODER_SIGN_DARWIN: "1"
AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
@@ -307,9 +296,11 @@ jobs:
version="$(./scripts/version.sh)"
mkdir -p build/helm
cp "build/coder_helm_${version}.tgz" build/helm
+ cp "build/provisioner_helm_${version}.tgz" build/helm
gsutil cp gs://helm.coder.com/v2/index.yaml build/helm/index.yaml
helm repo index build/helm --url https://helm.coder.com/v2 --merge build/helm/index.yaml
gsutil -h "Cache-Control:no-cache,max-age=0" cp build/helm/coder_helm_${version}.tgz gs://helm.coder.com/v2
+ gsutil -h "Cache-Control:no-cache,max-age=0" cp build/helm/provisioner_helm_${version}.tgz gs://helm.coder.com/v2
gsutil -h "Cache-Control:no-cache,max-age=0" cp build/helm/index.yaml gs://helm.coder.com/v2
gsutil -h "Cache-Control:no-cache,max-age=0" cp helm/artifacthub-repo.yml gs://helm.coder.com/v2
@@ -341,6 +332,7 @@ jobs:
name: Publish to winget-pkgs
runs-on: windows-latest
needs: release
+ if: ${{ !inputs.dry_run }}
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -376,12 +368,6 @@ jobs:
echo "Installer URL: ${installer_url}"
echo "Package version: ${version}"
- # Bail if dry-run.
- if ($env:CODER_DRY_RUN -match "t") {
- echo "Skipping submission due to dry-run."
- exit 0
- }
-
# The URL "|X64" suffix forces the architecture as it cannot be
# sniffed properly from the URL. wingetcreate checks both the URL and
# binary magic bytes for the architecture and they need to both match,
@@ -405,7 +391,6 @@ jobs:
WINGET_GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
- name: Comment on PR
- if: ${{ !inputs.dry_run }}
run: |
# wait 30 seconds
Start-Sleep -Seconds 30.0
@@ -421,3 +406,65 @@ jobs:
# For gh CLI. We need a real token since we're commenting on a PR in a
# different repo.
GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
+
+ publish-chocolatey:
+ name: Publish to Chocolatey
+ runs-on: windows-latest
+ needs: release
+ if: ${{ !inputs.dry_run }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ # Same reason as for release.
+ - name: Fetch git tags
+ run: git fetch --tags --force
+
+ # From https://chocolatey.org
+ - name: Install Chocolatey
+ run: |
+ Set-ExecutionPolicy Bypass -Scope Process -Force
+ [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
+
+ iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
+
+ - name: Build chocolatey package
+ run: |
+ cd scripts/chocolatey
+
+ # The package version is the same as the tag minus the leading "v".
+ # The version in this output already has the leading "v" removed but
+ # we do it again to be safe.
+ $version = "${{ needs.release.outputs.version }}".Trim('v')
+
+ $release_assets = gh release view --repo coder/coder "v${version}" --json assets | `
+ ConvertFrom-Json
+
+ # Get the URL for the Windows ZIP from the release assets.
+ $zip_url = $release_assets.assets | `
+ Where-Object name -Match ".*_windows_amd64.zip$" | `
+ Select -ExpandProperty url
+
+ echo "ZIP URL: ${zip_url}"
+ echo "Package version: ${version}"
+
+ echo "Downloading ZIP..."
+ Invoke-WebRequest $zip_url -OutFile assets.zip
+
+ echo "Extracting ZIP..."
+ Expand-Archive assets.zip -DestinationPath assets/
+
+ # No need to specify nuspec if there's only one in the directory.
+ choco pack --version=$version binary_path=assets/coder.exe
+
+ choco apikey --api-key $env:CHOCO_API_KEY --source https://push.chocolatey.org/
+
+ # No need to specify nupkg if there's only one in the directory.
+ choco push --source https://push.chocolatey.org/
+
+ env:
+ CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }}
+ # We need a GitHub token for the gh CLI to function under GitHub Actions
+ GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml
index daed19ee5dff5..04c3b1562147b 100644
--- a/.github/workflows/security.yaml
+++ b/.github/workflows/security.yaml
@@ -21,9 +21,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-security
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
-env:
- CODER_GO_VERSION: "1.20.6"
-
jobs:
codeql:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
@@ -69,16 +66,8 @@ jobs:
- name: Setup Go
uses: ./.github/actions/setup-go
- - name: Cache Node
- id: cache-node
- uses: buildjet/cache@v3
- with:
- path: |
- **/node_modules
- .eslintcache
- key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
- restore-keys: |
- js-${{ runner.os }}-
+ - name: Setup Node
+ uses: ./.github/actions/setup-node
- name: Setup sqlc
uses: ./.github/actions/setup-sqlc
diff --git a/.gitignore b/.gitignore
index b22db03c2089e..16c4b9a7aef94 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,3 +61,6 @@ site/stats/
./scaletest/terraform/.terraform.lock.hcl
scaletest/terraform/secrets.tfvars
.terraform.tfstate.*
+
+# Nix
+result
diff --git a/.golangci.yaml b/.golangci.yaml
index e3f3797d06b81..5f474602b2cfd 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -2,12 +2,16 @@
# Over time we should try tightening some of these.
linters-settings:
+ dupl:
+ # goal: 100
+ threshold: 412
+
exhaustruct:
include:
# Gradually extend to cover more of the codebase.
- 'httpmw\.\w+'
gocognit:
- min-complexity: 46 # Min code complexity (def 30).
+ min-complexity: 300
goconst:
min-len: 4 # Min length of string consts (def 3).
@@ -118,10 +122,6 @@ linters-settings:
goimports:
local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder
- gocyclo:
- # goal: 30
- min-complexity: 47
-
importas:
no-unaliased: true
@@ -211,6 +211,7 @@ issues:
run:
skip-dirs:
- node_modules
+ - .git
skip-files:
- scripts/rules.go
timeout: 10m
@@ -231,7 +232,11 @@ linters:
- exportloopref
- forcetypeassert
- gocritic
- - gocyclo
+ # gocyclo is may be useful in the future when we start caring
+ # about testing complexity, but for the time being we should
+ # create a good culture around cognitive complexity.
+ # - gocyclo
+ - gocognit
- goimports
- gomodguard
- gosec
@@ -267,3 +272,4 @@ linters:
- typecheck
- unconvert
- unused
+ - dupl
diff --git a/.prettierignore b/.prettierignore
index 9296d15d8802e..29a161fcb86f5 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -64,10 +64,13 @@ site/stats/
./scaletest/terraform/.terraform.lock.hcl
scaletest/terraform/secrets.tfvars
.terraform.tfstate.*
+
+# Nix
+result
# .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier.
-helm/templates/*.yaml
+helm/**/templates/*.yaml
# Terraform state files used in tests, these are automatically generated.
# Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json
diff --git a/.prettierignore.include b/.prettierignore.include
index 1f60eda9c54a7..975c00ca21b84 100644
--- a/.prettierignore.include
+++ b/.prettierignore.include
@@ -1,6 +1,6 @@
# Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier.
-helm/templates/*.yaml
+helm/**/templates/*.yaml
# Terraform state files used in tests, these are automatically generated.
# Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json
diff --git a/.prettierrc.yaml b/.prettierrc.yaml
index 9ba1d2ca9db7a..7fe31e7338ad4 100644
--- a/.prettierrc.yaml
+++ b/.prettierrc.yaml
@@ -2,6 +2,7 @@
# formatting for prettier-supported files. See `.editorconfig` and
# `site/.editorconfig`for whitespace formatting options.
printWidth: 80
+proseWrap: always
semi: false
trailingComma: all
useTabs: false
@@ -9,10 +10,9 @@ tabWidth: 2
overrides:
- files:
- README.md
+ - docs/api/**/*.md
+ - docs/cli/**/*.md
+ - .github/**/*.{yaml,yml,toml}
+ - scripts/**/*.{yaml,yml,toml}
options:
proseWrap: preserve
- - files:
- - "site/**/*.yaml"
- - "site/**/*.yml"
- options:
- proseWrap: always
diff --git a/.swaggo b/.swaggo
index e4b76f3ed82d9..bf8a6bad030c2 100644
--- a/.swaggo
+++ b/.swaggo
@@ -1,8 +1,8 @@
// Replace all NullTime with string
-replace github.com/coder/coder/codersdk.NullTime string
+replace github.com/coder/coder/v2/codersdk.NullTime string
// Prevent swaggo from rendering enums for time.Duration
replace time.Duration int64
// Do not expose "echo" provider
-replace github.com/coder/coder/codersdk.ProvisionerType string
+replace github.com/coder/coder/v2/codersdk.ProvisionerType string
// Do not render netip.Addr
replace netip.Addr string
diff --git a/Makefile b/Makefile
index c9089a9d4e452..56acd83ff70c8 100644
--- a/Makefile
+++ b/Makefile
@@ -344,15 +344,19 @@ push/$(CODER_MAIN_IMAGE): $(CODER_MAIN_IMAGE)
docker manifest push "$$image_tag"
.PHONY: push/$(CODER_MAIN_IMAGE)
+# Helm charts that are available
+charts = coder provisioner
+
# Shortcut for Helm chart package.
-build/coder_helm.tgz: build/coder_helm_$(VERSION).tgz
+$(foreach chart,$(charts),build/$(chart)_helm.tgz): build/%_helm.tgz: build/%_helm_$(VERSION).tgz
rm -f "$@"
ln "$<" "$@"
# Helm chart package.
-build/coder_helm_$(VERSION).tgz:
+$(foreach chart,$(charts),build/$(chart)_helm_$(VERSION).tgz): build/%_helm_$(VERSION).tgz:
./scripts/helm.sh \
--version "$(VERSION)" \
+ --chart $* \
--output "$@"
site/out/index.html: site/package.json $(shell find ./site $(FIND_EXCLUSIONS) -type f \( -name '*.ts' -o -name '*.tsx' \))
@@ -452,10 +456,10 @@ DB_GEN_FILES := \
# all gen targets should be added here and to gen/mark-fresh
gen: \
- coderd/database/dump.sql \
- $(DB_GEN_FILES) \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
+ coderd/database/dump.sql \
+ $(DB_GEN_FILES) \
site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \
docs/admin/prometheus.md \
@@ -466,17 +470,18 @@ gen: \
.prettierignore \
site/.prettierrc.yaml \
site/.prettierignore \
- site/.eslintignore
+ site/.eslintignore \
+ site/e2e/provisionerGenerated.ts
.PHONY: gen
# Mark all generated files as fresh so make thinks they're up-to-date. This is
# used during releases so we don't run generation scripts.
gen/mark-fresh:
files="\
- coderd/database/dump.sql \
- $(DB_GEN_FILES) \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
+ coderd/database/dump.sql \
+ $(DB_GEN_FILES) \
site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \
docs/admin/prometheus.md \
@@ -488,6 +493,7 @@ gen/mark-fresh:
site/.prettierrc.yaml \
site/.prettierignore \
site/.eslintignore \
+ site/e2e/provisionerGenerated.ts \
"
for file in $$files; do
echo "$$file"
@@ -532,7 +538,12 @@ provisionerd/proto/provisionerd.pb.go: provisionerd/proto/provisionerd.proto
site/src/api/typesGenerated.ts: scripts/apitypings/main.go $(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go')
go run scripts/apitypings/main.go > site/src/api/typesGenerated.ts
cd site
- pnpm run format:types
+ pnpm run format:types ./src/api/typesGenerated.ts
+
+site/e2e/provisionerGenerated.ts:
+ cd site
+ ../scripts/pnpm_install.sh
+ pnpm run gen:provisioner
coderd/rbac/object_gen.go: scripts/rbacgen/main.go coderd/rbac/object.go
go run scripts/rbacgen/main.go ./coderd/rbac > coderd/rbac/object_gen.go
@@ -553,7 +564,7 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS)
./scripts/apidocgen/generate.sh
pnpm run format:write:only ./docs/api ./docs/manifest.json ./coderd/apidoc/swagger.json
-update-golden-files: cli/testdata/.gen-golden helm/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden
+update-golden-files: cli/testdata/.gen-golden helm/coder/tests/testdata/.gen-golden helm/provisioner/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden coderd/.gen-golden
.PHONY: update-golden-files
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard cli/*_test.go)
@@ -564,8 +575,16 @@ enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden
go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
touch "$@"
-helm/tests/testdata/.gen-golden: $(wildcard helm/tests/testdata/*.yaml) $(wildcard helm/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/tests/*_test.go)
- go test ./helm/tests -run=TestUpdateGoldenFiles -update
+helm/coder/tests/testdata/.gen-golden: $(wildcard helm/coder/tests/testdata/*.yaml) $(wildcard helm/coder/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/coder/tests/*_test.go)
+ go test ./helm/coder/tests -run=TestUpdateGoldenFiles -update
+ touch "$@"
+
+helm/provisioner/tests/testdata/.gen-golden: $(wildcard helm/provisioner/tests/testdata/*.yaml) $(wildcard helm/provisioner/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/provisioner/tests/*_test.go)
+ go test ./helm/provisioner/tests -run=TestUpdateGoldenFiles -update
+ touch "$@"
+
+coderd/.gen-golden: $(wildcard coderd/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/*_test.go)
+ go test ./coderd -run="Test.*Golden$$" -update
touch "$@"
scripts/ci-report/testdata/.gen-golden: $(wildcard scripts/ci-report/testdata/*) $(wildcard scripts/ci-report/*.go)
diff --git a/README.md b/README.md
index 9443eb6b701fd..3f7d835125ff9 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,7 @@ You can run the install script with `--dry-run` to see the commands that will be
Once installed, you can start a production deployment1 with a single command:
-```console
+```shell
# Automatically sets up an external access URL on *.try.coder.app
coder server
diff --git a/SECURITY.md b/SECURITY.md
index 46986c9d3aadf..ee5ac8075eaf9 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,7 +1,7 @@
# Coder Security
-Coder welcomes feedback from security researchers and the general public
-to help improve our security. If you believe you have discovered a vulnerability,
+Coder welcomes feedback from security researchers and the general public to help
+improve our security. If you believe you have discovered a vulnerability,
privacy issue, exposed data, or other security issues in any of our assets, we
want to hear from you. This policy outlines steps for reporting vulnerabilities
to us, what we expect, what you can expect from us.
@@ -10,64 +10,72 @@ You can see the pretty version [here](https://coder.com/security/policy)
# Why Coder's security matters
-If an attacker could fully compromise a Coder installation, they could spin
-up expensive workstations, steal valuable credentials, or steal proprietary
-source code. We take this risk very seriously and employ routine pen testing,
-vulnerability scanning, and code reviews. We also welcome the contributions
-from the community that helped make this product possible.
+If an attacker could fully compromise a Coder installation, they could spin up
+expensive workstations, steal valuable credentials, or steal proprietary source
+code. We take this risk very seriously and employ routine pen testing,
+vulnerability scanning, and code reviews. We also welcome the contributions from
+the community that helped make this product possible.
# Where should I report security issues?
-Please report security issues to security@coder.com, providing
-all relevant information. The more details you provide, the easier it will be
-for us to triage and fix the issue.
+Please report security issues to security@coder.com, providing all relevant
+information. The more details you provide, the easier it will be for us to
+triage and fix the issue.
# Out of Scope
-Our primary concern is around an abuse of the Coder application that allows
-an attacker to gain access to another users workspace, or spin up unwanted
+Our primary concern is around an abuse of the Coder application that allows an
+attacker to gain access to another users workspace, or spin up unwanted
workspaces.
- DOS/DDOS attacks affecting availability --> While we do support rate limiting
- of requests, we primarily leave this to the owner of the Coder installation. Our
- rationale is that a DOS attack only affecting availability is not a valuable
- target for attackers.
+ of requests, we primarily leave this to the owner of the Coder installation.
+ Our rationale is that a DOS attack only affecting availability is not a
+ valuable target for attackers.
- Abuse of a compromised user credential --> If a user credential is compromised
- outside of the Coder ecosystem, then we consider it beyond the scope of our application.
- However, if an unprivileged user could escalate their permissions or gain access
- to another workspace, that is a cause for concern.
+ outside of the Coder ecosystem, then we consider it beyond the scope of our
+ application. However, if an unprivileged user could escalate their permissions
+ or gain access to another workspace, that is a cause for concern.
- Vulnerabilities in third party systems --> Vulnerabilities discovered in
- out-of-scope systems should be reported to the appropriate vendor or applicable authority.
+ out-of-scope systems should be reported to the appropriate vendor or
+ applicable authority.
# Our Commitments
When working with us, according to this policy, you can expect us to:
-- Respond to your report promptly, and work with you to understand and validate your report;
-- Strive to keep you informed about the progress of a vulnerability as it is processed;
-- Work to remediate discovered vulnerabilities in a timely manner, within our operational constraints; and
-- Extend Safe Harbor for your vulnerability research that is related to this policy.
+- Respond to your report promptly, and work with you to understand and validate
+ your report;
+- Strive to keep you informed about the progress of a vulnerability as it is
+ processed;
+- Work to remediate discovered vulnerabilities in a timely manner, within our
+ operational constraints; and
+- Extend Safe Harbor for your vulnerability research that is related to this
+ policy.
# Our Expectations
-In participating in our vulnerability disclosure program in good faith, we ask that you:
+In participating in our vulnerability disclosure program in good faith, we ask
+that you:
-- Play by the rules, including following this policy and any other relevant agreements.
- If there is any inconsistency between this policy and any other applicable terms, the
- terms of this policy will prevail;
+- Play by the rules, including following this policy and any other relevant
+ agreements. If there is any inconsistency between this policy and any other
+ applicable terms, the terms of this policy will prevail;
- Report any vulnerability you’ve discovered promptly;
-- Avoid violating the privacy of others, disrupting our systems, destroying data, and/or
- harming user experience;
+- Avoid violating the privacy of others, disrupting our systems, destroying
+ data, and/or harming user experience;
- Use only the Official Channels to discuss vulnerability information with us;
-- Provide us a reasonable amount of time (at least 90 days from the initial report) to
- resolve the issue before you disclose it publicly;
-- Perform testing only on in-scope systems, and respect systems and activities which
- are out-of-scope;
-- If a vulnerability provides unintended access to data: Limit the amount of data you
- access to the minimum required for effectively demonstrating a Proof of Concept; and
- cease testing and submit a report immediately if you encounter any user data during testing,
- such as Personally Identifiable Information (PII), Personal Healthcare Information (PHI),
- credit card data, or proprietary information;
-- You should only interact with test accounts you own or with explicit permission from
+- Provide us a reasonable amount of time (at least 90 days from the initial
+ report) to resolve the issue before you disclose it publicly;
+- Perform testing only on in-scope systems, and respect systems and activities
+ which are out-of-scope;
+- If a vulnerability provides unintended access to data: Limit the amount of
+ data you access to the minimum required for effectively demonstrating a Proof
+ of Concept; and cease testing and submit a report immediately if you encounter
+ any user data during testing, such as Personally Identifiable Information
+ (PII), Personal Healthcare Information (PHI), credit card data, or proprietary
+ information;
+- You should only interact with test accounts you own or with explicit
+ permission from
- the account holder; and
- Do not engage in extortion.
diff --git a/agent/agent.go b/agent/agent.go
index 52c423787fb44..532e7e5a88392 100644
--- a/agent/agent.go
+++ b/agent/agent.go
@@ -21,7 +21,7 @@ import (
"sync"
"time"
- "github.com/armon/circbuf"
+ "github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
@@ -34,14 +34,14 @@ import (
"tailscale.com/types/netlogtype"
"cdr.dev/slog"
- "github.com/coder/coder/agent/agentssh"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/pty"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/agent/agentssh"
+ "github.com/coder/coder/v2/agent/reconnectingpty"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/tailnet"
"github.com/coder/retry"
)
@@ -63,7 +63,7 @@ type Options struct {
IgnorePorts map[int]string
SSHMaxTimeout time.Duration
TailnetListenPort uint16
- Subsystem codersdk.AgentSubsystem
+ Subsystems []codersdk.AgentSubsystem
Addresses []netip.Prefix
PrometheusRegistry *prometheus.Registry
ReportMetadataInterval time.Duration
@@ -91,9 +91,6 @@ type Agent interface {
}
func New(options Options) Agent {
- if options.ReconnectingPTYTimeout == 0 {
- options.ReconnectingPTYTimeout = 5 * time.Minute
- }
if options.Filesystem == nil {
options.Filesystem = afero.NewOsFs()
}
@@ -144,7 +141,7 @@ func New(options Options) Agent {
reportMetadataInterval: options.ReportMetadataInterval,
serviceBannerRefreshInterval: options.ServiceBannerRefreshInterval,
sshMaxTimeout: options.SSHMaxTimeout,
- subsystem: options.Subsystem,
+ subsystems: options.Subsystems,
addresses: options.Addresses,
prometheusRegistry: prometheusRegistry,
@@ -166,7 +163,7 @@ type agent struct {
// listing all listening ports. This is helpful to hide ports that
// are used by the agent, that the user does not care about.
ignorePorts map[int]string
- subsystem codersdk.AgentSubsystem
+ subsystems []codersdk.AgentSubsystem
reconnectingPTYs sync.Map
reconnectingPTYTimeout time.Duration
@@ -608,7 +605,7 @@ func (a *agent) run(ctx context.Context) error {
err = a.client.PostStartup(ctx, agentsdk.PostStartupRequest{
Version: buildinfo.Version(),
ExpandedDirectory: manifest.Directory,
- Subsystem: a.subsystem,
+ Subsystems: a.subsystems,
})
if err != nil {
return xerrors.Errorf("update workspace agent version: %w", err)
@@ -657,7 +654,7 @@ func (a *agent) run(ctx context.Context) error {
select {
case err = <-scriptDone:
case <-timeout:
- a.logger.Warn(ctx, "script timed out", slog.F("lifecycle", "startup"), slog.F("timeout", manifest.ShutdownScriptTimeout))
+ a.logger.Warn(ctx, "script timed out", slog.F("lifecycle", "startup"), slog.F("timeout", manifest.StartupScriptTimeout))
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleStartTimeout)
err = <-scriptDone // The script can still complete after a timeout.
}
@@ -681,7 +678,7 @@ func (a *agent) run(ctx context.Context) error {
network := a.network
a.closeMutex.Unlock()
if network == nil {
- network, err = a.createTailnet(ctx, manifest.AgentID, manifest.DERPMap, manifest.DisableDirectConnections)
+ network, err = a.createTailnet(ctx, manifest.AgentID, manifest.DERPMap, manifest.DERPForceWebSockets, manifest.DisableDirectConnections)
if err != nil {
return xerrors.Errorf("create tailnet: %w", err)
}
@@ -704,8 +701,10 @@ func (a *agent) run(ctx context.Context) error {
if err != nil {
a.logger.Error(ctx, "update tailnet addresses", slog.Error(err))
}
- // Update the DERP map and allow/disallow direct connections.
+ // Update the DERP map, force WebSocket setting and allow/disallow
+ // direct connections.
network.SetDERPMap(manifest.DERPMap)
+ network.SetDERPForceWebSockets(manifest.DERPForceWebSockets)
network.SetBlockEndpoints(manifest.DisableDirectConnections)
}
@@ -759,13 +758,15 @@ func (a *agent) trackConnGoroutine(fn func()) error {
return nil
}
-func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *tailcfg.DERPMap, disableDirectConnections bool) (_ *tailnet.Conn, err error) {
+func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *tailcfg.DERPMap, derpForceWebSockets, disableDirectConnections bool) (_ *tailnet.Conn, err error) {
network, err := tailnet.NewConn(&tailnet.Options{
- Addresses: a.wireguardAddresses(agentID),
- DERPMap: derpMap,
- Logger: a.logger.Named("net.tailnet"),
- ListenPort: a.tailnetListenPort,
- BlockEndpoints: disableDirectConnections,
+ ID: agentID,
+ Addresses: a.wireguardAddresses(agentID),
+ DERPMap: derpMap,
+ DERPForceWebSockets: derpForceWebSockets,
+ Logger: a.logger.Named("net.tailnet"),
+ ListenPort: a.tailnetListenPort,
+ BlockEndpoints: disableDirectConnections,
})
if err != nil {
return nil, xerrors.Errorf("create tailnet: %w", err)
@@ -1074,8 +1075,8 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, m
defer a.connCountReconnectingPTY.Add(-1)
connectionID := uuid.NewString()
- logger = logger.With(slog.F("message_id", msg.ID), slog.F("connection_id", connectionID))
- logger.Debug(ctx, "starting handler")
+ connLogger := logger.With(slog.F("message_id", msg.ID), slog.F("connection_id", connectionID))
+ connLogger.Debug(ctx, "starting handler")
defer func() {
if err := retErr; err != nil {
@@ -1086,22 +1087,22 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, m
// If the agent is closed, we don't want to
// log this as an error since it's expected.
if closed {
- logger.Debug(ctx, "reconnecting PTY failed with session error (agent closed)", slog.Error(err))
+ connLogger.Debug(ctx, "reconnecting pty failed with attach error (agent closed)", slog.Error(err))
} else {
- logger.Error(ctx, "reconnecting PTY failed with session error", slog.Error(err))
+ connLogger.Error(ctx, "reconnecting pty failed with attach error", slog.Error(err))
}
}
- logger.Debug(ctx, "session closed")
+ connLogger.Debug(ctx, "reconnecting pty connection closed")
}()
- var rpty *reconnectingPTY
- sendConnected := make(chan *reconnectingPTY, 1)
+ var rpty reconnectingpty.ReconnectingPTY
+ sendConnected := make(chan reconnectingpty.ReconnectingPTY, 1)
// On store, reserve this ID to prevent multiple concurrent new connections.
waitReady, ok := a.reconnectingPTYs.LoadOrStore(msg.ID, sendConnected)
if ok {
close(sendConnected) // Unused.
- logger.Debug(ctx, "connecting to existing session")
- c, ok := waitReady.(chan *reconnectingPTY)
+ connLogger.Debug(ctx, "connecting to existing reconnecting pty")
+ c, ok := waitReady.(chan reconnectingpty.ReconnectingPTY)
if !ok {
return xerrors.Errorf("found invalid type in reconnecting pty map: %T", waitReady)
}
@@ -1111,7 +1112,7 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, m
}
c <- rpty // Put it back for the next reconnect.
} else {
- logger.Debug(ctx, "creating new session")
+ connLogger.Debug(ctx, "creating new reconnecting pty")
connected := false
defer func() {
@@ -1127,169 +1128,24 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, m
a.metrics.reconnectingPTYErrors.WithLabelValues("create_command").Add(1)
return xerrors.Errorf("create command: %w", err)
}
- cmd.Env = append(cmd.Env, "TERM=xterm-256color")
- // Default to buffer 64KiB.
- circularBuffer, err := circbuf.NewBuffer(64 << 10)
- if err != nil {
- return xerrors.Errorf("create circular buffer: %w", err)
- }
+ rpty = reconnectingpty.New(ctx, cmd, &reconnectingpty.Options{
+ Timeout: a.reconnectingPTYTimeout,
+ Metrics: a.metrics.reconnectingPTYErrors,
+ }, logger.With(slog.F("message_id", msg.ID)))
- ptty, process, err := pty.Start(cmd)
- if err != nil {
- a.metrics.reconnectingPTYErrors.WithLabelValues("start_command").Add(1)
- return xerrors.Errorf("start command: %w", err)
- }
-
- ctx, cancel := context.WithCancel(ctx)
- rpty = &reconnectingPTY{
- activeConns: map[string]net.Conn{
- // We have to put the connection in the map instantly otherwise
- // the connection won't be closed if the process instantly dies.
- connectionID: conn,
- },
- ptty: ptty,
- // Timeouts created with an after func can be reset!
- timeout: time.AfterFunc(a.reconnectingPTYTimeout, cancel),
- circularBuffer: circularBuffer,
- }
- // We don't need to separately monitor for the process exiting.
- // When it exits, our ptty.OutputReader() will return EOF after
- // reading all process output.
if err = a.trackConnGoroutine(func() {
- buffer := make([]byte, 1024)
- for {
- read, err := rpty.ptty.OutputReader().Read(buffer)
- if err != nil {
- // When the PTY is closed, this is triggered.
- // Error is typically a benign EOF, so only log for debugging.
- if errors.Is(err, io.EOF) {
- logger.Debug(ctx, "unable to read pty output, command might have exited", slog.Error(err))
- } else {
- logger.Warn(ctx, "unable to read pty output, command might have exited", slog.Error(err))
- a.metrics.reconnectingPTYErrors.WithLabelValues("output_reader").Add(1)
- }
- break
- }
- part := buffer[:read]
- rpty.circularBufferMutex.Lock()
- _, err = rpty.circularBuffer.Write(part)
- rpty.circularBufferMutex.Unlock()
- if err != nil {
- logger.Error(ctx, "write to circular buffer", slog.Error(err))
- break
- }
- rpty.activeConnsMutex.Lock()
- for cid, conn := range rpty.activeConns {
- _, err = conn.Write(part)
- if err != nil {
- logger.Warn(ctx,
- "error writing to active conn",
- slog.F("other_conn_id", cid),
- slog.Error(err),
- )
- a.metrics.reconnectingPTYErrors.WithLabelValues("write").Add(1)
- }
- }
- rpty.activeConnsMutex.Unlock()
- }
-
- // Cleanup the process, PTY, and delete it's
- // ID from memory.
- _ = process.Kill()
- rpty.Close()
+ rpty.Wait()
a.reconnectingPTYs.Delete(msg.ID)
}); err != nil {
- _ = process.Kill()
- _ = ptty.Close()
+ rpty.Close(err)
return xerrors.Errorf("start routine: %w", err)
}
+
connected = true
sendConnected <- rpty
}
- // Resize the PTY to initial height + width.
- err := rpty.ptty.Resize(msg.Height, msg.Width)
- if err != nil {
- // We can continue after this, it's not fatal!
- logger.Error(ctx, "reconnecting PTY initial resize failed, but will continue", slog.Error(err))
- a.metrics.reconnectingPTYErrors.WithLabelValues("resize").Add(1)
- }
- // Write any previously stored data for the TTY.
- rpty.circularBufferMutex.RLock()
- prevBuf := slices.Clone(rpty.circularBuffer.Bytes())
- rpty.circularBufferMutex.RUnlock()
- // Note that there is a small race here between writing buffered
- // data and storing conn in activeConns. This is likely a very minor
- // edge case, but we should look into ways to avoid it. Holding
- // activeConnsMutex would be one option, but holding this mutex
- // while also holding circularBufferMutex seems dangerous.
- _, err = conn.Write(prevBuf)
- if err != nil {
- a.metrics.reconnectingPTYErrors.WithLabelValues("write").Add(1)
- return xerrors.Errorf("write buffer to conn: %w", err)
- }
- // Multiple connections to the same TTY are permitted.
- // This could easily be used for terminal sharing, but
- // we do it because it's a nice user experience to
- // copy/paste a terminal URL and have it _just work_.
- rpty.activeConnsMutex.Lock()
- rpty.activeConns[connectionID] = conn
- rpty.activeConnsMutex.Unlock()
- // Resetting this timeout prevents the PTY from exiting.
- rpty.timeout.Reset(a.reconnectingPTYTimeout)
-
- ctx, cancelFunc := context.WithCancel(ctx)
- defer cancelFunc()
- heartbeat := time.NewTicker(a.reconnectingPTYTimeout / 2)
- defer heartbeat.Stop()
- go func() {
- // Keep updating the activity while this
- // connection is alive!
- for {
- select {
- case <-ctx.Done():
- return
- case <-heartbeat.C:
- }
- rpty.timeout.Reset(a.reconnectingPTYTimeout)
- }
- }()
- defer func() {
- // After this connection ends, remove it from
- // the PTYs active connections. If it isn't
- // removed, all PTY data will be sent to it.
- rpty.activeConnsMutex.Lock()
- delete(rpty.activeConns, connectionID)
- rpty.activeConnsMutex.Unlock()
- }()
- decoder := json.NewDecoder(conn)
- var req codersdk.ReconnectingPTYRequest
- for {
- err = decoder.Decode(&req)
- if xerrors.Is(err, io.EOF) {
- return nil
- }
- if err != nil {
- logger.Warn(ctx, "reconnecting PTY failed with read error", slog.Error(err))
- return nil
- }
- _, err = rpty.ptty.InputWriter().Write([]byte(req.Data))
- if err != nil {
- logger.Warn(ctx, "reconnecting PTY failed with write error", slog.Error(err))
- a.metrics.reconnectingPTYErrors.WithLabelValues("input_writer").Add(1)
- return nil
- }
- // Check if a resize needs to happen!
- if req.Height == 0 || req.Width == 0 {
- continue
- }
- err = rpty.ptty.Resize(req.Height, req.Width)
- if err != nil {
- // We can continue after this, it's not fatal!
- logger.Error(ctx, "reconnecting PTY resize failed, but will continue", slog.Error(err))
- a.metrics.reconnectingPTYErrors.WithLabelValues("resize").Add(1)
- }
- }
+ return rpty.Attach(ctx, connectionID, conn, msg.Height, msg.Width, connLogger)
}
// startReportingConnectionStats runs the connection stats reporting goroutine.
@@ -1408,24 +1264,57 @@ func (a *agent) isClosed() bool {
}
func (a *agent) HTTPDebug() http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ r := chi.NewRouter()
+
+ requireNetwork := func(w http.ResponseWriter) (*tailnet.Conn, bool) {
a.closeMutex.Lock()
network := a.network
a.closeMutex.Unlock()
if network == nil {
- w.WriteHeader(http.StatusOK)
+ w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte("network is not ready yet"))
+ return nil, false
+ }
+
+ return network, true
+ }
+
+ r.Get("/debug/magicsock", func(w http.ResponseWriter, r *http.Request) {
+ network, ok := requireNetwork(w)
+ if !ok {
return
}
+ network.MagicsockServeHTTPDebug(w, r)
+ })
- if r.URL.Path == "/debug/magicsock" {
- network.MagicsockServeHTTPDebug(w, r)
- } else {
- w.WriteHeader(http.StatusNotFound)
- _, _ = w.Write([]byte("404 not found"))
+ r.Get("/debug/magicsock/debug-logging/{state}", func(w http.ResponseWriter, r *http.Request) {
+ state := chi.URLParam(r, "state")
+ stateBool, err := strconv.ParseBool(state)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ _, _ = fmt.Fprintf(w, "invalid state %q, must be a boolean", state)
+ return
}
+
+ network, ok := requireNetwork(w)
+ if !ok {
+ return
+ }
+
+ network.MagicsockSetDebugLoggingEnabled(stateBool)
+ a.logger.Info(r.Context(), "updated magicsock debug logging due to debug request", slog.F("new_state", stateBool))
+
+ w.WriteHeader(http.StatusOK)
+ _, _ = fmt.Fprintf(w, "updated magicsock debug logging to %v", stateBool)
})
+
+ r.NotFound(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ _, _ = w.Write([]byte("404 not found"))
+ })
+
+ return r
}
func (a *agent) Close() error {
@@ -1507,31 +1396,6 @@ lifecycleWaitLoop:
return nil
}
-type reconnectingPTY struct {
- activeConnsMutex sync.Mutex
- activeConns map[string]net.Conn
-
- circularBuffer *circbuf.Buffer
- circularBufferMutex sync.RWMutex
- timeout *time.Timer
- ptty pty.PTYCmd
-}
-
-// Close ends all connections to the reconnecting
-// PTY and clear the circular buffer.
-func (r *reconnectingPTY) Close() {
- r.activeConnsMutex.Lock()
- defer r.activeConnsMutex.Unlock()
- for _, conn := range r.activeConns {
- _ = conn.Close()
- }
- _ = r.ptty.Close()
- r.circularBufferMutex.Lock()
- r.circularBuffer.Reset()
- r.circularBufferMutex.Unlock()
- r.timeout.Stop()
-}
-
// userHomeDir returns the home directory of the current user, giving
// priority to the $HOME environment variable.
func userHomeDir() (string, error) {
diff --git a/agent/agent_test.go b/agent/agent_test.go
index 34637992536b7..126e0f4fa4c97 100644
--- a/agent/agent_test.go
+++ b/agent/agent_test.go
@@ -1,7 +1,6 @@
package agent_test
import (
- "bufio"
"bytes"
"context"
"encoding/json"
@@ -12,6 +11,7 @@ import (
"net/http/httptest"
"net/netip"
"os"
+ "os/exec"
"os/user"
"path"
"path/filepath"
@@ -42,17 +42,17 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/agent/agentssh"
- "github.com/coder/coder/agent/agenttest"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/pty"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/tailnet/tailnettest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/agent/agentssh"
+ "github.com/coder/coder/v2/agent/agenttest"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/pty"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/tailnet/tailnettest"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
@@ -102,7 +102,7 @@ func TestAgent_Stats_ReconnectingPTY(t *testing.T) {
//nolint:dogsled
conn, _, stats, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
- ptyConn, err := conn.ReconnectingPTY(ctx, uuid.New(), 128, 128, "/bin/bash")
+ ptyConn, err := conn.ReconnectingPTY(ctx, uuid.New(), 128, 128, "bash")
require.NoError(t, err)
defer ptyConn.Close()
@@ -1587,8 +1587,8 @@ func TestAgent_Startup(t *testing.T) {
})
}
+//nolint:paralleltest // This test sets an environment variable.
func TestAgent_ReconnectingPTY(t *testing.T) {
- t.Parallel()
if runtime.GOOS == "windows" {
// This might be our implementation, or ConPTY itself.
// It's difficult to find extensive tests for it, so
@@ -1596,61 +1596,116 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
t.Skip("ConPTY appears to be inconsistent on Windows.")
}
- ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
- defer cancel()
+ backends := []string{"Buffered", "Screen"}
- //nolint:dogsled
- conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
- id := uuid.New()
- netConn, err := conn.ReconnectingPTY(ctx, id, 100, 100, "/bin/bash")
- require.NoError(t, err)
- defer netConn.Close()
+ _, err := exec.LookPath("screen")
+ hasScreen := err == nil
- bufRead := bufio.NewReader(netConn)
+ for _, backendType := range backends {
+ backendType := backendType
+ t.Run(backendType, func(t *testing.T) {
+ if backendType == "Screen" {
+ t.Parallel()
+ if runtime.GOOS != "linux" {
+ t.Skipf("`screen` is not supported on %s", runtime.GOOS)
+ } else if !hasScreen {
+ t.Skip("`screen` not found")
+ }
+ } else if hasScreen && runtime.GOOS == "linux" {
+ // Set up a PATH that does not have screen in it.
+ bashPath, err := exec.LookPath("bash")
+ require.NoError(t, err)
+ dir, err := os.MkdirTemp("/tmp", "coder-test-reconnecting-pty-PATH")
+ require.NoError(t, err, "create temp dir for reconnecting pty PATH")
+ err = os.Symlink(bashPath, filepath.Join(dir, "bash"))
+ require.NoError(t, err, "symlink bash into reconnecting pty PATH")
+ t.Setenv("PATH", dir)
+ } else {
+ t.Parallel()
+ }
- // Brief pause to reduce the likelihood that we send keystrokes while
- // the shell is simultaneously sending a prompt.
- time.Sleep(100 * time.Millisecond)
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
- data, err := json.Marshal(codersdk.ReconnectingPTYRequest{
- Data: "echo test\r\n",
- })
- require.NoError(t, err)
- _, err = netConn.Write(data)
- require.NoError(t, err)
+ //nolint:dogsled
+ conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
+ id := uuid.New()
+ netConn1, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash")
+ require.NoError(t, err)
+ defer netConn1.Close()
- expectLine := func(matcher func(string) bool) {
- for {
- line, err := bufRead.ReadString('\n')
+ // A second simultaneous connection.
+ netConn2, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash")
+ require.NoError(t, err)
+ defer netConn2.Close()
+
+ // Brief pause to reduce the likelihood that we send keystrokes while
+ // the shell is simultaneously sending a prompt.
+ time.Sleep(100 * time.Millisecond)
+
+ data, err := json.Marshal(codersdk.ReconnectingPTYRequest{
+ Data: "echo test\r\n",
+ })
+ require.NoError(t, err)
+ _, err = netConn1.Write(data)
require.NoError(t, err)
- if matcher(line) {
- break
+
+ matchEchoCommand := func(line string) bool {
+ return strings.Contains(line, "echo test")
+ }
+ matchEchoOutput := func(line string) bool {
+ return strings.Contains(line, "test") && !strings.Contains(line, "echo")
+ }
+ matchExitCommand := func(line string) bool {
+ return strings.Contains(line, "exit")
+ }
+ matchExitOutput := func(line string) bool {
+ return strings.Contains(line, "exit") || strings.Contains(line, "logout")
}
- }
- }
- matchEchoCommand := func(line string) bool {
- return strings.Contains(line, "echo test")
- }
- matchEchoOutput := func(line string) bool {
- return strings.Contains(line, "test") && !strings.Contains(line, "echo")
- }
+ // Once for typing the command...
+ require.NoError(t, testutil.ReadUntil(ctx, t, netConn1, matchEchoCommand), "find echo command")
+ // And another time for the actual output.
+ require.NoError(t, testutil.ReadUntil(ctx, t, netConn1, matchEchoOutput), "find echo output")
- // Once for typing the command...
- expectLine(matchEchoCommand)
- // And another time for the actual output.
- expectLine(matchEchoOutput)
+ // Same for the other connection.
+ require.NoError(t, testutil.ReadUntil(ctx, t, netConn2, matchEchoCommand), "find echo command")
+ require.NoError(t, testutil.ReadUntil(ctx, t, netConn2, matchEchoOutput), "find echo output")
- _ = netConn.Close()
- netConn, err = conn.ReconnectingPTY(ctx, id, 100, 100, "/bin/bash")
- require.NoError(t, err)
- defer netConn.Close()
+ _ = netConn1.Close()
+ _ = netConn2.Close()
+ netConn3, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash")
+ require.NoError(t, err)
+ defer netConn3.Close()
+
+ // Same output again!
+ require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchEchoCommand), "find echo command")
+ require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchEchoOutput), "find echo output")
+
+ // Exit should cause the connection to close.
+ data, err = json.Marshal(codersdk.ReconnectingPTYRequest{
+ Data: "exit\r\n",
+ })
+ require.NoError(t, err)
+ _, err = netConn3.Write(data)
+ require.NoError(t, err)
- bufRead = bufio.NewReader(netConn)
+ // Once for the input and again for the output.
+ require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchExitCommand), "find exit command")
+ require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchExitOutput), "find exit output")
+
+ // Wait for the connection to close.
+ require.ErrorIs(t, testutil.ReadUntil(ctx, t, netConn3, nil), io.EOF)
+
+ // Try a non-shell command. It should output then immediately exit.
+ netConn4, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "echo test")
+ require.NoError(t, err)
+ defer netConn4.Close()
- // Same output again!
- expectLine(matchEchoCommand)
- expectLine(matchEchoOutput)
+ require.NoError(t, testutil.ReadUntil(ctx, t, netConn4, matchEchoOutput), "find echo output")
+ require.ErrorIs(t, testutil.ReadUntil(ctx, t, netConn3, nil), io.EOF)
+ })
+ }
}
func TestAgent_Dial(t *testing.T) {
@@ -1932,6 +1987,96 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) {
}, testutil.WaitShort, testutil.IntervalFast)
}
+func TestAgent_DebugServer(t *testing.T) {
+ t.Parallel()
+
+ derpMap, _ := tailnettest.RunDERPAndSTUN(t)
+ //nolint:dogsled
+ conn, _, _, _, agnt := setupAgent(t, agentsdk.Manifest{
+ DERPMap: derpMap,
+ }, 0)
+
+ awaitReachableCtx := testutil.Context(t, testutil.WaitLong)
+ ok := conn.AwaitReachable(awaitReachableCtx)
+ require.True(t, ok)
+ _ = conn.Close()
+
+ srv := httptest.NewServer(agnt.HTTPDebug())
+ t.Cleanup(srv.Close)
+
+ t.Run("MagicsockDebug", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock", nil)
+ require.NoError(t, err)
+
+ res, err := srv.Client().Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+ require.Equal(t, http.StatusOK, res.StatusCode)
+
+ resBody, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(resBody), "magicsock ")
+ })
+
+ t.Run("MagicsockDebugLogging", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Enable", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/t", nil)
+ require.NoError(t, err)
+
+ res, err := srv.Client().Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+ require.Equal(t, http.StatusOK, res.StatusCode)
+
+ resBody, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(resBody), "updated magicsock debug logging to true")
+ })
+
+ t.Run("Disable", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/0", nil)
+ require.NoError(t, err)
+
+ res, err := srv.Client().Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+ require.Equal(t, http.StatusOK, res.StatusCode)
+
+ resBody, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(resBody), "updated magicsock debug logging to false")
+ })
+
+ t.Run("Invalid", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/magicsock/debug-logging/blah", nil)
+ require.NoError(t, err)
+
+ res, err := srv.Client().Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+ require.Equal(t, http.StatusBadRequest, res.StatusCode)
+
+ resBody, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(resBody), `invalid state "blah", must be a boolean`)
+ })
+ })
+}
+
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) (*ptytest.PTYCmd, pty.Process) {
//nolint:dogsled
agentConn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
@@ -2013,7 +2158,7 @@ func setupAgent(t *testing.T, metadata agentsdk.Manifest, ptyTimeout time.Durati
*agenttest.Client,
<-chan *agentsdk.Stats,
afero.Fs,
- io.Closer,
+ agent.Agent,
) {
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
if metadata.DERPMap == nil {
diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go
index 729cadd423ce2..46dabacd7f2c5 100644
--- a/agent/agentssh/agentssh.go
+++ b/agent/agentssh/agentssh.go
@@ -28,10 +28,10 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/agent/usershell"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/pty"
+ "github.com/coder/coder/v2/agent/usershell"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/pty"
)
const (
diff --git a/agent/agentssh/agentssh_internal_test.go b/agent/agentssh/agentssh_internal_test.go
index ba4295bdbc149..aa4cfe0236261 100644
--- a/agent/agentssh/agentssh_internal_test.go
+++ b/agent/agentssh/agentssh_internal_test.go
@@ -15,8 +15,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/pty"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/pty"
+ "github.com/coder/coder/v2/testutil"
"cdr.dev/slog/sloggers/slogtest"
)
diff --git a/agent/agentssh/agentssh_test.go b/agent/agentssh/agentssh_test.go
index 2467f1f221d6d..146da9b4c3bec 100644
--- a/agent/agentssh/agentssh_test.go
+++ b/agent/agentssh/agentssh_test.go
@@ -20,9 +20,9 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent/agentssh"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/agent/agentssh"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestMain(m *testing.M) {
diff --git a/agent/agentssh/x11_test.go b/agent/agentssh/x11_test.go
index 1fce885bab780..e5f3f62ddce74 100644
--- a/agent/agentssh/x11_test.go
+++ b/agent/agentssh/x11_test.go
@@ -19,9 +19,9 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent/agentssh"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent/agentssh"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestServer_X11(t *testing.T) {
diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go
index cc9d365b5331d..f8c69bf408869 100644
--- a/agent/agenttest/client.go
+++ b/agent/agenttest/client.go
@@ -13,10 +13,10 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/testutil"
)
func NewClient(t testing.TB,
diff --git a/agent/api.go b/agent/api.go
index c2cea963fbe66..0886b35bc0db1 100644
--- a/agent/api.go
+++ b/agent/api.go
@@ -5,10 +5,10 @@ import (
"sync"
"time"
- "github.com/go-chi/chi"
+ "github.com/go-chi/chi/v5"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
func (a *agent) apiHandler() http.Handler {
diff --git a/agent/apphealth.go b/agent/apphealth.go
index 3d93b6c85ac26..c32a9a6339668 100644
--- a/agent/apphealth.go
+++ b/agent/apphealth.go
@@ -10,8 +10,8 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/retry"
)
diff --git a/agent/apphealth_test.go b/agent/apphealth_test.go
index 20c0d152760fc..748a88356e2aa 100644
--- a/agent/apphealth_test.go
+++ b/agent/apphealth_test.go
@@ -13,11 +13,11 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestAppHealth_Healthy(t *testing.T) {
diff --git a/agent/metrics.go b/agent/metrics.go
index dc5fb6c018474..ddbe6f49beed1 100644
--- a/agent/metrics.go
+++ b/agent/metrics.go
@@ -11,7 +11,7 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/codersdk/agentsdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
)
type agentMetrics struct {
diff --git a/agent/ports_supported.go b/agent/ports_supported.go
index 7f9d30f3e9d05..81d177ee63de9 100644
--- a/agent/ports_supported.go
+++ b/agent/ports_supported.go
@@ -8,7 +8,7 @@ import (
"github.com/cakturk/go-netstat/netstat"
"golang.org/x/xerrors"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.WorkspaceAgentListeningPort, error) {
diff --git a/agent/ports_unsupported.go b/agent/ports_unsupported.go
index 0ab26ac299736..0af99d1dc79b4 100644
--- a/agent/ports_unsupported.go
+++ b/agent/ports_unsupported.go
@@ -2,7 +2,7 @@
package agent
-import "github.com/coder/coder/codersdk"
+import "github.com/coder/coder/v2/codersdk"
func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.WorkspaceAgentListeningPort, error) {
// Can't scan for ports on non-linux or non-windows_amd64 systems at the
diff --git a/agent/reaper/reaper_test.go b/agent/reaper/reaper_test.go
index 0509edb382c6b..84246fba0619b 100644
--- a/agent/reaper/reaper_test.go
+++ b/agent/reaper/reaper_test.go
@@ -14,8 +14,8 @@ import (
"github.com/hashicorp/go-reap"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/agent/reaper"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent/reaper"
+ "github.com/coder/coder/v2/testutil"
)
// TestReap checks that's the reaper is successfully reaping
diff --git a/agent/reconnectingpty/buffered.go b/agent/reconnectingpty/buffered.go
new file mode 100644
index 0000000000000..d53b22ffe2153
--- /dev/null
+++ b/agent/reconnectingpty/buffered.go
@@ -0,0 +1,241 @@
+package reconnectingpty
+
+import (
+ "context"
+ "errors"
+ "io"
+ "net"
+ "time"
+
+ "github.com/armon/circbuf"
+ "github.com/prometheus/client_golang/prometheus"
+ "golang.org/x/exp/slices"
+ "golang.org/x/xerrors"
+
+ "cdr.dev/slog"
+
+ "github.com/coder/coder/v2/pty"
+)
+
+// bufferedReconnectingPTY provides a reconnectable PTY by using a ring buffer to store
+// scrollback.
+type bufferedReconnectingPTY struct {
+ command *pty.Cmd
+
+ activeConns map[string]net.Conn
+ circularBuffer *circbuf.Buffer
+
+ ptty pty.PTYCmd
+ process pty.Process
+
+ metrics *prometheus.CounterVec
+
+ state *ptyState
+ // timer will close the reconnecting pty when it expires. The timer will be
+ // reset as long as there are active connections.
+ timer *time.Timer
+ timeout time.Duration
+}
+
+// newBuffered starts the buffered pty. If the context ends the process will be
+// killed.
+func newBuffered(ctx context.Context, cmd *pty.Cmd, options *Options, logger slog.Logger) *bufferedReconnectingPTY {
+ rpty := &bufferedReconnectingPTY{
+ activeConns: map[string]net.Conn{},
+ command: cmd,
+ metrics: options.Metrics,
+ state: newState(),
+ timeout: options.Timeout,
+ }
+
+ // Default to buffer 64KiB.
+ circularBuffer, err := circbuf.NewBuffer(64 << 10)
+ if err != nil {
+ rpty.state.setState(StateDone, xerrors.Errorf("create circular buffer: %w", err))
+ return rpty
+ }
+ rpty.circularBuffer = circularBuffer
+
+ // Add TERM then start the command with a pty. pty.Cmd duplicates Path as the
+ // first argument so remove it.
+ cmdWithEnv := pty.CommandContext(ctx, cmd.Path, cmd.Args[1:]...)
+ cmdWithEnv.Env = append(rpty.command.Env, "TERM=xterm-256color")
+ cmdWithEnv.Dir = rpty.command.Dir
+ ptty, process, err := pty.Start(cmdWithEnv)
+ if err != nil {
+ rpty.state.setState(StateDone, xerrors.Errorf("start pty: %w", err))
+ return rpty
+ }
+ rpty.ptty = ptty
+ rpty.process = process
+
+ go rpty.lifecycle(ctx, logger)
+
+ // Multiplex the output onto the circular buffer and each active connection.
+ // We do not need to separately monitor for the process exiting. When it
+ // exits, our ptty.OutputReader() will return EOF after reading all process
+ // output.
+ go func() {
+ buffer := make([]byte, 1024)
+ for {
+ read, err := ptty.OutputReader().Read(buffer)
+ if err != nil {
+ // When the PTY is closed, this is triggered.
+ // Error is typically a benign EOF, so only log for debugging.
+ if errors.Is(err, io.EOF) {
+ logger.Debug(ctx, "unable to read pty output, command might have exited", slog.Error(err))
+ } else {
+ logger.Warn(ctx, "unable to read pty output, command might have exited", slog.Error(err))
+ rpty.metrics.WithLabelValues("output_reader").Add(1)
+ }
+ // Could have been killed externally or failed to start at all (command
+ // not found for example).
+ // TODO: Should we check the process's exit code in case the command was
+ // invalid?
+ rpty.Close(nil)
+ break
+ }
+ part := buffer[:read]
+ rpty.state.cond.L.Lock()
+ _, err = rpty.circularBuffer.Write(part)
+ if err != nil {
+ logger.Error(ctx, "write to circular buffer", slog.Error(err))
+ rpty.metrics.WithLabelValues("write_buffer").Add(1)
+ }
+ // TODO: Instead of ranging over a map, could we send the output to a
+ // channel and have each individual Attach read from that?
+ for cid, conn := range rpty.activeConns {
+ _, err = conn.Write(part)
+ if err != nil {
+ logger.Warn(ctx,
+ "error writing to active connection",
+ slog.F("connection_id", cid),
+ slog.Error(err),
+ )
+ rpty.metrics.WithLabelValues("write").Add(1)
+ }
+ }
+ rpty.state.cond.L.Unlock()
+ }
+ }()
+
+ return rpty
+}
+
+// lifecycle manages the lifecycle of the reconnecting pty. If the context ends
+// or the reconnecting pty closes the pty will be shut down.
+func (rpty *bufferedReconnectingPTY) lifecycle(ctx context.Context, logger slog.Logger) {
+ rpty.timer = time.AfterFunc(attachTimeout, func() {
+ rpty.Close(xerrors.New("reconnecting pty timeout"))
+ })
+
+ logger.Debug(ctx, "reconnecting pty ready")
+ rpty.state.setState(StateReady, nil)
+
+ state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing)
+ if state < StateClosing {
+ // If we have not closed yet then the context is what unblocked us (which
+ // means the agent is shutting down) so move into the closing phase.
+ rpty.Close(reasonErr)
+ }
+ rpty.timer.Stop()
+
+ rpty.state.cond.L.Lock()
+ // Log these closes only for debugging since the connections or processes
+ // might have already closed on their own.
+ for _, conn := range rpty.activeConns {
+ err := conn.Close()
+ if err != nil {
+ logger.Debug(ctx, "closed conn with error", slog.Error(err))
+ }
+ }
+ // Connections get removed once they close but it is possible there is still
+ // some data that will be written before that happens so clear the map now to
+ // avoid writing to closed connections.
+ rpty.activeConns = map[string]net.Conn{}
+ rpty.state.cond.L.Unlock()
+
+ // Log close/kill only for debugging since the process might have already
+ // closed on its own.
+ err := rpty.ptty.Close()
+ if err != nil {
+ logger.Debug(ctx, "closed ptty with error", slog.Error(err))
+ }
+
+ err = rpty.process.Kill()
+ if err != nil {
+ logger.Debug(ctx, "killed process with error", slog.Error(err))
+ }
+
+ logger.Info(ctx, "closed reconnecting pty")
+ rpty.state.setState(StateDone, reasonErr)
+}
+
+func (rpty *bufferedReconnectingPTY) Attach(ctx context.Context, connID string, conn net.Conn, height, width uint16, logger slog.Logger) error {
+ logger.Info(ctx, "attach to reconnecting pty")
+
+ // This will kill the heartbeat once we hit EOF or an error.
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ err := rpty.doAttach(connID, conn)
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ rpty.state.cond.L.Lock()
+ defer rpty.state.cond.L.Unlock()
+ delete(rpty.activeConns, connID)
+ }()
+
+ state, err := rpty.state.waitForStateOrContext(ctx, StateReady)
+ if state != StateReady {
+ return err
+ }
+
+ go heartbeat(ctx, rpty.timer, rpty.timeout)
+
+ // Resize the PTY to initial height + width.
+ err = rpty.ptty.Resize(height, width)
+ if err != nil {
+ // We can continue after this, it's not fatal!
+ logger.Warn(ctx, "reconnecting PTY initial resize failed, but will continue", slog.Error(err))
+ rpty.metrics.WithLabelValues("resize").Add(1)
+ }
+
+ // Pipe conn -> pty and block. pty -> conn is handled in newBuffered().
+ readConnLoop(ctx, conn, rpty.ptty, rpty.metrics, logger)
+ return nil
+}
+
+// doAttach adds the connection to the map and replays the buffer. It exists
+// separately only for convenience to defer the mutex unlock which is not
+// possible in Attach since it blocks.
+func (rpty *bufferedReconnectingPTY) doAttach(connID string, conn net.Conn) error {
+ rpty.state.cond.L.Lock()
+ defer rpty.state.cond.L.Unlock()
+
+ // Write any previously stored data for the TTY. Since the command might be
+ // short-lived and have already exited, make sure we always at least output
+ // the buffer before returning, mostly just so tests pass.
+ prevBuf := slices.Clone(rpty.circularBuffer.Bytes())
+ _, err := conn.Write(prevBuf)
+ if err != nil {
+ rpty.metrics.WithLabelValues("write").Add(1)
+ return xerrors.Errorf("write buffer to conn: %w", err)
+ }
+
+ rpty.activeConns[connID] = conn
+
+ return nil
+}
+
+func (rpty *bufferedReconnectingPTY) Wait() {
+ _, _ = rpty.state.waitForState(StateClosing)
+}
+
+func (rpty *bufferedReconnectingPTY) Close(error error) {
+ // The closing state change will be handled by the lifecycle.
+ rpty.state.setState(StateClosing, error)
+}
diff --git a/agent/reconnectingpty/reconnectingpty.go b/agent/reconnectingpty/reconnectingpty.go
new file mode 100644
index 0000000000000..30b1f44801b9f
--- /dev/null
+++ b/agent/reconnectingpty/reconnectingpty.go
@@ -0,0 +1,226 @@
+package reconnectingpty
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net"
+ "os/exec"
+ "runtime"
+ "sync"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "golang.org/x/xerrors"
+
+ "cdr.dev/slog"
+
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty"
+)
+
+// attachTimeout is the initial timeout for attaching and will probably be far
+// shorter than the reconnect timeout in most cases; in tests it might be
+// longer. It should be at least long enough for the first screen attach to be
+// able to start up the daemon and for the buffered pty to start.
+const attachTimeout = 30 * time.Second
+
+// Options allows configuring the reconnecting pty.
+type Options struct {
+ // Timeout describes how long to keep the pty alive without any connections.
+ // Once elapsed the pty will be killed.
+ Timeout time.Duration
+ // Metrics tracks various error counters.
+ Metrics *prometheus.CounterVec
+}
+
+// ReconnectingPTY is a pty that can be reconnected within a timeout and to
+// simultaneous connections. The reconnecting pty can be backed by screen if
+// installed or a (buggy) buffer replay fallback.
+type ReconnectingPTY interface {
+ // Attach pipes the connection and pty, spawning it if necessary, replays
+ // history, then blocks until EOF, an error, or the context's end. The
+ // connection is expected to send JSON-encoded messages and accept raw output
+ // from the ptty. If the context ends or the process dies the connection will
+ // be detached.
+ Attach(ctx context.Context, connID string, conn net.Conn, height, width uint16, logger slog.Logger) error
+ // Wait waits for the reconnecting pty to close. The underlying process might
+ // still be exiting.
+ Wait()
+ // Close kills the reconnecting pty process.
+ Close(err error)
+}
+
+// New sets up a new reconnecting pty that wraps the provided command. Any
+// errors with starting are returned on Attach(). The reconnecting pty will
+// close itself (and all connections to it) if nothing is attached for the
+// duration of the timeout, if the context ends, or the process exits (buffered
+// backend only).
+func New(ctx context.Context, cmd *pty.Cmd, options *Options, logger slog.Logger) ReconnectingPTY {
+ if options.Timeout == 0 {
+ options.Timeout = 5 * time.Minute
+ }
+ // Screen seems flaky on Darwin. Locally the tests pass 100% of the time (100
+ // runs) but in CI screen often incorrectly claims the session name does not
+ // exist even though screen -list shows it. For now, restrict screen to
+ // Linux.
+ backendType := "buffered"
+ if runtime.GOOS == "linux" {
+ _, err := exec.LookPath("screen")
+ if err == nil {
+ backendType = "screen"
+ }
+ }
+
+ logger.Info(ctx, "start reconnecting pty", slog.F("backend_type", backendType))
+
+ switch backendType {
+ case "screen":
+ return newScreen(ctx, cmd, options, logger)
+ default:
+ return newBuffered(ctx, cmd, options, logger)
+ }
+}
+
+// heartbeat resets timer before timeout elapses and blocks until ctx ends.
+func heartbeat(ctx context.Context, timer *time.Timer, timeout time.Duration) {
+ // Reset now in case it is near the end.
+ timer.Reset(timeout)
+
+ // Reset when the context ends to ensure the pty stays up for the full
+ // timeout.
+ defer timer.Reset(timeout)
+
+ heartbeat := time.NewTicker(timeout / 2)
+ defer heartbeat.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-heartbeat.C:
+ timer.Reset(timeout)
+ }
+ }
+}
+
+// State represents the current state of the reconnecting pty. States are
+// sequential and will only move forward.
+type State int
+
+const (
+ // StateStarting is the default/start state. Attaching will block until the
+ // reconnecting pty becomes ready.
+ StateStarting = iota
+ // StateReady means the reconnecting pty is ready to be attached.
+ StateReady
+ // StateClosing means the reconnecting pty has begun closing. The underlying
+ // process may still be exiting. Attaching will result in an error.
+ StateClosing
+ // StateDone means the reconnecting pty has completely shut down and the
+ // process has exited. Attaching will result in an error.
+ StateDone
+)
+
+// ptyState is a helper for tracking the reconnecting PTY's state.
+type ptyState struct {
+ // cond broadcasts state changes and any accompanying errors.
+ cond *sync.Cond
+ // error describes the error that caused the state change, if there was one.
+ // It is not safe to access outside of cond.L.
+ error error
+ // state holds the current reconnecting pty state. It is not safe to access
+ // this outside of cond.L.
+ state State
+}
+
+func newState() *ptyState {
+ return &ptyState{
+ cond: sync.NewCond(&sync.Mutex{}),
+ state: StateStarting,
+ }
+}
+
+// setState sets and broadcasts the provided state if it is greater than the
+// current state and the error if one has not already been set.
+func (s *ptyState) setState(state State, err error) {
+ s.cond.L.Lock()
+ defer s.cond.L.Unlock()
+ // Cannot regress states. For example, trying to close after the process is
+ // done should leave us in the done state and not the closing state.
+ if state <= s.state {
+ return
+ }
+ s.error = err
+ s.state = state
+ s.cond.Broadcast()
+}
+
+// waitForState blocks until the state or a greater one is reached.
+func (s *ptyState) waitForState(state State) (State, error) {
+ s.cond.L.Lock()
+ defer s.cond.L.Unlock()
+ for state > s.state {
+ s.cond.Wait()
+ }
+ return s.state, s.error
+}
+
+// waitForStateOrContext blocks until the state or a greater one is reached or
+// the provided context ends.
+func (s *ptyState) waitForStateOrContext(ctx context.Context, state State) (State, error) {
+ s.cond.L.Lock()
+ defer s.cond.L.Unlock()
+
+ nevermind := make(chan struct{})
+ defer close(nevermind)
+ go func() {
+ select {
+ case <-ctx.Done():
+ // Wake up when the context ends.
+ s.cond.Broadcast()
+ case <-nevermind:
+ }
+ }()
+
+ for ctx.Err() == nil && state > s.state {
+ s.cond.Wait()
+ }
+ if ctx.Err() != nil {
+ return s.state, ctx.Err()
+ }
+ return s.state, s.error
+}
+
+// readConnLoop reads messages from conn and writes to ptty as needed. Blocks
+// until EOF or an error writing to ptty or reading from conn.
+func readConnLoop(ctx context.Context, conn net.Conn, ptty pty.PTYCmd, metrics *prometheus.CounterVec, logger slog.Logger) {
+ decoder := json.NewDecoder(conn)
+ var req codersdk.ReconnectingPTYRequest
+ for {
+ err := decoder.Decode(&req)
+ if xerrors.Is(err, io.EOF) {
+ return
+ }
+ if err != nil {
+ logger.Warn(ctx, "reconnecting pty failed with read error", slog.Error(err))
+ return
+ }
+ _, err = ptty.InputWriter().Write([]byte(req.Data))
+ if err != nil {
+ logger.Warn(ctx, "reconnecting pty failed with write error", slog.Error(err))
+ metrics.WithLabelValues("input_writer").Add(1)
+ return
+ }
+ // Check if a resize needs to happen!
+ if req.Height == 0 || req.Width == 0 {
+ continue
+ }
+ err = ptty.Resize(req.Height, req.Width)
+ if err != nil {
+ // We can continue after this, it's not fatal!
+ logger.Warn(ctx, "reconnecting pty resize failed, but will continue", slog.Error(err))
+ metrics.WithLabelValues("resize").Add(1)
+ }
+ }
+}
diff --git a/agent/reconnectingpty/screen.go b/agent/reconnectingpty/screen.go
new file mode 100644
index 0000000000000..a2db7bb9c001e
--- /dev/null
+++ b/agent/reconnectingpty/screen.go
@@ -0,0 +1,388 @@
+package reconnectingpty
+
+import (
+ "bytes"
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "io"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gliderlabs/ssh"
+ "github.com/prometheus/client_golang/prometheus"
+ "golang.org/x/xerrors"
+
+ "cdr.dev/slog"
+ "github.com/coder/coder/v2/pty"
+)
+
+// screenReconnectingPTY provides a reconnectable PTY via `screen`.
+type screenReconnectingPTY struct {
+ command *pty.Cmd
+
+ // id holds the id of the session for both creating and attaching. This will
+ // be generated uniquely for each session because without control of the
+ // screen daemon we do not have its PID and without the PID screen will do
+ // partial matching. Enforcing a unique ID should guarantee we match on the
+ // right session.
+ id string
+
+ // mutex prevents concurrent attaches to the session. Screen will happily
+ // spawn two separate sessions with the same name if multiple attaches happen
+ // in a close enough interval. We are not able to control the screen daemon
+ // ourselves to prevent this because the daemon will spawn with a hardcoded
+ // 24x80 size which results in confusing padding above the prompt once the
+ // attach comes in and resizes.
+ mutex sync.Mutex
+
+ configFile string
+
+ metrics *prometheus.CounterVec
+
+ state *ptyState
+ // timer will close the reconnecting pty when it expires. The timer will be
+ // reset as long as there are active connections.
+ timer *time.Timer
+ timeout time.Duration
+}
+
+// newScreen creates a new screen-backed reconnecting PTY. It writes config
+// settings and creates the socket directory. If we could, we would want to
+// spawn the daemon here and attach each connection to it but since doing that
+// spawns the daemon with a hardcoded 24x80 size it is not a very good user
+// experience. Instead we will let the attach command spawn the daemon on its
+// own which causes it to spawn with the specified size.
+func newScreen(ctx context.Context, cmd *pty.Cmd, options *Options, logger slog.Logger) *screenReconnectingPTY {
+ rpty := &screenReconnectingPTY{
+ command: cmd,
+ metrics: options.Metrics,
+ state: newState(),
+ timeout: options.Timeout,
+ }
+
+ go rpty.lifecycle(ctx, logger)
+
+ // Socket paths are limited to around 100 characters on Linux and macOS which
+ // depending on the temporary directory can be a problem. To give more leeway
+ // use a short ID.
+ buf := make([]byte, 4)
+ _, err := rand.Read(buf)
+ if err != nil {
+ rpty.state.setState(StateDone, xerrors.Errorf("generate screen id: %w", err))
+ return rpty
+ }
+ rpty.id = hex.EncodeToString(buf)
+
+ settings := []string{
+ // Tell screen not to handle motion for xterm* terminals which allows
+ // scrolling the terminal via the mouse wheel or scroll bar (by default
+ // screen uses it to cycle through the command history). There does not
+ // seem to be a way to make screen itself scroll on mouse wheel. tmux can
+ // do it but then there is no scroll bar and it kicks you into copy mode
+ // where keys stop working until you exit copy mode which seems like it
+ // could be confusing.
+ "termcapinfo xterm* ti@:te@",
+ // Enable alternate screen emulation otherwise applications get rendered in
+ // the current window which wipes out visible output resulting in missing
+ // output when scrolling back with the mouse wheel (copy mode still works
+ // since that is screen itself scrolling).
+ "altscreen on",
+ // Remap the control key to C-s since C-a may be used in applications. C-s
+ // is chosen because it cannot actually be used because by default it will
+ // pause and C-q to resume will just kill the browser window. We may not
+ // want people using the control key anyway since it will not be obvious
+ // they are in screen and doing things like switching windows makes mouse
+ // wheel scroll wonky due to the terminal doing the scrolling rather than
+ // screen itself (but again copy mode will work just fine).
+ "escape ^Ss",
+ }
+
+ rpty.configFile = filepath.Join(os.TempDir(), "coder-screen", "config")
+ err = os.MkdirAll(filepath.Dir(rpty.configFile), 0o700)
+ if err != nil {
+ rpty.state.setState(StateDone, xerrors.Errorf("make screen config dir: %w", err))
+ return rpty
+ }
+
+ err = os.WriteFile(rpty.configFile, []byte(strings.Join(settings, "\n")), 0o600)
+ if err != nil {
+ rpty.state.setState(StateDone, xerrors.Errorf("create config file: %w", err))
+ return rpty
+ }
+
+ return rpty
+}
+
+// lifecycle manages the lifecycle of the reconnecting pty. If the context ends
+// the reconnecting pty will be closed.
+func (rpty *screenReconnectingPTY) lifecycle(ctx context.Context, logger slog.Logger) {
+ rpty.timer = time.AfterFunc(attachTimeout, func() {
+ rpty.Close(xerrors.New("reconnecting pty timeout"))
+ })
+
+ logger.Debug(ctx, "reconnecting pty ready")
+ rpty.state.setState(StateReady, nil)
+
+ state, reasonErr := rpty.state.waitForStateOrContext(ctx, StateClosing)
+ if state < StateClosing {
+ // If we have not closed yet then the context is what unblocked us (which
+ // means the agent is shutting down) so move into the closing phase.
+ rpty.Close(reasonErr)
+ }
+ rpty.timer.Stop()
+
+ // If the command errors that the session is already gone that is fine.
+ err := rpty.sendCommand(context.Background(), "quit", []string{"No screen session found"})
+ if err != nil {
+ logger.Error(ctx, "close screen session", slog.Error(err))
+ }
+
+ logger.Info(ctx, "closed reconnecting pty")
+ rpty.state.setState(StateDone, reasonErr)
+}
+
+func (rpty *screenReconnectingPTY) Attach(ctx context.Context, _ string, conn net.Conn, height, width uint16, logger slog.Logger) error {
+ logger.Info(ctx, "attach to reconnecting pty")
+
+ // This will kill the heartbeat once we hit EOF or an error.
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ state, err := rpty.state.waitForStateOrContext(ctx, StateReady)
+ if state != StateReady {
+ return err
+ }
+
+ go heartbeat(ctx, rpty.timer, rpty.timeout)
+
+ ptty, process, err := rpty.doAttach(ctx, conn, height, width, logger)
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ // Likely the process was too short-lived and canceled the version command.
+ // TODO: Is it worth distinguishing between that and a cancel from the
+ // Attach() caller? Additionally, since this could also happen if
+ // the command was invalid, should we check the process's exit code?
+ return nil
+ }
+ return err
+ }
+
+ defer func() {
+ // Log only for debugging since the process might have already exited on its
+ // own.
+ err := ptty.Close()
+ if err != nil {
+ logger.Debug(ctx, "closed ptty with error", slog.Error(err))
+ }
+ err = process.Kill()
+ if err != nil {
+ logger.Debug(ctx, "killed process with error", slog.Error(err))
+ }
+ }()
+
+ // Pipe conn -> pty and block.
+ readConnLoop(ctx, conn, ptty, rpty.metrics, logger)
+ return nil
+}
+
+// doAttach spawns the screen client and starts the heartbeat. It exists
+// separately only so we can defer the mutex unlock which is not possible in
+// Attach since it blocks.
+func (rpty *screenReconnectingPTY) doAttach(ctx context.Context, conn net.Conn, height, width uint16, logger slog.Logger) (pty.PTYCmd, pty.Process, error) {
+ // Ensure another attach does not come in and spawn a duplicate session.
+ rpty.mutex.Lock()
+ defer rpty.mutex.Unlock()
+
+ logger.Debug(ctx, "spawning screen client", slog.F("screen_id", rpty.id))
+
+ // Wrap the command with screen and tie it to the connection's context.
+ cmd := pty.CommandContext(ctx, "screen", append([]string{
+ // -S is for setting the session's name.
+ "-S", rpty.id,
+ // -x allows attaching to an already attached session.
+ // -RR reattaches to the daemon or creates the session daemon if missing.
+ // -q disables the "New screen..." message that appears for five seconds
+ // when creating a new session with -RR.
+ // -c is the flag for the config file.
+ "-xRRqc", rpty.configFile,
+ rpty.command.Path,
+ // pty.Cmd duplicates Path as the first argument so remove it.
+ }, rpty.command.Args[1:]...)...)
+ cmd.Env = append(rpty.command.Env, "TERM=xterm-256color")
+ cmd.Dir = rpty.command.Dir
+ ptty, process, err := pty.Start(cmd, pty.WithPTYOption(
+ pty.WithSSHRequest(ssh.Pty{
+ Window: ssh.Window{
+ // Make sure to spawn at the right size because if we resize afterward it
+ // leaves confusing padding (screen will resize such that the screen
+ // contents are aligned to the bottom).
+ Height: int(height),
+ Width: int(width),
+ },
+ }),
+ ))
+ if err != nil {
+ rpty.metrics.WithLabelValues("screen_spawn").Add(1)
+ return nil, nil, err
+ }
+
+ // This context lets us abort the version command if the process dies.
+ versionCtx, versionCancel := context.WithCancel(ctx)
+ defer versionCancel()
+
+ // Pipe pty -> conn and close the connection when the process exits.
+ // We do not need to separately monitor for the process exiting. When it
+ // exits, our ptty.OutputReader() will return EOF after reading all process
+ // output.
+ go func() {
+ defer versionCancel()
+ defer func() {
+ err := conn.Close()
+ if err != nil {
+ // Log only for debugging since the connection might have already closed
+ // on its own.
+ logger.Debug(ctx, "closed connection with error", slog.Error(err))
+ }
+ }()
+ buffer := make([]byte, 1024)
+ for {
+ read, err := ptty.OutputReader().Read(buffer)
+ if err != nil {
+ // When the PTY is closed, this is triggered.
+ // Error is typically a benign EOF, so only log for debugging.
+ if errors.Is(err, io.EOF) {
+ logger.Debug(ctx, "unable to read pty output; screen might have exited", slog.Error(err))
+ } else {
+ logger.Warn(ctx, "unable to read pty output; screen might have exited", slog.Error(err))
+ rpty.metrics.WithLabelValues("screen_output_reader").Add(1)
+ }
+ // The process might have died because the session itself died or it
+ // might have been separately killed and the session is still up (for
+ // example `exit` or we killed it when the connection closed). If the
+ // session is still up we might leave the reconnecting pty in memory
+ // around longer than it needs to be but it will eventually clean up
+ // with the timer or context, or the next attach will respawn the screen
+ // daemon which is fine too.
+ break
+ }
+ part := buffer[:read]
+ _, err = conn.Write(part)
+ if err != nil {
+ // Connection might have been closed.
+ if errors.Unwrap(err).Error() != "endpoint is closed for send" {
+ logger.Warn(ctx, "error writing to active conn", slog.Error(err))
+ rpty.metrics.WithLabelValues("screen_write").Add(1)
+ }
+ break
+ }
+ }
+ }()
+
+ // Version seems to be the only command without a side effect (other than
+ // making the version pop up briefly) so use it to wait for the session to
+ // come up. If we do not wait we could end up spawning multiple sessions with
+ // the same name.
+ err = rpty.sendCommand(versionCtx, "version", nil)
+ if err != nil {
+ // Log only for debugging since the process might already have closed.
+ closeErr := ptty.Close()
+ if closeErr != nil {
+ logger.Debug(ctx, "closed ptty with error", slog.Error(closeErr))
+ }
+ closeErr = process.Kill()
+ if closeErr != nil {
+ logger.Debug(ctx, "killed process with error", slog.Error(closeErr))
+ }
+ rpty.metrics.WithLabelValues("screen_wait").Add(1)
+ return nil, nil, err
+ }
+
+ return ptty, process, nil
+}
+
+// sendCommand runs a screen command against a running screen session. If the
+// command fails with an error matching anything in successErrors it will be
+// considered a success state (for example "no session" when quitting and the
+// session is already dead). The command will be retried until successful, the
+// timeout is reached, or the context ends. A canceled context will return the
+// canceled context's error as-is while a timed-out context returns together
+// with the last error from the command.
+func (rpty *screenReconnectingPTY) sendCommand(ctx context.Context, command string, successErrors []string) error {
+ ctx, cancel := context.WithTimeout(ctx, attachTimeout)
+ defer cancel()
+
+ var lastErr error
+ run := func() bool {
+ var stdout bytes.Buffer
+ //nolint:gosec
+ cmd := exec.CommandContext(ctx, "screen",
+ // -x targets an attached session.
+ "-x", rpty.id,
+ // -c is the flag for the config file.
+ "-c", rpty.configFile,
+ // -X runs a command in the matching session.
+ "-X", command,
+ )
+ cmd.Env = append(rpty.command.Env, "TERM=xterm-256color")
+ cmd.Dir = rpty.command.Dir
+ cmd.Stdout = &stdout
+ err := cmd.Run()
+ if err == nil {
+ return true
+ }
+
+ stdoutStr := stdout.String()
+ for _, se := range successErrors {
+ if strings.Contains(stdoutStr, se) {
+ return true
+ }
+ }
+
+ // Things like "exit status 1" are imprecise so include stdout as it may
+ // contain more information ("no screen session found" for example).
+ if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
+ lastErr = xerrors.Errorf("`screen -x %s -X %s`: %w: %s", rpty.id, command, err, stdoutStr)
+ }
+
+ return false
+ }
+
+ // Run immediately.
+ if done := run(); done {
+ return nil
+ }
+
+ // Then run on an interval.
+ ticker := time.NewTicker(250 * time.Millisecond)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ if errors.Is(ctx.Err(), context.Canceled) {
+ return ctx.Err()
+ }
+ return errors.Join(ctx.Err(), lastErr)
+ case <-ticker.C:
+ if done := run(); done {
+ return nil
+ }
+ }
+ }
+}
+
+func (rpty *screenReconnectingPTY) Wait() {
+ _, _ = rpty.state.waitForState(StateClosing)
+}
+
+func (rpty *screenReconnectingPTY) Close(err error) {
+ // The closing state change will be handled by the lifecycle.
+ rpty.state.setState(StateClosing, err)
+}
diff --git a/agent/usershell/usershell_test.go b/agent/usershell/usershell_test.go
index 676ee462ffe63..ee49afcb14412 100644
--- a/agent/usershell/usershell_test.go
+++ b/agent/usershell/usershell_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/agent/usershell"
+ "github.com/coder/coder/v2/agent/usershell"
)
//nolint:paralleltest,tparallel // This test sets an environment variable.
diff --git a/buildinfo/buildinfo_test.go b/buildinfo/buildinfo_test.go
index 12cc8c99a3ee7..2b4b6a3270654 100644
--- a/buildinfo/buildinfo_test.go
+++ b/buildinfo/buildinfo_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"
- "github.com/coder/coder/buildinfo"
+ "github.com/coder/coder/v2/buildinfo"
)
func TestBuildInfo(t *testing.T) {
diff --git a/cli/agent.go b/cli/agent.go
index 1d9a2ba02d51c..8b77c057ef31a 100644
--- a/cli/agent.go
+++ b/cli/agent.go
@@ -12,6 +12,7 @@ import (
"path/filepath"
"runtime"
"strconv"
+ "strings"
"sync"
"time"
@@ -27,12 +28,12 @@ import (
"cdr.dev/slog/sloggers/sloghuman"
"cdr.dev/slog/sloggers/slogjson"
"cdr.dev/slog/sloggers/slogstackdriver"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/agent/reaper"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/agent/reaper"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
)
func (r *RootCmd) workspaceAgent() *clibase.Cmd {
@@ -253,7 +254,19 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
}
prometheusRegistry := prometheus.NewRegistry()
- subsystem := inv.Environ.Get(agent.EnvAgentSubsystem)
+ subsystemsRaw := inv.Environ.Get(agent.EnvAgentSubsystem)
+ subsystems := []codersdk.AgentSubsystem{}
+ for _, s := range strings.Split(subsystemsRaw, ",") {
+ subsystem := codersdk.AgentSubsystem(strings.TrimSpace(s))
+ if subsystem == "" {
+ continue
+ }
+ if !subsystem.Valid() {
+ return xerrors.Errorf("invalid subsystem %q", subsystem)
+ }
+ subsystems = append(subsystems, subsystem)
+ }
+
agnt := agent.New(agent.Options{
Client: client,
Logger: logger,
@@ -275,7 +288,7 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
},
IgnorePorts: ignorePorts,
SSHMaxTimeout: sshMaxTimeout,
- Subsystem: codersdk.AgentSubsystem(subsystem),
+ Subsystems: subsystems,
PrometheusRegistry: prometheusRegistry,
})
diff --git a/cli/agent_test.go b/cli/agent_test.go
index 462ef3c204541..7073f7c0f18ca 100644
--- a/cli/agent_test.go
+++ b/cli/agent_test.go
@@ -2,6 +2,7 @@ package cli_test
import (
"context"
+ "fmt"
"os"
"path/filepath"
"runtime"
@@ -12,13 +13,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestWorkspaceAgent(t *testing.T) {
@@ -74,9 +75,9 @@ func TestWorkspaceAgent(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
@@ -126,9 +127,9 @@ func TestWorkspaceAgent(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
@@ -178,9 +179,9 @@ func TestWorkspaceAgent(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
@@ -264,8 +265,8 @@ func TestWorkspaceAgent(t *testing.T) {
"--agent-url", client.URL.String(),
"--log-dir", logDir,
)
- // Set the subsystem for the agent.
- inv.Environ.Set(agent.EnvAgentSubsystem, string(codersdk.AgentSubsystemEnvbox))
+ // Set the subsystems for the agent.
+ inv.Environ.Set(agent.EnvAgentSubsystem, fmt.Sprintf("%s,%s", codersdk.AgentSubsystemExectrace, codersdk.AgentSubsystemEnvbox))
pty := ptytest.New(t).Attach(inv)
@@ -275,6 +276,9 @@ func TestWorkspaceAgent(t *testing.T) {
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
require.Len(t, resources, 1)
require.Len(t, resources[0].Agents, 1)
- require.Equal(t, codersdk.AgentSubsystemEnvbox, resources[0].Agents[0].Subsystem)
+ require.Len(t, resources[0].Agents[0].Subsystems, 2)
+ // Sorted
+ require.Equal(t, codersdk.AgentSubsystemEnvbox, resources[0].Agents[0].Subsystems[0])
+ require.Equal(t, codersdk.AgentSubsystemExectrace, resources[0].Agents[0].Subsystems[1])
})
}
diff --git a/cli/clibase/cmd.go b/cli/clibase/cmd.go
index 3e7dfe3903633..c3729d2d586cb 100644
--- a/cli/clibase/cmd.go
+++ b/cli/clibase/cmd.go
@@ -14,6 +14,8 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
+
+ "github.com/coder/coder/v2/coderd/util/slice"
)
// Cmd describes an executable command.
@@ -102,11 +104,11 @@ func (c *Cmd) PrepareAll() error {
}
}
- slices.SortFunc(c.Options, func(a, b Option) bool {
- return a.Name < b.Name
+ slices.SortFunc(c.Options, func(a, b Option) int {
+ return slice.Ascending(a.Name, b.Name)
})
- slices.SortFunc(c.Children, func(a, b *Cmd) bool {
- return a.Name() < b.Name()
+ slices.SortFunc(c.Children, func(a, b *Cmd) int {
+ return slice.Ascending(a.Name(), b.Name())
})
for _, child := range c.Children {
child.Parent = c
diff --git a/cli/clibase/cmd_test.go b/cli/clibase/cmd_test.go
index fedbcd0cf2fd7..f0c21dd0b0bbb 100644
--- a/cli/clibase/cmd_test.go
+++ b/cli/clibase/cmd_test.go
@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/cli/clibase"
)
// ioBufs is the standard input, output, and error for a command.
diff --git a/cli/clibase/env_test.go b/cli/clibase/env_test.go
index d8830e64f580f..19dcc4e76d9a9 100644
--- a/cli/clibase/env_test.go
+++ b/cli/clibase/env_test.go
@@ -4,7 +4,7 @@ import (
"reflect"
"testing"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/cli/clibase"
)
func TestFilterNamePrefix(t *testing.T) {
diff --git a/cli/clibase/option_test.go b/cli/clibase/option_test.go
index cacd8d3a10793..f1b881d94408e 100644
--- a/cli/clibase/option_test.go
+++ b/cli/clibase/option_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/cli/clibase"
)
func TestOptionSet_ParseFlags(t *testing.T) {
@@ -72,6 +72,40 @@ func TestOptionSet_ParseFlags(t *testing.T) {
err := os.FlagSet().Parse([]string{"--some-unknown", "foo"})
require.Error(t, err)
})
+
+ t.Run("RegexValid", func(t *testing.T) {
+ t.Parallel()
+
+ var regexpString clibase.Regexp
+
+ os := clibase.OptionSet{
+ clibase.Option{
+ Name: "RegexpString",
+ Value: ®expString,
+ Flag: "regexp-string",
+ },
+ }
+
+ err := os.FlagSet().Parse([]string{"--regexp-string", "$test^"})
+ require.NoError(t, err)
+ })
+
+ t.Run("RegexInvalid", func(t *testing.T) {
+ t.Parallel()
+
+ var regexpString clibase.Regexp
+
+ os := clibase.OptionSet{
+ clibase.Option{
+ Name: "RegexpString",
+ Value: ®expString,
+ Flag: "regexp-string",
+ },
+ }
+
+ err := os.FlagSet().Parse([]string{"--regexp-string", "(("})
+ require.Error(t, err)
+ })
}
func TestOptionSet_ParseEnv(t *testing.T) {
diff --git a/cli/clibase/values.go b/cli/clibase/values.go
index 288a7c372b152..6ec67d2d1bc09 100644
--- a/cli/clibase/values.go
+++ b/cli/clibase/values.go
@@ -7,6 +7,7 @@ import (
"net"
"net/url"
"reflect"
+ "regexp"
"strconv"
"strings"
"time"
@@ -461,6 +462,43 @@ func (e *Enum) String() string {
return *e.Value
}
+type Regexp regexp.Regexp
+
+func (r *Regexp) MarshalYAML() (interface{}, error) {
+ return yaml.Node{
+ Kind: yaml.ScalarNode,
+ Value: r.String(),
+ }, nil
+}
+
+func (r *Regexp) UnmarshalYAML(n *yaml.Node) error {
+ return r.Set(n.Value)
+}
+
+func (r *Regexp) Set(v string) error {
+ exp, err := regexp.Compile(v)
+ if err != nil {
+ return xerrors.Errorf("invalid regex expression: %w", err)
+ }
+ *r = Regexp(*exp)
+ return nil
+}
+
+func (r Regexp) String() string {
+ return r.Value().String()
+}
+
+func (r *Regexp) Value() *regexp.Regexp {
+ if r == nil {
+ return nil
+ }
+ return (*regexp.Regexp)(r)
+}
+
+func (Regexp) Type() string {
+ return "regexp"
+}
+
var _ pflag.Value = (*YAMLConfigPath)(nil)
// YAMLConfigPath is a special value type that encodes a path to a YAML
diff --git a/cli/clibase/yaml_test.go b/cli/clibase/yaml_test.go
index d14bfc7c75ea6..77a8880019649 100644
--- a/cli/clibase/yaml_test.go
+++ b/cli/clibase/yaml_test.go
@@ -8,7 +8,7 @@ import (
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/cli/clibase"
)
func TestOptionSet_YAML(t *testing.T) {
diff --git a/cli/clitest/clitest.go b/cli/clitest/clitest.go
index 00ec1310043dc..b1c8cd665d8cb 100644
--- a/cli/clitest/clitest.go
+++ b/cli/clitest/clitest.go
@@ -19,12 +19,12 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/config"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/config"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
// New creates a CLI instance with a configuration pointed to a
diff --git a/cli/clitest/clitest_test.go b/cli/clitest/clitest_test.go
index 283f7b48ca588..db31513d182c7 100644
--- a/cli/clitest/clitest_test.go
+++ b/cli/clitest/clitest_test.go
@@ -5,9 +5,9 @@ import (
"go.uber.org/goleak"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestMain(m *testing.M) {
diff --git a/cli/clitest/golden.go b/cli/clitest/golden.go
index ba445efcea577..8abaaeb3d154f 100644
--- a/cli/clitest/golden.go
+++ b/cli/clitest/golden.go
@@ -15,12 +15,12 @@ import (
"github.com/muesli/termenv"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/config"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/config"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
// UpdateGoldenFiles indicates golden files should be updated.
diff --git a/cli/clitest/handlers.go b/cli/clitest/handlers.go
index 5151bc6c0ed6c..2af0c4a5bee0c 100644
--- a/cli/clitest/handlers.go
+++ b/cli/clitest/handlers.go
@@ -3,7 +3,7 @@ package clitest
import (
"testing"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/cli/clibase"
)
// HandlersOK asserts that all commands have a handler.
diff --git a/cli/cliui/agent.go b/cli/cliui/agent.go
index faad31f411f90..c6cc9f413fe54 100644
--- a/cli/cliui/agent.go
+++ b/cli/cliui/agent.go
@@ -8,7 +8,7 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
var errAgentShuttingDown = xerrors.New("agent is shutting down")
diff --git a/cli/cliui/agent_test.go b/cli/cliui/agent_test.go
index c910bf11c301a..c5b5f9f4a3965 100644
--- a/cli/cliui/agent_test.go
+++ b/cli/cliui/agent_test.go
@@ -14,12 +14,12 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestAgent(t *testing.T) {
diff --git a/cli/cliui/gitauth.go b/cli/cliui/gitauth.go
index 7b4bd6f30e264..2e9453c1aac9d 100644
--- a/cli/cliui/gitauth.go
+++ b/cli/cliui/gitauth.go
@@ -8,7 +8,7 @@ import (
"github.com/briandowns/spinner"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
type GitAuthOptions struct {
diff --git a/cli/cliui/gitauth_test.go b/cli/cliui/gitauth_test.go
index dfe142f99be28..22da1b46ca6f9 100644
--- a/cli/cliui/gitauth_test.go
+++ b/cli/cliui/gitauth_test.go
@@ -8,11 +8,11 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestGitAuth(t *testing.T) {
diff --git a/cli/cliui/output.go b/cli/cliui/output.go
index d4cada78f1a03..63a4d4ee5d2c4 100644
--- a/cli/cliui/output.go
+++ b/cli/cliui/output.go
@@ -9,7 +9,7 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/cli/clibase"
)
type OutputFormat interface {
diff --git a/cli/cliui/output_test.go b/cli/cliui/output_test.go
index 22ef241fba7ea..e74213803f09b 100644
--- a/cli/cliui/output_test.go
+++ b/cli/cliui/output_test.go
@@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
)
type format struct {
diff --git a/cli/cliui/parameter.go b/cli/cliui/parameter.go
index f0f4fd99e45d0..a4c8d8e817d59 100644
--- a/cli/cliui/parameter.go
+++ b/cli/cliui/parameter.go
@@ -5,8 +5,8 @@ import (
"fmt"
"strings"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/codersdk"
)
func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
diff --git a/cli/cliui/prompt.go b/cli/cliui/prompt.go
index f927b60749769..ef859814f6299 100644
--- a/cli/cliui/prompt.go
+++ b/cli/cliui/prompt.go
@@ -13,7 +13,7 @@ import (
"github.com/mattn/go-isatty"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/cli/clibase"
)
// PromptOptions supply a set of options to the prompt.
diff --git a/cli/cliui/prompt_test.go b/cli/cliui/prompt_test.go
index 49f6dee46e957..69fc3a539f4df 100644
--- a/cli/cliui/prompt_test.go
+++ b/cli/cliui/prompt_test.go
@@ -11,11 +11,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/pty"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/pty"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestPrompt(t *testing.T) {
diff --git a/cli/cliui/provisionerjob.go b/cli/cliui/provisionerjob.go
index 16d2f366e531c..b09ac6bc73cad 100644
--- a/cli/cliui/provisionerjob.go
+++ b/cli/cliui/provisionerjob.go
@@ -14,7 +14,7 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
func WorkspaceBuild(ctx context.Context, writer io.Writer, client *codersdk.Client, build uuid.UUID) error {
diff --git a/cli/cliui/provisionerjob_test.go b/cli/cliui/provisionerjob_test.go
index 2aa25b6046517..0cf71d8444b95 100644
--- a/cli/cliui/provisionerjob_test.go
+++ b/cli/cliui/provisionerjob_test.go
@@ -11,11 +11,11 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
)
// This cannot be ran in parallel because it uses a signal.
diff --git a/cli/cliui/resources.go b/cli/cliui/resources.go
index 586d37eb5cc81..b1646e22a7b9b 100644
--- a/cli/cliui/resources.go
+++ b/cli/cliui/resources.go
@@ -9,9 +9,9 @@ import (
"github.com/jedib0t/go-pretty/v6/table"
"golang.org/x/mod/semver"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
type WorkspaceResourcesOptions struct {
diff --git a/cli/cliui/resources_test.go b/cli/cliui/resources_test.go
index c9d87c258a6e4..6fc0d7a266c47 100644
--- a/cli/cliui/resources_test.go
+++ b/cli/cliui/resources_test.go
@@ -6,10 +6,10 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestWorkspaceResources(t *testing.T) {
diff --git a/cli/cliui/select.go b/cli/cliui/select.go
index 52a255367ebcf..fafd1c9fcd368 100644
--- a/cli/cliui/select.go
+++ b/cli/cliui/select.go
@@ -10,8 +10,8 @@ import (
"github.com/AlecAivazis/survey/v2/terminal"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/codersdk"
)
func init() {
diff --git a/cli/cliui/select_test.go b/cli/cliui/select_test.go
index f7467098cb263..9465d82b45c8f 100644
--- a/cli/cliui/select_test.go
+++ b/cli/cliui/select_test.go
@@ -6,10 +6,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestSelect(t *testing.T) {
diff --git a/cli/cliui/table_test.go b/cli/cliui/table_test.go
index aca6f7bc825fd..32159abb9fc2b 100644
--- a/cli/cliui/table_test.go
+++ b/cli/cliui/table_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/cliui"
+ "github.com/coder/coder/v2/cli/cliui"
)
type stringWrapper struct {
diff --git a/cli/config/file_test.go b/cli/config/file_test.go
index b3ca15322e217..3177bbfaca101 100644
--- a/cli/config/file_test.go
+++ b/cli/config/file_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/config"
+ "github.com/coder/coder/v2/cli/config"
)
func TestFile(t *testing.T) {
diff --git a/cli/configssh.go b/cli/configssh.go
index 162c3c2a95855..7e9e8109ea554 100644
--- a/cli/configssh.go
+++ b/cli/configssh.go
@@ -22,9 +22,10 @@ import (
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/codersdk"
)
const (
@@ -189,7 +190,6 @@ func sshPrepareWorkspaceConfigs(ctx context.Context, client *codersdk.Client) (r
}
}
-//nolint:gocyclo
func (r *RootCmd) configSSH() *clibase.Cmd {
var (
sshConfigFile string
@@ -367,8 +367,8 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
}
// Ensure stable sorting of output.
- slices.SortFunc(workspaceConfigs, func(a, b sshWorkspaceConfig) bool {
- return a.Name < b.Name
+ slices.SortFunc(workspaceConfigs, func(a, b sshWorkspaceConfig) int {
+ return slice.Ascending(a.Name, b.Name)
})
for _, wc := range workspaceConfigs {
sort.Strings(wc.Hosts)
diff --git a/cli/configssh_test.go b/cli/configssh_test.go
index 34da7dd03fcc0..44246da2596e7 100644
--- a/cli/configssh_test.go
+++ b/cli/configssh_test.go
@@ -21,15 +21,15 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func sshConfigFileName(t *testing.T) (sshConfig string) {
@@ -82,9 +82,9 @@ func TestConfigSSH(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: []*proto.Response{{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -720,22 +720,11 @@ func TestConfigSSH_Hostnames(t *testing.T) {
resources = append(resources, resource)
}
- provisionResponse := []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Resources: resources,
- },
- },
- }}
-
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
// authToken := uuid.NewString()
- version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionPlan: provisionResponse,
- ProvisionApply: provisionResponse,
- })
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID,
+ echo.WithResources(resources))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
diff --git a/cli/create.go b/cli/create.go
index 602b7b40a45bc..971fbc27aac36 100644
--- a/cli/create.go
+++ b/cli/create.go
@@ -10,19 +10,21 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) create() *clibase.Cmd {
var (
- richParameterFile string
- templateName string
- startAt string
- stopAfter time.Duration
- workspaceName string
+ templateName string
+ startAt string
+ stopAfter time.Duration
+ workspaceName string
+
+ parameterFlags workspaceParameterFlags
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
@@ -80,8 +82,8 @@ func (r *RootCmd) create() *clibase.Cmd {
return err
}
- slices.SortFunc(templates, func(a, b codersdk.Template) bool {
- return a.ActiveUserCount > b.ActiveUserCount
+ slices.SortFunc(templates, func(a, b codersdk.Template) int {
+ return slice.Descending(a.ActiveUserCount, b.ActiveUserCount)
})
templateNames := make([]string, 0, len(templates))
@@ -129,10 +131,18 @@ func (r *RootCmd) create() *clibase.Cmd {
schedSpec = ptr.Ref(sched.String())
}
- buildParams, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
- Template: template,
- RichParameterFile: richParameterFile,
- NewWorkspaceName: workspaceName,
+ cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
+ if err != nil {
+ return xerrors.Errorf("can't parse given parameter values: %w", err)
+ }
+
+ richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
+ Action: WorkspaceCreate,
+ Template: template,
+ NewWorkspaceName: workspaceName,
+
+ RichParameterFile: parameterFlags.richParameterFile,
+ RichParameters: cliRichParameters,
})
if err != nil {
return xerrors.Errorf("prepare build: %w", err)
@@ -156,7 +166,7 @@ func (r *RootCmd) create() *clibase.Cmd {
Name: workspaceName,
AutostartSchedule: schedSpec,
TTLMillis: ttlMillis,
- RichParameterValues: buildParams.richParameters,
+ RichParameterValues: richParameters,
})
if err != nil {
return xerrors.Errorf("create workspace: %w", err)
@@ -179,12 +189,6 @@ func (r *RootCmd) create() *clibase.Cmd {
Description: "Specify a template name.",
Value: clibase.StringOf(&templateName),
},
- clibase.Option{
- Flag: "rich-parameter-file",
- Env: "CODER_RICH_PARAMETER_FILE",
- Description: "Specify a file path with values for rich parameters defined in the template.",
- Value: clibase.StringOf(&richParameterFile),
- },
clibase.Option{
Flag: "start-at",
Env: "CODER_WORKSPACE_START_AT",
@@ -199,99 +203,59 @@ func (r *RootCmd) create() *clibase.Cmd {
},
cliui.SkipPromptOption(),
)
+ cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
return cmd
}
type prepWorkspaceBuildArgs struct {
- Template codersdk.Template
- ExistingRichParams []codersdk.WorkspaceBuildParameter
- RichParameterFile string
- NewWorkspaceName string
-
- UpdateWorkspace bool
- BuildOptions bool
- WorkspaceID uuid.UUID
-}
+ Action WorkspaceCLIAction
+ Template codersdk.Template
+ NewWorkspaceName string
+ WorkspaceID uuid.UUID
+
+ LastBuildParameters []codersdk.WorkspaceBuildParameter
+
+ PromptBuildOptions bool
+ BuildOptions []codersdk.WorkspaceBuildParameter
-type buildParameters struct {
- // Rich parameters stores values for build parameters annotated with description, icon, type, etc.
- richParameters []codersdk.WorkspaceBuildParameter
+ PromptRichParameters bool
+ RichParameters []codersdk.WorkspaceBuildParameter
+ RichParameterFile string
}
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
-// Any missing params will be prompted to the user. It supports legacy and rich parameters.
-func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) (*buildParameters, error) {
+// Any missing params will be prompted to the user. It supports rich parameters.
+func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) ([]codersdk.WorkspaceBuildParameter, error) {
ctx := inv.Context()
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
if err != nil {
- return nil, err
+ return nil, xerrors.Errorf("get template version: %w", err)
}
- // Rich parameters
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
if err != nil {
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
}
- parameterMapFromFile := map[string]string{}
- useParamFile := false
+ parameterFile := map[string]string{}
if args.RichParameterFile != "" {
- useParamFile = true
- _, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("Attempting to read the variables from the rich parameter file.")+"\r\n")
- parameterMapFromFile, err = createParameterMapFromFile(args.RichParameterFile)
- if err != nil {
- return nil, err
- }
- }
- disclaimerPrinted := false
- richParameters := make([]codersdk.WorkspaceBuildParameter, 0)
-PromptRichParamLoop:
- for _, templateVersionParameter := range templateVersionParameters {
- if !args.BuildOptions && templateVersionParameter.Ephemeral {
- continue
- }
-
- if !disclaimerPrinted {
- _, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n")
- disclaimerPrinted = true
- }
-
- // Param file is all or nothing
- if !useParamFile && !templateVersionParameter.Ephemeral {
- for _, e := range args.ExistingRichParams {
- if e.Name == templateVersionParameter.Name {
- // If the param already exists, we do not need to prompt it again.
- // The workspace scope will reuse params for each build.
- continue PromptRichParamLoop
- }
- }
- }
-
- if args.UpdateWorkspace && !templateVersionParameter.Mutable {
- // Check if the immutable parameter was used in the previous build. If so, then it isn't a fresh one
- // and the user should be warned.
- exists, err := workspaceBuildParameterExists(ctx, client, args.WorkspaceID, templateVersionParameter)
- if err != nil {
- return nil, err
- }
-
- if exists {
- _, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Warn.Render(fmt.Sprintf(`Parameter %q is not mutable, so can't be customized after workspace creation.`, templateVersionParameter.Name)))
- continue
- }
- }
-
- parameterValue, err := getWorkspaceBuildParameterValueFromMapOrInput(inv, parameterMapFromFile, templateVersionParameter)
+ parameterFile, err = parseParameterMapFile(args.RichParameterFile)
if err != nil {
- return nil, err
+ return nil, xerrors.Errorf("can't parse parameter map file: %w", err)
}
-
- richParameters = append(richParameters, *parameterValue)
}
- if disclaimerPrinted {
- _, _ = fmt.Fprintln(inv.Stdout)
+ resolver := new(ParameterResolver).
+ WithLastBuildParameters(args.LastBuildParameters).
+ WithPromptBuildOptions(args.PromptBuildOptions).
+ WithBuildOptions(args.BuildOptions).
+ WithPromptRichParameters(args.PromptRichParameters).
+ WithRichParameters(args.RichParameters).
+ WithRichParametersFile(parameterFile)
+ buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
+ if err != nil {
+ return nil, err
}
err = cliui.GitAuth(ctx, inv.Stdout, cliui.GitAuthOptions{
@@ -306,7 +270,7 @@ PromptRichParamLoop:
// Run a dry-run with the given parameters to check correctness
dryRun, err := client.CreateTemplateVersionDryRun(inv.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
WorkspaceName: args.NewWorkspaceName,
- RichParameterValues: richParameters,
+ RichParameterValues: buildParameters,
})
if err != nil {
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
@@ -346,21 +310,5 @@ PromptRichParamLoop:
return nil, xerrors.Errorf("get resources: %w", err)
}
- return &buildParameters{
- richParameters: richParameters,
- }, nil
-}
-
-func workspaceBuildParameterExists(ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID, templateVersionParameter codersdk.TemplateVersionParameter) (bool, error) {
- lastBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceID)
- if err != nil {
- return false, xerrors.Errorf("can't fetch last workspace build parameters: %w", err)
- }
-
- for _, p := range lastBuildParameters {
- if p.Name == templateVersionParameter.Name {
- return true, nil
- }
- }
- return false, nil
+ return buildParameters, nil
}
diff --git a/cli/create_test.go b/cli/create_test.go
index 8f2bb6719a377..bdd229775ec68 100644
--- a/cli/create_test.go
+++ b/cli/create_test.go
@@ -2,6 +2,7 @@ package cli_test
import (
"context"
+ "fmt"
"net/http"
"os"
"regexp"
@@ -11,15 +12,15 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestCreate(t *testing.T) {
@@ -28,11 +29,7 @@ func TestCreate(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
- version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- ProvisionPlan: provisionCompleteWithAgent,
- })
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgent())
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
args := []string{
@@ -83,11 +80,7 @@ func TestCreate(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
- version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- ProvisionPlan: provisionCompleteWithAgent,
- })
+ version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
_, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
@@ -140,11 +133,7 @@ func TestCreate(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
- version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- ProvisionPlan: provisionCompleteWithAgent,
- })
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgent())
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
var defaultTTLMillis int64 = 2 * 60 * 60 * 1000 // 2 hours
@@ -239,6 +228,22 @@ func TestCreate(t *testing.T) {
})
}
+func prepareEchoResponses(parameters []*proto.RichParameter) *echo.Responses {
+ return &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: []*proto.Response{
+ {
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Parameters: parameters,
+ },
+ },
+ },
+ },
+ ProvisionApply: echo.ApplyComplete,
+ }
+}
+
func TestCreateWithRichParameters(t *testing.T) {
t.Parallel()
@@ -257,27 +262,12 @@ func TestCreateWithRichParameters(t *testing.T) {
immutableParameterValue = "4"
)
- echoResponses := &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: []*proto.RichParameter{
- {Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
- {Name: secondParameterName, DisplayName: secondParameterDisplayName, Description: secondParameterDescription, Mutable: true},
- {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
- },
- },
- },
- },
- },
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- }},
- }
+ echoResponses := prepareEchoResponses([]*proto.RichParameter{
+ {Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
+ {Name: secondParameterName, DisplayName: secondParameterDisplayName, Description: secondParameterDescription, Mutable: true},
+ {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
+ },
+ )
t.Run("InputParameters", func(t *testing.T) {
t.Parallel()
@@ -357,6 +347,41 @@ func TestCreateWithRichParameters(t *testing.T) {
}
<-doneChan
})
+
+ t.Run("ParameterFlags", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+
+ inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name,
+ "--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
+ "--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
+ "--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
+ clitest.SetupConfig(t, client, root)
+ doneChan := make(chan struct{})
+ pty := ptytest.New(t).Attach(inv)
+ go func() {
+ defer close(doneChan)
+ err := inv.Run()
+ assert.NoError(t, err)
+ }()
+
+ matches := []string{
+ "Confirm create?", "yes",
+ }
+ for i := 0; i < len(matches); i += 2 {
+ match := matches[i]
+ value := matches[i+1]
+ pty.ExpectMatch(match)
+ pty.WriteLine(value)
+ }
+ <-doneChan
+ })
}
func TestCreateValidateRichParameters(t *testing.T) {
@@ -391,28 +416,6 @@ func TestCreateValidateRichParameters(t *testing.T) {
{Name: boolParameterName, Type: "bool", Mutable: true},
}
- prepareEchoResponses := func(richParameters []*proto.RichParameter) *echo.Responses {
- return &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: richParameters,
- },
- },
- },
- },
- ProvisionApply: []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- },
- },
- }
- }
-
t.Run("ValidateString", func(t *testing.T) {
t.Parallel()
@@ -590,20 +593,16 @@ func TestCreateWithGitAuth(t *testing.T) {
t.Parallel()
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
+ ProvisionPlan: []*proto.Response{
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
GitAuthProviders: []string{"github"},
},
},
},
},
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- }},
+ ProvisionApply: echo.ApplyComplete,
}
client := coderdtest.New(t, &coderdtest.Options{
diff --git a/cli/delete.go b/cli/delete.go
index 867abe0326a30..760c0c4e77dd0 100644
--- a/cli/delete.go
+++ b/cli/delete.go
@@ -4,9 +4,9 @@ import (
"fmt"
"time"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
// nolint
@@ -22,16 +22,19 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd {
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
- _, err := cliui.Prompt(inv, cliui.PromptOptions{
- Text: "Confirm delete workspace?",
- IsConfirm: true,
- Default: cliui.ConfirmNo,
- })
+ workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return err
}
- workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
+ sinceLastUsed := time.Since(workspace.LastUsedAt)
+ cliui.Infof(inv.Stderr, "%v was last used %.0f days ago", workspace.FullName(), sinceLastUsed.Hours()/24)
+
+ _, err = cliui.Prompt(inv, cliui.PromptOptions{
+ Text: "Confirm delete workspace?",
+ IsConfirm: true,
+ Default: cliui.ConfirmNo,
+ })
if err != nil {
return err
}
@@ -51,7 +54,7 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd {
return err
}
- _, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been deleted at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
+ _, _ = fmt.Fprintf(inv.Stdout, "\n%s has been deleted at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.FullName()), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
return nil
},
}
diff --git a/cli/delete_test.go b/cli/delete_test.go
index 40f5b9ac22168..58da1aa9a4efd 100644
--- a/cli/delete_test.go
+++ b/cli/delete_test.go
@@ -9,13 +9,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestDelete(t *testing.T) {
@@ -41,7 +41,7 @@ func TestDelete(t *testing.T) {
assert.ErrorIs(t, err, io.EOF)
}
}()
- pty.ExpectMatch("workspace has been deleted")
+ pty.ExpectMatch("has been deleted")
<-doneChan
})
@@ -68,7 +68,7 @@ func TestDelete(t *testing.T) {
assert.ErrorIs(t, err, io.EOF)
}
}()
- pty.ExpectMatch("workspace has been deleted")
+ pty.ExpectMatch("has been deleted")
<-doneChan
})
@@ -113,7 +113,7 @@ func TestDelete(t *testing.T) {
assert.ErrorIs(t, err, io.EOF)
}
}()
- pty.ExpectMatch("workspace has been deleted")
+ pty.ExpectMatch("has been deleted")
<-doneChan
})
@@ -145,7 +145,7 @@ func TestDelete(t *testing.T) {
}
}()
- pty.ExpectMatch("workspace has been deleted")
+ pty.ExpectMatch("has been deleted")
<-doneChan
workspace, err = client.Workspace(context.Background(), workspace.ID)
diff --git a/cli/dotfiles.go b/cli/dotfiles.go
index 60be52a0fc629..635a3add33c0a 100644
--- a/cli/dotfiles.go
+++ b/cli/dotfiles.go
@@ -13,8 +13,8 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
)
func (r *RootCmd) dotfiles() *clibase.Cmd {
diff --git a/cli/dotfiles_test.go b/cli/dotfiles_test.go
index e979fec3e7980..d5511c986aecc 100644
--- a/cli/dotfiles_test.go
+++ b/cli/dotfiles_test.go
@@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/config"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/config"
+ "github.com/coder/coder/v2/cryptorand"
)
func TestDotfiles(t *testing.T) {
diff --git a/cli/exp.go b/cli/exp.go
index 2513a8fda43ee..815d334256414 100644
--- a/cli/exp.go
+++ b/cli/exp.go
@@ -1,6 +1,6 @@
package cli
-import "github.com/coder/coder/cli/clibase"
+import "github.com/coder/coder/v2/cli/clibase"
func (r *RootCmd) expCmd() *clibase.Cmd {
cmd := &clibase.Cmd{
diff --git a/cli/exp_scaletest.go b/cli/exp_scaletest.go
index d2ee36c1819eb..5f0dc34bf68bc 100644
--- a/cli/exp_scaletest.go
+++ b/cli/exp_scaletest.go
@@ -22,19 +22,19 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/scaletest/agentconn"
- "github.com/coder/coder/scaletest/createworkspaces"
- "github.com/coder/coder/scaletest/dashboard"
- "github.com/coder/coder/scaletest/harness"
- "github.com/coder/coder/scaletest/reconnectingpty"
- "github.com/coder/coder/scaletest/workspacebuild"
- "github.com/coder/coder/scaletest/workspacetraffic"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/scaletest/agentconn"
+ "github.com/coder/coder/v2/scaletest/createworkspaces"
+ "github.com/coder/coder/v2/scaletest/dashboard"
+ "github.com/coder/coder/v2/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/reconnectingpty"
+ "github.com/coder/coder/v2/scaletest/workspacebuild"
+ "github.com/coder/coder/v2/scaletest/workspacetraffic"
)
const scaletestTracerName = "coder_scaletest"
@@ -427,7 +427,7 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
cliui.Errorf(inv.Stderr, "Found %d scaletest workspaces\n", len(workspaces))
if len(workspaces) != 0 {
- cliui.Infof(inv.Stdout, "Deleting scaletest workspaces..."+"\n")
+ cliui.Infof(inv.Stdout, "Deleting scaletest workspaces...")
harness := harness.NewTestHarness(cleanupStrategy.toStrategy(), harness.ConcurrentExecutionStrategy{})
for i, w := range workspaces {
@@ -443,7 +443,7 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
return xerrors.Errorf("run test harness to delete workspaces (harness failure, not a test failure): %w", err)
}
- cliui.Infof(inv.Stdout, "Done deleting scaletest workspaces:"+"\n")
+ cliui.Infof(inv.Stdout, "Done deleting scaletest workspaces:")
res := harness.Results()
res.PrintText(inv.Stderr)
@@ -460,7 +460,7 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
cliui.Errorf(inv.Stderr, "Found %d scaletest users\n", len(users))
if len(users) != 0 {
- cliui.Infof(inv.Stdout, "Deleting scaletest users..."+"\n")
+ cliui.Infof(inv.Stdout, "Deleting scaletest users...")
harness := harness.NewTestHarness(cleanupStrategy.toStrategy(), harness.ConcurrentExecutionStrategy{})
for i, u := range users {
@@ -479,7 +479,7 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
return xerrors.Errorf("run test harness to delete users (harness failure, not a test failure): %w", err)
}
- cliui.Infof(inv.Stdout, "Done deleting scaletest users:"+"\n")
+ cliui.Infof(inv.Stdout, "Done deleting scaletest users:")
res := harness.Results()
res.PrintText(inv.Stderr)
diff --git a/cli/exp_scaletest_test.go b/cli/exp_scaletest_test.go
index 4c10b722ca357..db58d41e6926d 100644
--- a/cli/exp_scaletest_test.go
+++ b/cli/exp_scaletest_test.go
@@ -1,17 +1,16 @@
package cli_test
import (
- "bytes"
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestScaleTestCreateWorkspaces(t *testing.T) {
@@ -72,9 +71,10 @@ func TestScaleTestWorkspaceTraffic(t *testing.T) {
"--ssh",
)
clitest.SetupConfig(t, client, root)
- var stdout, stderr bytes.Buffer
- inv.Stdout = &stdout
- inv.Stderr = &stderr
+ pty := ptytest.New(t)
+ inv.Stdout = pty.Output()
+ inv.Stderr = pty.Output()
+
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "no scaletest workspaces exist")
}
@@ -82,6 +82,9 @@ func TestScaleTestWorkspaceTraffic(t *testing.T) {
// This test just validates that the CLI command accepts its known arguments.
func TestScaleTestDashboard(t *testing.T) {
t.Parallel()
+ if testutil.RaceEnabled() {
+ t.Skip("Flakes under race detector, see https://github.com/coder/coder/issues/9168")
+ }
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()
@@ -98,9 +101,10 @@ func TestScaleTestDashboard(t *testing.T) {
"--scaletest-prometheus-wait", "0s",
)
clitest.SetupConfig(t, client, root)
- var stdout, stderr bytes.Buffer
- inv.Stdout = &stdout
- inv.Stderr = &stderr
+ pty := ptytest.New(t)
+ inv.Stdout = pty.Output()
+ inv.Stderr = pty.Output()
+
err := inv.WithContext(ctx).Run()
require.NoError(t, err, "")
}
diff --git a/cli/gitaskpass.go b/cli/gitaskpass.go
index 5bb67adf82416..fb41613d26836 100644
--- a/cli/gitaskpass.go
+++ b/cli/gitaskpass.go
@@ -9,10 +9,10 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/codersdk"
"github.com/coder/retry"
)
@@ -51,9 +51,9 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd {
}
if token.URL != "" {
if err := openURL(inv, token.URL); err == nil {
- cliui.Infof(inv.Stderr, "Your browser has been opened to authenticate with Git:\n\n%s\n", token.URL)
+ cliui.Infof(inv.Stderr, "Your browser has been opened to authenticate with Git:\n%s", token.URL)
} else {
- cliui.Infof(inv.Stderr, "Open the following URL to authenticate with Git:\n\n%s\n", token.URL)
+ cliui.Infof(inv.Stderr, "Open the following URL to authenticate with Git:\n%s", token.URL)
}
for r := retry.New(250*time.Millisecond, 10*time.Second); r.Wait(ctx); {
@@ -61,7 +61,7 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd {
if err != nil {
continue
}
- cliui.Infof(inv.Stderr, "You've been authenticated with Git!\n")
+ cliui.Infof(inv.Stderr, "You've been authenticated with Git!")
break
}
}
diff --git a/cli/gitaskpass_test.go b/cli/gitaskpass_test.go
index 809bc1035005f..5ec7f4c6bb258 100644
--- a/cli/gitaskpass_test.go
+++ b/cli/gitaskpass_test.go
@@ -10,12 +10,12 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestGitAskpass(t *testing.T) {
diff --git a/cli/gitssh.go b/cli/gitssh.go
index 6c4046c03cafe..de5482a8ae387 100644
--- a/cli/gitssh.go
+++ b/cli/gitssh.go
@@ -14,8 +14,8 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
)
func (r *RootCmd) gitssh() *clibase.Cmd {
diff --git a/cli/gitssh_test.go b/cli/gitssh_test.go
index 39daab430c01c..3e5045acf0288 100644
--- a/cli/gitssh_test.go
+++ b/cli/gitssh_test.go
@@ -20,12 +20,12 @@ import (
"github.com/stretchr/testify/require"
gossh "golang.org/x/crypto/ssh"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func prepareTestGitSSH(ctx context.Context, t *testing.T) (*codersdk.Client, string, gossh.PublicKey) {
@@ -48,7 +48,7 @@ func prepareTestGitSSH(ctx context.Context, t *testing.T) (*codersdk.Client, str
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(agentToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
diff --git a/cli/help.go b/cli/help.go
index fa813febc53e9..3741dbfc28119 100644
--- a/cli/help.go
+++ b/cli/help.go
@@ -17,8 +17,8 @@ import (
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
)
//go:embed help.tpl
diff --git a/cli/list.go b/cli/list.go
index 4b50ba16a7d34..12d0a48149ef7 100644
--- a/cli/list.go
+++ b/cli/list.go
@@ -7,11 +7,11 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
)
// workspaceListRow is the type provided to the OutputFormatter. This is a bit
@@ -30,6 +30,7 @@ type workspaceListRow struct {
Outdated bool `json:"-" table:"outdated"`
StartsAt string `json:"-" table:"starts at"`
StopsAfter string `json:"-" table:"stops after"`
+ DailyCost string `json:"-" table:"daily cost"`
}
func workspaceListRowFromWorkspace(now time.Time, usersByID map[uuid.UUID]codersdk.User, workspace codersdk.Workspace) workspaceListRow {
@@ -68,6 +69,7 @@ func workspaceListRowFromWorkspace(now time.Time, usersByID map[uuid.UUID]coders
Outdated: workspace.Outdated,
StartsAt: autostartDisplay,
StopsAfter: autostopDisplay,
+ DailyCost: strconv.Itoa(int(workspace.LatestBuild.DailyCost)),
}
}
@@ -78,7 +80,19 @@ func (r *RootCmd) list() *clibase.Cmd {
searchQuery string
displayWorkspaces []workspaceListRow
formatter = cliui.NewOutputFormatter(
- cliui.TableFormat([]workspaceListRow{}, nil),
+ cliui.TableFormat(
+ []workspaceListRow{},
+ []string{
+ "workspace",
+ "template",
+ "status",
+ "healthy",
+ "last built",
+ "outdated",
+ "starts at",
+ "stops after",
+ },
+ ),
cliui.JSONFormat(),
)
)
diff --git a/cli/list_test.go b/cli/list_test.go
index 39567cd6d9167..6f5fb883c06d9 100644
--- a/cli/list_test.go
+++ b/cli/list_test.go
@@ -9,11 +9,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestList(t *testing.T) {
diff --git a/cli/login.go b/cli/login.go
index e16118dfec0d6..3fe871ad84136 100644
--- a/cli/login.go
+++ b/cli/login.go
@@ -16,10 +16,10 @@ import (
"github.com/pkg/browser"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/userpassword"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/userpassword"
+ "github.com/coder/coder/v2/codersdk"
)
const (
@@ -76,7 +76,7 @@ func (r *RootCmd) login() *clibase.Cmd {
serverURL.Scheme = "https"
}
- client, err := r.createUnauthenticatedClient(serverURL)
+ client, err := r.createUnauthenticatedClient(ctx, serverURL)
if err != nil {
return err
}
diff --git a/cli/login_test.go b/cli/login_test.go
index 1bab4721ea181..0837c1e89b401 100644
--- a/cli/login_test.go
+++ b/cli/login_test.go
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestLogin(t *testing.T) {
diff --git a/cli/logout.go b/cli/logout.go
index 6a4e8872bd227..4e4008e4ffad5 100644
--- a/cli/logout.go
+++ b/cli/logout.go
@@ -7,9 +7,9 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) logout() *clibase.Cmd {
diff --git a/cli/logout_test.go b/cli/logout_test.go
index 849016a68ce81..b7c1a571a6605 100644
--- a/cli/logout_test.go
+++ b/cli/logout_test.go
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/config"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/config"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestLogout(t *testing.T) {
diff --git a/cli/netcheck.go b/cli/netcheck.go
index b670e9c12b8ed..32ce77758f8e4 100644
--- a/cli/netcheck.go
+++ b/cli/netcheck.go
@@ -8,9 +8,9 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) netcheck() *clibase.Cmd {
diff --git a/cli/netcheck_test.go b/cli/netcheck_test.go
index 890260c1a704e..aff65d565bd27 100644
--- a/cli/netcheck_test.go
+++ b/cli/netcheck_test.go
@@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestNetcheck(t *testing.T) {
@@ -31,7 +31,7 @@ func TestNetcheck(t *testing.T) {
require.NoError(t, json.Unmarshal(b, &report))
assert.True(t, report.Healthy)
- require.Len(t, report.Regions, 1)
+ require.Len(t, report.Regions, 1+1) // 1 built-in region + 1 test-managed STUN region
for _, v := range report.Regions {
require.Len(t, v.NodeReports, len(v.Region.Nodes))
}
diff --git a/cli/parameter.go b/cli/parameter.go
index 77e8ccdc8ee67..bca83ee1a62b1 100644
--- a/cli/parameter.go
+++ b/cli/parameter.go
@@ -4,71 +4,98 @@ import (
"encoding/json"
"fmt"
"os"
+ "strings"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/codersdk"
)
-// Reads a YAML file and populates a string -> string map.
-// Throws an error if the file name is empty.
-func createParameterMapFromFile(parameterFile string) (map[string]string, error) {
- if parameterFile != "" {
- parameterFileContents, err := os.ReadFile(parameterFile)
- if err != nil {
- return nil, err
- }
+// workspaceParameterFlags are used by commands processing rich parameters and/or build options.
+type workspaceParameterFlags struct {
+ promptBuildOptions bool
+ buildOptions []string
- mapStringInterface := make(map[string]interface{})
- err = yaml.Unmarshal(parameterFileContents, &mapStringInterface)
- if err != nil {
- return nil, err
- }
+ richParameterFile string
+ richParameters []string
+}
- parameterMap := map[string]string{}
- for k, v := range mapStringInterface {
- switch val := v.(type) {
- case string, bool, int:
- parameterMap[k] = fmt.Sprintf("%v", val)
- case []interface{}:
- b, err := json.Marshal(&val)
- if err != nil {
- return nil, err
- }
- parameterMap[k] = string(b)
- default:
- return nil, xerrors.Errorf("invalid parameter type: %T", v)
- }
- }
- return parameterMap, nil
+func (wpf *workspaceParameterFlags) cliBuildOptions() []clibase.Option {
+ return clibase.OptionSet{
+ {
+ Flag: "build-option",
+ Env: "CODER_BUILD_OPTION",
+ Description: `Build option value in the format "name=value".`,
+ Value: clibase.StringArrayOf(&wpf.buildOptions),
+ },
+ {
+ Flag: "build-options",
+ Description: "Prompt for one-time build options defined with ephemeral parameters.",
+ Value: clibase.BoolOf(&wpf.promptBuildOptions),
+ },
+ }
+}
+
+func (wpf *workspaceParameterFlags) cliParameters() []clibase.Option {
+ return clibase.OptionSet{
+ clibase.Option{
+ Flag: "parameter",
+ Env: "CODER_RICH_PARAMETER",
+ Description: `Rich parameter value in the format "name=value".`,
+ Value: clibase.StringArrayOf(&wpf.richParameters),
+ },
+ clibase.Option{
+ Flag: "rich-parameter-file",
+ Env: "CODER_RICH_PARAMETER_FILE",
+ Description: "Specify a file path with values for rich parameters defined in the template.",
+ Value: clibase.StringOf(&wpf.richParameterFile),
+ },
}
+}
- return nil, xerrors.Errorf("Parameter file name is not specified")
+func asWorkspaceBuildParameters(nameValuePairs []string) ([]codersdk.WorkspaceBuildParameter, error) {
+ var params []codersdk.WorkspaceBuildParameter
+ for _, nameValue := range nameValuePairs {
+ split := strings.SplitN(nameValue, "=", 2)
+ if len(split) < 2 {
+ return nil, xerrors.Errorf("format key=value expected, but got %s", nameValue)
+ }
+ params = append(params, codersdk.WorkspaceBuildParameter{
+ Name: split[0],
+ Value: split[1],
+ })
+ }
+ return params, nil
}
-func getWorkspaceBuildParameterValueFromMapOrInput(inv *clibase.Invocation, parameterMap map[string]string, templateVersionParameter codersdk.TemplateVersionParameter) (*codersdk.WorkspaceBuildParameter, error) {
- var parameterValue string
- var err error
- if parameterMap != nil {
- var ok bool
- parameterValue, ok = parameterMap[templateVersionParameter.Name]
- if !ok {
- parameterValue, err = cliui.RichParameter(inv, templateVersionParameter)
+func parseParameterMapFile(parameterFile string) (map[string]string, error) {
+ parameterFileContents, err := os.ReadFile(parameterFile)
+ if err != nil {
+ return nil, err
+ }
+
+ mapStringInterface := make(map[string]interface{})
+ err = yaml.Unmarshal(parameterFileContents, &mapStringInterface)
+ if err != nil {
+ return nil, err
+ }
+
+ parameterMap := map[string]string{}
+ for k, v := range mapStringInterface {
+ switch val := v.(type) {
+ case string, bool, int:
+ parameterMap[k] = fmt.Sprintf("%v", val)
+ case []interface{}:
+ b, err := json.Marshal(&val)
if err != nil {
return nil, err
}
- }
- } else {
- parameterValue, err = cliui.RichParameter(inv, templateVersionParameter)
- if err != nil {
- return nil, err
+ parameterMap[k] = string(b)
+ default:
+ return nil, xerrors.Errorf("invalid parameter type: %T", v)
}
}
- return &codersdk.WorkspaceBuildParameter{
- Name: templateVersionParameter.Name,
- Value: parameterValue,
- }, nil
+ return parameterMap, nil
}
diff --git a/cli/parameter_internal_test.go b/cli/parameter_internal_test.go
index 81dfcefdf49b2..935486c6eae26 100644
--- a/cli/parameter_internal_test.go
+++ b/cli/parameter_internal_test.go
@@ -16,7 +16,7 @@ func TestCreateParameterMapFromFile(t *testing.T) {
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("region: \"bananas\"\ndisk: \"20\"\n")
- parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name())
+ parameterMapFromFile, err := parseParameterMapFile(parameterFile.Name())
expectedMap := map[string]string{
"region": "bananas",
@@ -28,18 +28,10 @@ func TestCreateParameterMapFromFile(t *testing.T) {
removeTmpDirUntilSuccess(t, tempDir)
})
- t.Run("WithEmptyFilename", func(t *testing.T) {
- t.Parallel()
-
- parameterMapFromFile, err := createParameterMapFromFile("")
-
- assert.Nil(t, parameterMapFromFile)
- assert.EqualError(t, err, "Parameter file name is not specified")
- })
t.Run("WithInvalidFilename", func(t *testing.T) {
t.Parallel()
- parameterMapFromFile, err := createParameterMapFromFile("invalidFile.yaml")
+ parameterMapFromFile, err := parseParameterMapFile("invalidFile.yaml")
assert.Nil(t, parameterMapFromFile)
@@ -57,7 +49,7 @@ func TestCreateParameterMapFromFile(t *testing.T) {
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("region = \"bananas\"\ndisk = \"20\"\n")
- parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name())
+ parameterMapFromFile, err := parseParameterMapFile(parameterFile.Name())
assert.Nil(t, parameterMapFromFile)
assert.EqualError(t, err, "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `region ...` into map[string]interface {}")
diff --git a/cli/parameterresolver.go b/cli/parameterresolver.go
new file mode 100644
index 0000000000000..486188d52a27a
--- /dev/null
+++ b/cli/parameterresolver.go
@@ -0,0 +1,254 @@
+package cli
+
+import (
+ "fmt"
+
+ "golang.org/x/xerrors"
+
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
+)
+
+type WorkspaceCLIAction int
+
+const (
+ WorkspaceCreate WorkspaceCLIAction = iota
+ WorkspaceStart
+ WorkspaceUpdate
+ WorkspaceRestart
+)
+
+type ParameterResolver struct {
+ lastBuildParameters []codersdk.WorkspaceBuildParameter
+
+ richParameters []codersdk.WorkspaceBuildParameter
+ richParametersFile map[string]string
+ buildOptions []codersdk.WorkspaceBuildParameter
+
+ promptRichParameters bool
+ promptBuildOptions bool
+}
+
+func (pr *ParameterResolver) WithLastBuildParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
+ pr.lastBuildParameters = params
+ return pr
+}
+
+func (pr *ParameterResolver) WithRichParameters(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
+ pr.richParameters = params
+ return pr
+}
+
+func (pr *ParameterResolver) WithBuildOptions(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
+ pr.buildOptions = params
+ return pr
+}
+
+func (pr *ParameterResolver) WithRichParametersFile(fileMap map[string]string) *ParameterResolver {
+ pr.richParametersFile = fileMap
+ return pr
+}
+
+func (pr *ParameterResolver) WithPromptRichParameters(promptRichParameters bool) *ParameterResolver {
+ pr.promptRichParameters = promptRichParameters
+ return pr
+}
+
+func (pr *ParameterResolver) WithPromptBuildOptions(promptBuildOptions bool) *ParameterResolver {
+ pr.promptBuildOptions = promptBuildOptions
+ return pr
+}
+
+func (pr *ParameterResolver) Resolve(inv *clibase.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) {
+ var staged []codersdk.WorkspaceBuildParameter
+ var err error
+
+ staged = pr.resolveWithParametersMapFile(staged)
+ staged = pr.resolveWithCommandLineOrEnv(staged)
+ staged = pr.resolveWithLastBuildParameters(staged, templateVersionParameters)
+ if err = pr.verifyConstraints(staged, action, templateVersionParameters); err != nil {
+ return nil, err
+ }
+ if staged, err = pr.resolveWithInput(staged, inv, action, templateVersionParameters); err != nil {
+ return nil, err
+ }
+ return staged, nil
+}
+
+func (pr *ParameterResolver) resolveWithParametersMapFile(resolved []codersdk.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter {
+next:
+ for name, value := range pr.richParametersFile {
+ for i, r := range resolved {
+ if r.Name == name {
+ resolved[i].Value = value
+ continue next
+ }
+ }
+
+ resolved = append(resolved, codersdk.WorkspaceBuildParameter{
+ Name: name,
+ Value: value,
+ })
+ }
+ return resolved
+}
+
+func (pr *ParameterResolver) resolveWithCommandLineOrEnv(resolved []codersdk.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter {
+nextRichParameter:
+ for _, richParameter := range pr.richParameters {
+ for i, r := range resolved {
+ if r.Name == richParameter.Name {
+ resolved[i].Value = richParameter.Value
+ continue nextRichParameter
+ }
+ }
+
+ resolved = append(resolved, richParameter)
+ }
+
+nextBuildOption:
+ for _, buildOption := range pr.buildOptions {
+ for i, r := range resolved {
+ if r.Name == buildOption.Name {
+ resolved[i].Value = buildOption.Value
+ continue nextBuildOption
+ }
+ }
+
+ resolved = append(resolved, buildOption)
+ }
+ return resolved
+}
+
+func (pr *ParameterResolver) resolveWithLastBuildParameters(resolved []codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) []codersdk.WorkspaceBuildParameter {
+ if pr.promptRichParameters {
+ return resolved // don't pull parameters from last build
+ }
+
+next:
+ for _, buildParameter := range pr.lastBuildParameters {
+ tvp := findTemplateVersionParameter(buildParameter, templateVersionParameters)
+ if tvp == nil {
+ continue // it looks like this parameter is not present anymore
+ }
+
+ if tvp.Ephemeral {
+ continue // ephemeral parameters should not be passed to consecutive builds
+ }
+
+ if !tvp.Mutable {
+ continue // immutables should not be passed to consecutive builds
+ }
+
+ if len(tvp.Options) > 0 && !isValidTemplateParameterOption(buildParameter, tvp.Options) {
+ continue // do not propagate invalid options
+ }
+
+ for i, r := range resolved {
+ if r.Name == buildParameter.Name {
+ resolved[i].Value = buildParameter.Value
+ continue next
+ }
+ }
+
+ resolved = append(resolved, buildParameter)
+ }
+ return resolved
+}
+
+func (pr *ParameterResolver) verifyConstraints(resolved []codersdk.WorkspaceBuildParameter, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) error {
+ for _, r := range resolved {
+ tvp := findTemplateVersionParameter(r, templateVersionParameters)
+ if tvp == nil {
+ return xerrors.Errorf("parameter %q is not present in the template", r.Name)
+ }
+
+ if tvp.Ephemeral && !pr.promptBuildOptions && findWorkspaceBuildParameter(tvp.Name, pr.buildOptions) == nil {
+ return xerrors.Errorf("ephemeral parameter %q can be used only with --build-options or --build-option flag", r.Name)
+ }
+
+ if !tvp.Mutable && action != WorkspaceCreate {
+ return xerrors.Errorf("parameter %q is immutable and cannot be updated", r.Name)
+ }
+ }
+ return nil
+}
+
+func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuildParameter, inv *clibase.Invocation, action WorkspaceCLIAction, templateVersionParameters []codersdk.TemplateVersionParameter) ([]codersdk.WorkspaceBuildParameter, error) {
+ for _, tvp := range templateVersionParameters {
+ p := findWorkspaceBuildParameter(tvp.Name, resolved)
+ if p != nil {
+ continue
+ }
+ // Parameter has not been resolved yet, so CLI needs to determine if user should input it.
+
+ firstTimeUse := pr.isFirstTimeUse(tvp.Name)
+ promptParameterOption := pr.isLastBuildParameterInvalidOption(tvp)
+
+ if (tvp.Ephemeral && pr.promptBuildOptions) ||
+ (action == WorkspaceCreate && tvp.Required) ||
+ (action == WorkspaceCreate && !tvp.Ephemeral) ||
+ (action == WorkspaceUpdate && promptParameterOption) ||
+ (action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
+ (action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
+ (action == WorkspaceUpdate && tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
+ parameterValue, err := cliui.RichParameter(inv, tvp)
+ if err != nil {
+ return nil, err
+ }
+
+ resolved = append(resolved, codersdk.WorkspaceBuildParameter{
+ Name: tvp.Name,
+ Value: parameterValue,
+ })
+ } else if action == WorkspaceUpdate && !tvp.Mutable && !firstTimeUse {
+ _, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Warn.Render(fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", tvp.Name)))
+ }
+ }
+ return resolved, nil
+}
+
+func (pr *ParameterResolver) isFirstTimeUse(parameterName string) bool {
+ return findWorkspaceBuildParameter(parameterName, pr.lastBuildParameters) == nil
+}
+
+func (pr *ParameterResolver) isLastBuildParameterInvalidOption(templateVersionParameter codersdk.TemplateVersionParameter) bool {
+ if len(templateVersionParameter.Options) == 0 {
+ return false
+ }
+
+ for _, buildParameter := range pr.lastBuildParameters {
+ if buildParameter.Name == templateVersionParameter.Name {
+ return !isValidTemplateParameterOption(buildParameter, templateVersionParameter.Options)
+ }
+ }
+ return false
+}
+
+func findTemplateVersionParameter(workspaceBuildParameter codersdk.WorkspaceBuildParameter, templateVersionParameters []codersdk.TemplateVersionParameter) *codersdk.TemplateVersionParameter {
+ for _, tvp := range templateVersionParameters {
+ if tvp.Name == workspaceBuildParameter.Name {
+ return &tvp
+ }
+ }
+ return nil
+}
+
+func findWorkspaceBuildParameter(parameterName string, params []codersdk.WorkspaceBuildParameter) *codersdk.WorkspaceBuildParameter {
+ for _, p := range params {
+ if p.Name == parameterName {
+ return &p
+ }
+ }
+ return nil
+}
+
+func isValidTemplateParameterOption(buildParameter codersdk.WorkspaceBuildParameter, options []codersdk.TemplateVersionParameterOption) bool {
+ for _, opt := range options {
+ if opt.Value == buildParameter.Value {
+ return true
+ }
+ }
+ return false
+}
diff --git a/cli/ping.go b/cli/ping.go
index f69958666a044..a3075a85ad3e4 100644
--- a/cli/ping.go
+++ b/cli/ping.go
@@ -10,9 +10,9 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) ping() *clibase.Cmd {
diff --git a/cli/ping_test.go b/cli/ping_test.go
index 959c11c8ed9b4..d054cbf38057a 100644
--- a/cli/ping_test.go
+++ b/cli/ping_test.go
@@ -8,11 +8,11 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestPing(t *testing.T) {
diff --git a/cli/portforward.go b/cli/portforward.go
index 3df1a6f6c9d9f..034b14f894db7 100644
--- a/cli/portforward.go
+++ b/cli/portforward.go
@@ -18,10 +18,10 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/agent/agentssh"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/agent/agentssh"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) portForward() *clibase.Cmd {
diff --git a/cli/portforward_test.go b/cli/portforward_test.go
index 5ae1997285ab7..030133a7ae317 100644
--- a/cli/portforward_test.go
+++ b/cli/portforward_test.go
@@ -13,12 +13,12 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestPortForward_None(t *testing.T) {
@@ -302,7 +302,7 @@ func runAgent(t *testing.T, client *codersdk.Client, userID uuid.UUID) codersdk.
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(agentToken),
})
diff --git a/cli/publickey.go b/cli/publickey.go
index 43537eec428a1..c41c5e2fd4d46 100644
--- a/cli/publickey.go
+++ b/cli/publickey.go
@@ -5,9 +5,9 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) publickey() *clibase.Cmd {
@@ -45,12 +45,12 @@ func (r *RootCmd) publickey() *clibase.Cmd {
cliui.Infof(inv.Stdout,
"This is your public key for using "+cliui.DefaultStyles.Field.Render("git")+" in "+
- "Coder. All clones with SSH will be authenticated automatically 🪄.\n\n",
+ "Coder. All clones with SSH will be authenticated automatically 🪄.",
)
- cliui.Infof(inv.Stdout, cliui.DefaultStyles.Code.Render(strings.TrimSpace(key.PublicKey))+"\n\n")
- cliui.Infof(inv.Stdout, "Add to GitHub and GitLab:"+"\n")
- cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://github.com/settings/ssh/new"+"\n")
- cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://gitlab.com/-/profile/keys"+"\n")
+ cliui.Infof(inv.Stdout, cliui.DefaultStyles.Code.Render(strings.TrimSpace(key.PublicKey))+"\n")
+ cliui.Infof(inv.Stdout, "Add to GitHub and GitLab:")
+ cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://github.com/settings/ssh/new")
+ cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://gitlab.com/-/profile/keys")
return nil
},
diff --git a/cli/publickey_test.go b/cli/publickey_test.go
index a5664ec2bda07..8d04a9b66af53 100644
--- a/cli/publickey_test.go
+++ b/cli/publickey_test.go
@@ -6,8 +6,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
)
func TestPublicKey(t *testing.T) {
diff --git a/cli/remoteforward.go b/cli/remoteforward.go
index 9e53669a7ee47..95daa46663ea5 100644
--- a/cli/remoteforward.go
+++ b/cli/remoteforward.go
@@ -11,7 +11,7 @@ import (
gossh "golang.org/x/crypto/ssh"
"golang.org/x/xerrors"
- "github.com/coder/coder/agent/agentssh"
+ "github.com/coder/coder/v2/agent/agentssh"
)
// cookieAddr is a special net.Addr accepted by sshRemoteForward() which includes a
diff --git a/cli/rename.go b/cli/rename.go
index d9e2af5316603..94d9fc5517278 100644
--- a/cli/rename.go
+++ b/cli/rename.go
@@ -5,9 +5,9 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) rename() *clibase.Cmd {
diff --git a/cli/rename_test.go b/cli/rename_test.go
index 6cd92ff9e1451..42dd4bd897c0e 100644
--- a/cli/rename_test.go
+++ b/cli/rename_test.go
@@ -6,10 +6,10 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestRename(t *testing.T) {
diff --git a/cli/resetpassword.go b/cli/resetpassword.go
index 02a98993368cc..7df9481417b28 100644
--- a/cli/resetpassword.go
+++ b/cli/resetpassword.go
@@ -6,11 +6,11 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/migrations"
- "github.com/coder/coder/coderd/userpassword"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/migrations"
+ "github.com/coder/coder/v2/coderd/userpassword"
)
func (*RootCmd) resetPassword() *clibase.Cmd {
diff --git a/cli/resetpassword_test.go b/cli/resetpassword_test.go
index 40cfc1042dcdc..3ae1c4acb8acb 100644
--- a/cli/resetpassword_test.go
+++ b/cli/resetpassword_test.go
@@ -9,11 +9,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/database/postgres"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/database/postgres"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
// nolint:paralleltest
diff --git a/cli/restart.go b/cli/restart.go
index 4cff7ac7571d7..2f5ca1fff7a77 100644
--- a/cli/restart.go
+++ b/cli/restart.go
@@ -4,9 +4,11 @@ import (
"fmt"
"time"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "golang.org/x/xerrors"
+
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) restart() *clibase.Cmd {
@@ -21,7 +23,7 @@ func (r *RootCmd) restart() *clibase.Cmd {
clibase.RequireNArgs(1),
r.InitClient(client),
),
- Options: append(parameterFlags.options(), cliui.SkipPromptOption()),
+ Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()),
Handler: func(inv *clibase.Invocation) error {
ctx := inv.Context()
out := inv.Stdout
@@ -31,14 +33,29 @@ func (r *RootCmd) restart() *clibase.Cmd {
return err
}
+ lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
+ if err != nil {
+ return err
+ }
+
template, err := client.Template(inv.Context(), workspace.TemplateID)
if err != nil {
return err
}
- buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
- Template: template,
- BuildOptions: parameterFlags.buildOptions,
+ buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
+ if err != nil {
+ return xerrors.Errorf("can't parse build options: %w", err)
+ }
+
+ buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
+ Action: WorkspaceRestart,
+ Template: template,
+
+ LastBuildParameters: lastBuildParameters,
+
+ PromptBuildOptions: parameterFlags.promptBuildOptions,
+ BuildOptions: buildOptions,
})
if err != nil {
return err
@@ -65,7 +82,7 @@ func (r *RootCmd) restart() *clibase.Cmd {
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
- RichParameterValues: buildParams.richParameters,
+ RichParameterValues: buildParameters,
})
if err != nil {
return err
diff --git a/cli/restart_test.go b/cli/restart_test.go
index 83b066e4defc5..43b512c1bc30b 100644
--- a/cli/restart_test.go
+++ b/cli/restart_test.go
@@ -2,47 +2,32 @@ package cli_test
import (
"context"
+ "fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestRestart(t *testing.T) {
t.Parallel()
- echoResponses := &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: []*proto.RichParameter{
- {
- Name: ephemeralParameterName,
- Description: ephemeralParameterDescription,
- Mutable: true,
- Ephemeral: true,
- },
- },
- },
- },
- },
+ echoResponses := prepareEchoResponses([]*proto.RichParameter{
+ {
+ Name: ephemeralParameterName,
+ Description: ephemeralParameterDescription,
+ Mutable: true,
+ Ephemeral: true,
},
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- }},
- }
+ })
t.Run("OK", func(t *testing.T) {
t.Parallel()
@@ -126,4 +111,128 @@ func TestRestart(t *testing.T) {
Value: ephemeralParameterValue,
})
})
+
+ t.Run("BuildOptionFlags", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
+
+ inv, root := clitest.New(t, "restart", workspace.Name,
+ "--build-option", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue))
+ clitest.SetupConfig(t, client, root)
+ doneChan := make(chan struct{})
+ pty := ptytest.New(t).Attach(inv)
+ go func() {
+ defer close(doneChan)
+ err := inv.Run()
+ assert.NoError(t, err)
+ }()
+
+ matches := []string{
+ "Confirm restart workspace?", "yes",
+ "Stopping workspace", "",
+ "Starting workspace", "",
+ "workspace has been restarted", "",
+ }
+ for i := 0; i < len(matches); i += 2 {
+ match := matches[i]
+ value := matches[i+1]
+ pty.ExpectMatch(match)
+
+ if value != "" {
+ pty.WriteLine(value)
+ }
+ }
+ <-doneChan
+
+ // Verify if build option is set
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
+ defer cancel()
+
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspace.Name, codersdk.WorkspaceOptions{})
+ require.NoError(t, err)
+ actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
+ require.NoError(t, err)
+ require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
+ Name: ephemeralParameterName,
+ Value: ephemeralParameterValue,
+ })
+ })
+}
+
+func TestRestartWithParameters(t *testing.T) {
+ t.Parallel()
+
+ echoResponses := &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: []*proto.Response{
+ {
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Parameters: []*proto.RichParameter{
+ {
+ Name: immutableParameterName,
+ Description: immutableParameterDescription,
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ ProvisionApply: echo.ApplyComplete,
+ }
+
+ t.Run("DoNotAskForImmutables", func(t *testing.T) {
+ t.Parallel()
+
+ // Create the workspace
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
+ cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {
+ Name: immutableParameterName,
+ Value: immutableParameterValue,
+ },
+ }
+ })
+ coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
+
+ // Restart the workspace again
+ inv, root := clitest.New(t, "restart", workspace.Name, "-y")
+ clitest.SetupConfig(t, client, root)
+ doneChan := make(chan struct{})
+ pty := ptytest.New(t).Attach(inv)
+ go func() {
+ defer close(doneChan)
+ err := inv.Run()
+ assert.NoError(t, err)
+ }()
+
+ pty.ExpectMatch("workspace has been restarted")
+ <-doneChan
+
+ // Verify if immutable parameter is set
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
+ defer cancel()
+
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
+ require.NoError(t, err)
+ actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
+ require.NoError(t, err)
+ require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
+ Name: immutableParameterName,
+ Value: immutableParameterValue,
+ })
+ })
}
diff --git a/cli/root.go b/cli/root.go
index 4c268235a0f96..3ab2f0d7f33b9 100644
--- a/cli/root.go
+++ b/cli/root.go
@@ -1,6 +1,8 @@
package cli
import (
+ "bufio"
+ "bytes"
"context"
"encoding/base64"
"encoding/json"
@@ -13,6 +15,7 @@ import (
"net/http"
"net/url"
"os"
+ "os/exec"
"os/signal"
"path/filepath"
"runtime"
@@ -28,15 +31,15 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/cli/config"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/cli/config"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
)
var (
@@ -55,6 +58,7 @@ const (
varAgentToken = "agent-token"
varAgentURL = "agent-url"
varHeader = "header"
+ varHeaderCommand = "header-command"
varNoOpen = "no-open"
varNoVersionCheck = "no-version-warning"
varNoFeatureWarning = "no-feature-warning"
@@ -356,6 +360,13 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) {
Value: clibase.StringArrayOf(&r.header),
Group: globalGroup,
},
+ {
+ Flag: varHeaderCommand,
+ Env: "CODER_HEADER_COMMAND",
+ Description: "An external command that outputs additional HTTP headers added to all requests. The command must output each header as `key=value` on its own line.",
+ Value: clibase.StringOf(&r.headerCommand),
+ Group: globalGroup,
+ },
{
Flag: varNoOpen,
Env: "CODER_NO_OPEN",
@@ -437,6 +448,7 @@ type RootCmd struct {
token string
globalConfig string
header []string
+ headerCommand string
agentToken string
agentURL *url.URL
forceTTY bool
@@ -494,6 +506,15 @@ func addTelemetryHeader(client *codersdk.Client, inv *clibase.Invocation) {
// InitClient sets client to a new client.
// It reads from global configuration files if flags are not set.
func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc {
+ return r.initClientInternal(client, false)
+}
+
+func (r *RootCmd) InitClientMissingTokenOK(client *codersdk.Client) clibase.MiddlewareFunc {
+ return r.initClientInternal(client, true)
+}
+
+// nolint: revive
+func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing bool) clibase.MiddlewareFunc {
if client == nil {
panic("client is nil")
}
@@ -508,7 +529,7 @@ func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc {
rawURL, err := conf.URL().Read()
// If the configuration files are absent, the user is logged out
if os.IsNotExist(err) {
- return (errUnauthenticated)
+ return errUnauthenticated
}
if err != nil {
return err
@@ -524,15 +545,14 @@ func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc {
r.token, err = conf.Session().Read()
// If the configuration files are absent, the user is logged out
if os.IsNotExist(err) {
- return (errUnauthenticated)
- }
- if err != nil {
+ if !allowTokenMissing {
+ return errUnauthenticated
+ }
+ } else if err != nil {
return err
}
}
- err = r.setClient(
- client, r.clientURL,
- )
+ err = r.setClient(inv.Context(), client, r.clientURL)
if err != nil {
return err
}
@@ -582,12 +602,38 @@ func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc {
}
}
-func (r *RootCmd) setClient(client *codersdk.Client, serverURL *url.URL) error {
+func (r *RootCmd) setClient(ctx context.Context, client *codersdk.Client, serverURL *url.URL) error {
transport := &headerTransport{
transport: http.DefaultTransport,
header: http.Header{},
}
- for _, header := range r.header {
+ headers := r.header
+ if r.headerCommand != "" {
+ shell := "sh"
+ caller := "-c"
+ if runtime.GOOS == "windows" {
+ shell = "cmd.exe"
+ caller = "/c"
+ }
+ var outBuf bytes.Buffer
+ // #nosec
+ cmd := exec.CommandContext(ctx, shell, caller, r.headerCommand)
+ cmd.Env = append(os.Environ(), "CODER_URL="+serverURL.String())
+ cmd.Stdout = &outBuf
+ cmd.Stderr = io.Discard
+ err := cmd.Run()
+ if err != nil {
+ return xerrors.Errorf("failed to run %v: %w", cmd.Args, err)
+ }
+ scanner := bufio.NewScanner(&outBuf)
+ for scanner.Scan() {
+ headers = append(headers, scanner.Text())
+ }
+ if err := scanner.Err(); err != nil {
+ return xerrors.Errorf("scan %v: %w", cmd.Args, err)
+ }
+ }
+ for _, header := range headers {
parts := strings.SplitN(header, "=", 2)
if len(parts) < 2 {
return xerrors.Errorf("split header %q had less than two parts", header)
@@ -601,9 +647,9 @@ func (r *RootCmd) setClient(client *codersdk.Client, serverURL *url.URL) error {
return nil
}
-func (r *RootCmd) createUnauthenticatedClient(serverURL *url.URL) (*codersdk.Client, error) {
+func (r *RootCmd) createUnauthenticatedClient(ctx context.Context, serverURL *url.URL) (*codersdk.Client, error) {
var client codersdk.Client
- err := r.setClient(&client, serverURL)
+ err := r.setClient(ctx, &client, serverURL)
return &client, err
}
@@ -785,6 +831,13 @@ func (r *RootCmd) checkWarnings(i *clibase.Invocation, client *codersdk.Client)
return nil
}
+// Verbosef logs a message if verbose mode is enabled.
+func (r *RootCmd) Verbosef(inv *clibase.Invocation, fmtStr string, args ...interface{}) {
+ if r.verbose {
+ cliui.Infof(inv.Stdout, fmtStr, args...)
+ }
+}
+
type headerTransport struct {
transport http.RoundTripper
header http.Header
@@ -935,6 +988,8 @@ func (p *prettyErrorFormatter) format(err error) {
msg = sdkError.Message
if sdkError.Helper != "" {
msg = msg + "\n" + sdkError.Helper
+ } else if sdkError.Detail != "" {
+ msg = msg + "\n" + sdkError.Detail
}
// The SDK error is usually good enough, and we don't want to overwhelm
// the user with output.
diff --git a/cli/root_internal_test.go b/cli/root_internal_test.go
index e8c463e95cc90..2d99ab8247518 100644
--- a/cli/root_internal_test.go
+++ b/cli/root_internal_test.go
@@ -1,6 +1,8 @@
package cli
import (
+ "os"
+ "runtime"
"testing"
"github.com/stretchr/testify/require"
@@ -67,6 +69,11 @@ func Test_formatExamples(t *testing.T) {
}
func TestMain(m *testing.M) {
+ if runtime.GOOS == "windows" {
+ // Don't run goleak on windows tests, they're super flaky right now.
+ // See: https://github.com/coder/coder/issues/8954
+ os.Exit(m.Run())
+ }
goleak.VerifyTestMain(m,
// The lumberjack library is used by by agent and seems to leave
// goroutines after Close(), fails TestGitSSH tests.
diff --git a/cli/root_test.go b/cli/root_test.go
index c892701a3acbc..68336ba23a599 100644
--- a/cli/root_test.go
+++ b/cli/root_test.go
@@ -5,23 +5,24 @@ import (
"fmt"
"net/http"
"net/http/httptest"
+ "runtime"
"strings"
"sync/atomic"
"testing"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clitest"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clitest"
)
//nolint:tparallel,paralleltest
@@ -72,20 +73,29 @@ func TestRoot(t *testing.T) {
t.Run("Header", func(t *testing.T) {
t.Parallel()
+ var url string
var called int64
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&called, 1)
assert.Equal(t, "wow", r.Header.Get("X-Testing"))
assert.Equal(t, "Dean was Here!", r.Header.Get("Cool-Header"))
+ assert.Equal(t, "very-wow-"+url, r.Header.Get("X-Process-Testing"))
+ assert.Equal(t, "more-wow", r.Header.Get("X-Process-Testing2"))
w.WriteHeader(http.StatusGone)
}))
defer srv.Close()
+ url = srv.URL
buf := new(bytes.Buffer)
+ coderURLEnv := "$CODER_URL"
+ if runtime.GOOS == "windows" {
+ coderURLEnv = "%CODER_URL%"
+ }
inv, _ := clitest.New(t,
"--no-feature-warning",
"--no-version-warning",
"--header", "X-Testing=wow",
"--header", "Cool-Header=Dean was Here!",
+ "--header-command", "printf X-Process-Testing=very-wow-"+coderURLEnv+"'\\r\\n'X-Process-Testing2=more-wow",
"login", srv.URL,
)
inv.Stdout = buf
@@ -97,14 +107,18 @@ func TestRoot(t *testing.T) {
})
}
-// TestDERPHeaders ensures that the client sends the global `--header`s to the
-// DERP server when connecting.
+// TestDERPHeaders ensures that the client sends the global `--header`s and
+// `--header-command` to the DERP server when connecting.
func TestDERPHeaders(t *testing.T) {
t.Parallel()
// Create a coderd API instance the hard way since we need to change the
// handler to inject our custom /derp handler.
- setHandler, cancelFunc, serverURL, newOptions := coderdtest.NewOptions(t, nil)
+ dv := coderdtest.DeploymentValues(t)
+ dv.DERP.Config.BlockDirect = true
+ setHandler, cancelFunc, serverURL, newOptions := coderdtest.NewOptions(t, &coderdtest.Options{
+ DeploymentValues: dv,
+ })
// We set the handler after server creation for the access URL.
coderAPI := coderd.New(newOptions)
@@ -129,8 +143,9 @@ func TestDERPHeaders(t *testing.T) {
// Inject custom /derp handler so we can inspect the headers.
var (
expectedHeaders = map[string]string{
- "X-Test-Header": "test-value",
- "Cool-Header": "Dean was Here!",
+ "X-Test-Header": "test-value",
+ "Cool-Header": "Dean was Here!",
+ "X-Process-Testing": "very-wow",
}
derpCalled int64
)
@@ -159,9 +174,12 @@ func TestDERPHeaders(t *testing.T) {
"--no-version-warning",
"ping", workspace.Name,
"-n", "1",
+ "--header-command", "printf X-Process-Testing=very-wow",
}
for k, v := range expectedHeaders {
- args = append(args, "--header", fmt.Sprintf("%s=%s", k, v))
+ if k != "X-Process-Testing" {
+ args = append(args, "--header", fmt.Sprintf("%s=%s", k, v))
+ }
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
diff --git a/cli/schedule.go b/cli/schedule.go
index 8fff0121ae8db..629d6160fa3ee 100644
--- a/cli/schedule.go
+++ b/cli/schedule.go
@@ -8,12 +8,12 @@ import (
"github.com/jedib0t/go-pretty/v6/table"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/coderd/util/tz"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/coderd/util/tz"
+ "github.com/coder/coder/v2/codersdk"
)
const (
diff --git a/cli/schedule_test.go b/cli/schedule_test.go
index d1e6fe2da543f..65e2b23ec5db9 100644
--- a/cli/schedule_test.go
+++ b/cli/schedule_test.go
@@ -11,11 +11,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
)
func TestScheduleShow(t *testing.T) {
diff --git a/cli/server.go b/cli/server.go
index 170b7c5eb9f00..779215f0fce35 100644
--- a/cli/server.go
+++ b/cli/server.go
@@ -41,7 +41,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
- "github.com/spf13/afero"
"go.opentelemetry.io/otel/trace"
"golang.org/x/mod/semver"
"golang.org/x/oauth2"
@@ -57,41 +56,43 @@ import (
"cdr.dev/slog/sloggers/sloghuman"
"cdr.dev/slog/sloggers/slogjson"
"cdr.dev/slog/sloggers/slogstackdriver"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/cli/config"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/autobuild"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbmetrics"
- "github.com/coder/coder/coderd/database/dbpurge"
- "github.com/coder/coder/coderd/database/migrations"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/coderd/devtunnel"
- "github.com/coder/coder/coderd/dormancy"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/gitsshkey"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/prometheusmetrics"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/coderd/unhanger"
- "github.com/coder/coder/coderd/updatecheck"
- "github.com/coder/coder/coderd/util/slice"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisioner/terraform"
- "github.com/coder/coder/provisionerd"
- "github.com/coder/coder/provisionerd/proto"
- "github.com/coder/coder/provisionersdk"
- sdkproto "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/cli/config"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/autobuild"
+ "github.com/coder/coder/v2/coderd/batchstats"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbmetrics"
+ "github.com/coder/coder/v2/coderd/database/dbpurge"
+ "github.com/coder/coder/v2/coderd/database/migrations"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/devtunnel"
+ "github.com/coder/coder/v2/coderd/dormancy"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/gitsshkey"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/oauthpki"
+ "github.com/coder/coder/v2/coderd/prometheusmetrics"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/unhanger"
+ "github.com/coder/coder/v2/coderd/updatecheck"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisioner/terraform"
+ "github.com/coder/coder/v2/provisionerd"
+ "github.com/coder/coder/v2/provisionerd/proto"
+ "github.com/coder/coder/v2/provisionersdk"
+ sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/tailnet"
"github.com/coder/retry"
"github.com/coder/wgtunnel/tunnelsdk"
)
@@ -175,18 +176,147 @@ func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.GitAuthConfig, er
return providers, nil
}
-// nolint:gocyclo
+func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*coderd.OIDCConfig, error) {
+ if vals.OIDC.ClientID == "" {
+ return nil, xerrors.Errorf("OIDC client ID must be set!")
+ }
+ if vals.OIDC.IssuerURL == "" {
+ return nil, xerrors.Errorf("OIDC issuer URL must be set!")
+ }
+
+ oidcProvider, err := oidc.NewProvider(
+ ctx, vals.OIDC.IssuerURL.String(),
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("configure oidc provider: %w", err)
+ }
+ redirectURL, err := vals.AccessURL.Value().Parse("/api/v2/users/oidc/callback")
+ if err != nil {
+ return nil, xerrors.Errorf("parse oidc oauth callback url: %w", err)
+ }
+ // If the scopes contain 'groups', we enable group support.
+ // Do not override any custom value set by the user.
+ if slice.Contains(vals.OIDC.Scopes, "groups") && vals.OIDC.GroupField == "" {
+ vals.OIDC.GroupField = "groups"
+ }
+ oauthCfg := &oauth2.Config{
+ ClientID: vals.OIDC.ClientID.String(),
+ ClientSecret: vals.OIDC.ClientSecret.String(),
+ RedirectURL: redirectURL.String(),
+ Endpoint: oidcProvider.Endpoint(),
+ Scopes: vals.OIDC.Scopes,
+ }
+
+ var useCfg httpmw.OAuth2Config = oauthCfg
+ if vals.OIDC.ClientKeyFile != "" {
+ // PKI authentication is done in the params. If a
+ // counter example is found, we can add a config option to
+ // change this.
+ oauthCfg.Endpoint.AuthStyle = oauth2.AuthStyleInParams
+ if vals.OIDC.ClientSecret != "" {
+ return nil, xerrors.Errorf("cannot specify both oidc client secret and oidc client key file")
+ }
+
+ pkiCfg, err := configureOIDCPKI(oauthCfg, vals.OIDC.ClientKeyFile.Value(), vals.OIDC.ClientCertFile.Value())
+ if err != nil {
+ return nil, xerrors.Errorf("configure oauth pki authentication: %w", err)
+ }
+ useCfg = pkiCfg
+ }
+ return &coderd.OIDCConfig{
+ OAuth2Config: useCfg,
+ Provider: oidcProvider,
+ Verifier: oidcProvider.Verifier(&oidc.Config{
+ ClientID: vals.OIDC.ClientID.String(),
+ }),
+ EmailDomain: vals.OIDC.EmailDomain,
+ AllowSignups: vals.OIDC.AllowSignups.Value(),
+ UsernameField: vals.OIDC.UsernameField.String(),
+ EmailField: vals.OIDC.EmailField.String(),
+ AuthURLParams: vals.OIDC.AuthURLParams.Value,
+ IgnoreUserInfo: vals.OIDC.IgnoreUserInfo.Value(),
+ GroupField: vals.OIDC.GroupField.String(),
+ GroupFilter: vals.OIDC.GroupRegexFilter.Value(),
+ CreateMissingGroups: vals.OIDC.GroupAutoCreate.Value(),
+ GroupMapping: vals.OIDC.GroupMapping.Value,
+ UserRoleField: vals.OIDC.UserRoleField.String(),
+ UserRoleMapping: vals.OIDC.UserRoleMapping.Value,
+ UserRolesDefault: vals.OIDC.UserRolesDefault.GetSlice(),
+ SignInText: vals.OIDC.SignInText.String(),
+ IconURL: vals.OIDC.IconURL.String(),
+ IgnoreEmailVerified: vals.OIDC.IgnoreEmailVerified.Value(),
+ }, nil
+}
+
+func afterCtx(ctx context.Context, fn func()) {
+ go func() {
+ <-ctx.Done()
+ fn()
+ }()
+}
+
+func enablePrometheus(
+ ctx context.Context,
+ logger slog.Logger,
+ vals *codersdk.DeploymentValues,
+ options *coderd.Options,
+) (closeFn func(), err error) {
+ options.PrometheusRegistry.MustRegister(collectors.NewGoCollector())
+ options.PrometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
+
+ closeUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.PrometheusRegistry, options.Database, 0)
+ if err != nil {
+ return nil, xerrors.Errorf("register active users prometheus metric: %w", err)
+ }
+ afterCtx(ctx, closeUsersFunc)
+
+ closeWorkspacesFunc, err := prometheusmetrics.Workspaces(ctx, options.PrometheusRegistry, options.Database, 0)
+ if err != nil {
+ return nil, xerrors.Errorf("register workspaces prometheus metric: %w", err)
+ }
+ afterCtx(ctx, closeWorkspacesFunc)
+
+ if vals.Prometheus.CollectAgentStats {
+ closeAgentStatsFunc, err := prometheusmetrics.AgentStats(ctx, logger, options.PrometheusRegistry, options.Database, time.Now(), 0)
+ if err != nil {
+ return nil, xerrors.Errorf("register agent stats prometheus metric: %w", err)
+ }
+ afterCtx(ctx, closeAgentStatsFunc)
+
+ metricsAggregator, err := prometheusmetrics.NewMetricsAggregator(logger, options.PrometheusRegistry, 0)
+ if err != nil {
+ return nil, xerrors.Errorf("can't initialize metrics aggregator: %w", err)
+ }
+
+ cancelMetricsAggregator := metricsAggregator.Run(ctx)
+ afterCtx(ctx, cancelMetricsAggregator)
+
+ options.UpdateAgentMetrics = metricsAggregator.Update
+ err = options.PrometheusRegistry.Register(metricsAggregator)
+ if err != nil {
+ return nil, xerrors.Errorf("can't register metrics aggregator as collector: %w", err)
+ }
+ }
+
+ //nolint:revive
+ return ServeHandler(
+ ctx, logger, promhttp.InstrumentMetricHandler(
+ options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}),
+ ), vals.Prometheus.Address.String(), "prometheus",
+ ), nil
+}
+
func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *clibase.Cmd {
var (
- cfg = new(codersdk.DeploymentValues)
- opts = cfg.Options()
+ vals = new(codersdk.DeploymentValues)
+ opts = vals.Options()
)
serverCmd := &clibase.Cmd{
Use: "server",
Short: "Start a Coder server",
Options: opts,
Middleware: clibase.Chain(
- WriteConfigMW(cfg),
+ WriteConfigMW(vals),
PrintDeprecatedOptions(),
clibase.RequireNArgs(0),
),
@@ -196,32 +326,32 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
- if cfg.Config != "" {
+ if vals.Config != "" {
cliui.Warnf(inv.Stderr, "YAML support is experimental and offers no compatibility guarantees.")
}
go DumpHandler(ctx)
// Validate bind addresses.
- if cfg.Address.String() != "" {
- if cfg.TLS.Enable {
- cfg.HTTPAddress = ""
- cfg.TLS.Address = cfg.Address
+ if vals.Address.String() != "" {
+ if vals.TLS.Enable {
+ vals.HTTPAddress = ""
+ vals.TLS.Address = vals.Address
} else {
- _ = cfg.HTTPAddress.Set(cfg.Address.String())
- cfg.TLS.Address.Host = ""
- cfg.TLS.Address.Port = ""
+ _ = vals.HTTPAddress.Set(vals.Address.String())
+ vals.TLS.Address.Host = ""
+ vals.TLS.Address.Port = ""
}
}
- if cfg.TLS.Enable && cfg.TLS.Address.String() == "" {
+ if vals.TLS.Enable && vals.TLS.Address.String() == "" {
return xerrors.Errorf("TLS address must be set if TLS is enabled")
}
- if !cfg.TLS.Enable && cfg.HTTPAddress.String() == "" {
+ if !vals.TLS.Enable && vals.HTTPAddress.String() == "" {
return xerrors.Errorf("TLS is disabled. Enable with --tls-enable or specify a HTTP address")
}
- if cfg.AccessURL.String() != "" &&
- !(cfg.AccessURL.Scheme == "http" || cfg.AccessURL.Scheme == "https") {
+ if vals.AccessURL.String() != "" &&
+ !(vals.AccessURL.Scheme == "http" || vals.AccessURL.Scheme == "https") {
return xerrors.Errorf("access-url must include a scheme (e.g. 'http://' or 'https://)")
}
@@ -229,14 +359,14 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// was specified.
loginRateLimit := 60
filesRateLimit := 12
- if cfg.RateLimit.DisableAll {
- cfg.RateLimit.API = -1
+ if vals.RateLimit.DisableAll {
+ vals.RateLimit.API = -1
loginRateLimit = -1
filesRateLimit = -1
}
PrintLogo(inv, "Coder")
- logger, logCloser, err := BuildLogger(inv, cfg)
+ logger, logCloser, err := BuildLogger(inv, vals)
if err != nil {
return xerrors.Errorf("make logger: %w", err)
}
@@ -259,7 +389,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
notifyCtx, notifyStop := signal.NotifyContext(ctx, InterruptSignals...)
defer notifyStop()
- cacheDir := cfg.CacheDir.String()
+ cacheDir := vals.CacheDir.String()
err = os.MkdirAll(cacheDir, 0o700)
if err != nil {
return xerrors.Errorf("create cache directory: %w", err)
@@ -270,14 +400,14 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// which is caught by goleaks.
defer http.DefaultClient.CloseIdleConnections()
- tracerProvider, sqlDriver, closeTracing := ConfigureTraceProvider(ctx, logger, inv, cfg)
+ tracerProvider, sqlDriver, closeTracing := ConfigureTraceProvider(ctx, logger, inv, vals)
defer func() {
logger.Debug(ctx, "closing tracing")
traceCloseErr := shutdownWithTimeout(closeTracing, 5*time.Second)
logger.Debug(ctx, "tracing closed", slog.Error(traceCloseErr))
}()
- httpServers, err := ConfigureHTTPServers(inv, cfg)
+ httpServers, err := ConfigureHTTPServers(inv, vals)
if err != nil {
return xerrors.Errorf("configure http(s): %w", err)
}
@@ -287,7 +417,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
builtinPostgres := false
// Only use built-in if PostgreSQL URL isn't specified!
- if !cfg.InMemoryDatabase && cfg.PostgresURL == "" {
+ if !vals.InMemoryDatabase && vals.PostgresURL == "" {
var closeFunc func() error
cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)", config.PostgresPath())
pgURL, closeFunc, err := startBuiltinPostgres(ctx, config, logger)
@@ -295,7 +425,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return err
}
- err = cfg.PostgresURL.Set(pgURL)
+ err = vals.PostgresURL.Set(pgURL)
if err != nil {
return err
}
@@ -319,9 +449,9 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
ctx, httpClient, err := ConfigureHTTPClient(
ctx,
- cfg.TLS.ClientCertFile.String(),
- cfg.TLS.ClientKeyFile.String(),
- cfg.TLS.ClientCAFile.String(),
+ vals.TLS.ClientCertFile.String(),
+ vals.TLS.ClientKeyFile.String(),
+ vals.TLS.ClientCAFile.String(),
)
if err != nil {
return xerrors.Errorf("configure http client: %w", err)
@@ -333,30 +463,30 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
tunnel *tunnelsdk.Tunnel
tunnelDone <-chan struct{} = make(chan struct{}, 1)
)
- if cfg.AccessURL.String() == "" {
+ if vals.AccessURL.String() == "" {
cliui.Infof(inv.Stderr, "Opening tunnel so workspaces can connect to your deployment. For production scenarios, specify an external access URL")
- tunnel, err = devtunnel.New(ctx, logger.Named("net.devtunnel"), cfg.WgtunnelHost.String())
+ tunnel, err = devtunnel.New(ctx, logger.Named("net.devtunnel"), vals.WgtunnelHost.String())
if err != nil {
return xerrors.Errorf("create tunnel: %w", err)
}
defer tunnel.Close()
tunnelDone = tunnel.Wait()
- cfg.AccessURL = clibase.URL(*tunnel.URL)
+ vals.AccessURL = clibase.URL(*tunnel.URL)
- if cfg.WildcardAccessURL.String() == "" {
+ if vals.WildcardAccessURL.String() == "" {
// Suffixed wildcard access URL.
u, err := url.Parse(fmt.Sprintf("*--%s", tunnel.URL.Hostname()))
if err != nil {
return xerrors.Errorf("parse wildcard url: %w", err)
}
- cfg.WildcardAccessURL = clibase.URL(*u)
+ vals.WildcardAccessURL = clibase.URL(*u)
}
}
- _, accessURLPortRaw, _ := net.SplitHostPort(cfg.AccessURL.Host)
+ _, accessURLPortRaw, _ := net.SplitHostPort(vals.AccessURL.Host)
if accessURLPortRaw == "" {
accessURLPortRaw = "80"
- if cfg.AccessURL.Scheme == "https" {
+ if vals.AccessURL.Scheme == "https" {
accessURLPortRaw = "443"
}
}
@@ -366,8 +496,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return xerrors.Errorf("parse access URL port: %w", err)
}
- // Warn the user if the access URL appears to be a loopback address.
- isLocal, err := IsLocalURL(ctx, cfg.AccessURL.Value())
+ // Warn the user if the access URL is loopback or unresolvable.
+ isLocal, err := IsLocalURL(ctx, vals.AccessURL.Value())
if isLocal || err != nil {
reason := "could not be resolved"
if isLocal {
@@ -376,12 +506,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
cliui.Warnf(
inv.Stderr,
"The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n",
- cliui.DefaultStyles.Field.Render(cfg.AccessURL.String()), reason,
+ cliui.DefaultStyles.Field.Render(vals.AccessURL.String()), reason,
)
}
// A newline is added before for visibility in terminal output.
- cliui.Infof(inv.Stdout, "\nView the Web UI: %s", cfg.AccessURL.String())
+ cliui.Infof(inv.Stdout, "\nView the Web UI: %s", vals.AccessURL.String())
// Used for zero-trust instance identity with Google Cloud.
googleTokenValidator, err := idtoken.NewValidator(ctx, option.WithoutAuthentication())
@@ -389,51 +519,39 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return err
}
- sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(cfg.SSHKeygenAlgorithm.String())
+ sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(vals.SSHKeygenAlgorithm.String())
if err != nil {
- return xerrors.Errorf("parse ssh keygen algorithm %s: %w", cfg.SSHKeygenAlgorithm, err)
+ return xerrors.Errorf("parse ssh keygen algorithm %s: %w", vals.SSHKeygenAlgorithm, err)
}
defaultRegion := &tailcfg.DERPRegion{
EmbeddedRelay: true,
- RegionID: int(cfg.DERP.Server.RegionID.Value()),
- RegionCode: cfg.DERP.Server.RegionCode.String(),
- RegionName: cfg.DERP.Server.RegionName.String(),
+ RegionID: int(vals.DERP.Server.RegionID.Value()),
+ RegionCode: vals.DERP.Server.RegionCode.String(),
+ RegionName: vals.DERP.Server.RegionName.String(),
Nodes: []*tailcfg.DERPNode{{
- Name: fmt.Sprintf("%db", cfg.DERP.Server.RegionID),
- RegionID: int(cfg.DERP.Server.RegionID.Value()),
- HostName: cfg.AccessURL.Value().Hostname(),
+ Name: fmt.Sprintf("%db", vals.DERP.Server.RegionID),
+ RegionID: int(vals.DERP.Server.RegionID.Value()),
+ HostName: vals.AccessURL.Value().Hostname(),
DERPPort: accessURLPort,
STUNPort: -1,
- ForceHTTP: cfg.AccessURL.Scheme == "http",
+ ForceHTTP: vals.AccessURL.Scheme == "http",
}},
}
- if !cfg.DERP.Server.Enable {
+ if !vals.DERP.Server.Enable {
defaultRegion = nil
}
- // HACK: see https://github.com/coder/coder/issues/6791.
- for _, addr := range cfg.DERP.Server.STUNAddresses {
- if addr != "disable" {
- continue
- }
- err := cfg.DERP.Server.STUNAddresses.Replace(nil)
- if err != nil {
- panic(err)
- }
- break
- }
-
derpMap, err := tailnet.NewDERPMap(
- ctx, defaultRegion, cfg.DERP.Server.STUNAddresses,
- cfg.DERP.Config.URL.String(), cfg.DERP.Config.Path.String(),
- cfg.DERP.Config.BlockDirect.Value(),
+ ctx, defaultRegion, vals.DERP.Server.STUNAddresses,
+ vals.DERP.Config.URL.String(), vals.DERP.Config.Path.String(),
+ vals.DERP.Config.BlockDirect.Value(),
)
if err != nil {
return xerrors.Errorf("create derp map: %w", err)
}
- appHostname := cfg.WildcardAccessURL.String()
+ appHostname := vals.WildcardAccessURL.String()
var appHostnameRegex *regexp.Regexp
if appHostname != "" {
appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname)
@@ -447,10 +565,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return xerrors.Errorf("read git auth providers from env: %w", err)
}
- cfg.GitAuthProviders.Value = append(cfg.GitAuthProviders.Value, gitAuthEnv...)
+ vals.GitAuthProviders.Value = append(vals.GitAuthProviders.Value, gitAuthEnv...)
gitAuthConfigs, err := gitauth.ConvertConfig(
- cfg.GitAuthProviders.Value,
- cfg.AccessURL.Value(),
+ vals.GitAuthProviders.Value,
+ vals.AccessURL.Value(),
)
if err != nil {
return xerrors.Errorf("convert git auth config: %w", err)
@@ -462,18 +580,18 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
)
}
- realIPConfig, err := httpmw.ParseRealIPConfig(cfg.ProxyTrustedHeaders, cfg.ProxyTrustedOrigins)
+ realIPConfig, err := httpmw.ParseRealIPConfig(vals.ProxyTrustedHeaders, vals.ProxyTrustedOrigins)
if err != nil {
return xerrors.Errorf("parse real ip config: %w", err)
}
- configSSHOptions, err := cfg.SSHConfig.ParseOptions()
+ configSSHOptions, err := vals.SSHConfig.ParseOptions()
if err != nil {
- return xerrors.Errorf("parse ssh config options %q: %w", cfg.SSHConfig.SSHConfigOptions.String(), err)
+ return xerrors.Errorf("parse ssh config options %q: %w", vals.SSHConfig.SSHConfigOptions.String(), err)
}
options := &coderd.Options{
- AccessURL: cfg.AccessURL.Value(),
+ AccessURL: vals.AccessURL.Value(),
AppHostname: appHostname,
AppHostnameRegex: appHostnameRegex,
Logger: logger.Named("coderd"),
@@ -484,22 +602,22 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
GoogleTokenValidator: googleTokenValidator,
GitAuthConfigs: gitAuthConfigs,
RealIPConfig: realIPConfig,
- SecureAuthCookie: cfg.SecureAuthCookie.Value(),
+ SecureAuthCookie: vals.SecureAuthCookie.Value(),
SSHKeygenAlgorithm: sshKeygenAlgorithm,
TracerProvider: tracerProvider,
Telemetry: telemetry.NewNoop(),
- MetricsCacheRefreshInterval: cfg.MetricsCacheRefreshInterval.Value(),
- AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value(),
- DeploymentValues: cfg,
+ MetricsCacheRefreshInterval: vals.MetricsCacheRefreshInterval.Value(),
+ AgentStatsRefreshInterval: vals.AgentStatRefreshInterval.Value(),
+ DeploymentValues: vals,
PrometheusRegistry: prometheus.NewRegistry(),
- APIRateLimit: int(cfg.RateLimit.API.Value()),
+ APIRateLimit: int(vals.RateLimit.API.Value()),
LoginRateLimit: loginRateLimit,
FilesRateLimit: filesRateLimit,
HTTPClient: httpClient,
TemplateScheduleStore: &atomic.Pointer[schedule.TemplateScheduleStore]{},
UserQuietHoursScheduleStore: &atomic.Pointer[schedule.UserQuietHoursScheduleStore]{},
SSHConfig: codersdk.SSHConfigResponse{
- HostnamePrefix: cfg.SSHConfig.DeploymentName.String(),
+ HostnamePrefix: vals.SSHConfig.DeploymentName.String(),
SSHConfigOptions: configSSHOptions,
},
}
@@ -507,16 +625,16 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
options.TLSCertificates = httpServers.TLSConfig.Certificates
}
- if cfg.StrictTransportSecurity > 0 {
+ if vals.StrictTransportSecurity > 0 {
options.StrictTransportSecurityCfg, err = httpmw.HSTSConfigOptions(
- int(cfg.StrictTransportSecurity.Value()), cfg.StrictTransportSecurityOptions,
+ int(vals.StrictTransportSecurity.Value()), vals.StrictTransportSecurityOptions,
)
if err != nil {
- return xerrors.Errorf("coderd: setting hsts header failed (options: %v): %w", cfg.StrictTransportSecurityOptions, err)
+ return xerrors.Errorf("coderd: setting hsts header failed (options: %v): %w", vals.StrictTransportSecurityOptions, err)
}
}
- if cfg.UpdateCheck {
+ if vals.UpdateCheck {
options.UpdateCheckOptions = &updatecheck.Options{
// Avoid spamming GitHub API checking for updates.
Interval: 24 * time.Hour,
@@ -535,83 +653,39 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}
}
- if cfg.OAuth2.Github.ClientSecret != "" {
- options.GithubOAuth2Config, err = configureGithubOAuth2(cfg.AccessURL.Value(),
- cfg.OAuth2.Github.ClientID.String(),
- cfg.OAuth2.Github.ClientSecret.String(),
- cfg.OAuth2.Github.AllowSignups.Value(),
- cfg.OAuth2.Github.AllowEveryone.Value(),
- cfg.OAuth2.Github.AllowedOrgs,
- cfg.OAuth2.Github.AllowedTeams,
- cfg.OAuth2.Github.EnterpriseBaseURL.String(),
+ if vals.OAuth2.Github.ClientSecret != "" {
+ options.GithubOAuth2Config, err = configureGithubOAuth2(vals.AccessURL.Value(),
+ vals.OAuth2.Github.ClientID.String(),
+ vals.OAuth2.Github.ClientSecret.String(),
+ vals.OAuth2.Github.AllowSignups.Value(),
+ vals.OAuth2.Github.AllowEveryone.Value(),
+ vals.OAuth2.Github.AllowedOrgs,
+ vals.OAuth2.Github.AllowedTeams,
+ vals.OAuth2.Github.EnterpriseBaseURL.String(),
)
if err != nil {
return xerrors.Errorf("configure github oauth2: %w", err)
}
}
- if cfg.OIDC.ClientSecret != "" {
- if cfg.OIDC.ClientID == "" {
- return xerrors.Errorf("OIDC client ID be set!")
- }
- if cfg.OIDC.IssuerURL == "" {
- return xerrors.Errorf("OIDC issuer URL must be set!")
- }
-
- if cfg.OIDC.IgnoreEmailVerified {
+ if vals.OIDC.ClientKeyFile != "" || vals.OIDC.ClientSecret != "" {
+ if vals.OIDC.IgnoreEmailVerified {
logger.Warn(ctx, "coder will not check email_verified for OIDC logins")
}
- oidcProvider, err := oidc.NewProvider(
- ctx, cfg.OIDC.IssuerURL.String(),
- )
+ oc, err := createOIDCConfig(ctx, vals)
if err != nil {
- return xerrors.Errorf("configure oidc provider: %w", err)
- }
- redirectURL, err := cfg.AccessURL.Value().Parse("/api/v2/users/oidc/callback")
- if err != nil {
- return xerrors.Errorf("parse oidc oauth callback url: %w", err)
- }
- // If the scopes contain 'groups', we enable group support.
- // Do not override any custom value set by the user.
- if slice.Contains(cfg.OIDC.Scopes, "groups") && cfg.OIDC.GroupField == "" {
- cfg.OIDC.GroupField = "groups"
- }
- options.OIDCConfig = &coderd.OIDCConfig{
- OAuth2Config: &oauth2.Config{
- ClientID: cfg.OIDC.ClientID.String(),
- ClientSecret: cfg.OIDC.ClientSecret.String(),
- RedirectURL: redirectURL.String(),
- Endpoint: oidcProvider.Endpoint(),
- Scopes: cfg.OIDC.Scopes,
- },
- Provider: oidcProvider,
- Verifier: oidcProvider.Verifier(&oidc.Config{
- ClientID: cfg.OIDC.ClientID.String(),
- }),
- EmailDomain: cfg.OIDC.EmailDomain,
- AllowSignups: cfg.OIDC.AllowSignups.Value(),
- UsernameField: cfg.OIDC.UsernameField.String(),
- EmailField: cfg.OIDC.EmailField.String(),
- AuthURLParams: cfg.OIDC.AuthURLParams.Value,
- IgnoreUserInfo: cfg.OIDC.IgnoreUserInfo.Value(),
- GroupField: cfg.OIDC.GroupField.String(),
- GroupMapping: cfg.OIDC.GroupMapping.Value,
- UserRoleField: cfg.OIDC.UserRoleField.String(),
- UserRoleMapping: cfg.OIDC.UserRoleMapping.Value,
- UserRolesDefault: cfg.OIDC.UserRolesDefault.GetSlice(),
- SignInText: cfg.OIDC.SignInText.String(),
- IconURL: cfg.OIDC.IconURL.String(),
- IgnoreEmailVerified: cfg.OIDC.IgnoreEmailVerified.Value(),
+ return xerrors.Errorf("create oidc config: %w", err)
}
+ options.OIDCConfig = oc
}
- if cfg.InMemoryDatabase {
+ if vals.InMemoryDatabase {
// This is only used for testing.
options.Database = dbfake.New()
options.Pubsub = pubsub.NewInMemory()
} else {
- sqlDB, err := connectToPostgres(ctx, logger, sqlDriver, cfg.PostgresURL.String())
+ sqlDB, err := connectToPostgres(ctx, logger, sqlDriver, vals.PostgresURL.String())
if err != nil {
return xerrors.Errorf("connect to postgres: %w", err)
}
@@ -620,7 +694,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
}()
options.Database = database.New(sqlDB)
- options.Pubsub, err = pubsub.New(ctx, sqlDB, cfg.PostgresURL.String())
+ options.Pubsub, err = pubsub.New(ctx, sqlDB, vals.PostgresURL.String())
if err != nil {
return xerrors.Errorf("create pubsub: %w", err)
}
@@ -727,7 +801,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return err
}
- if cfg.Telemetry.Enable {
+ if vals.Telemetry.Enable {
gitAuth := make([]telemetry.GitAuth, 0)
// TODO:
var gitAuthConfigs []codersdk.GitAuthConfig
@@ -742,15 +816,15 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
DeploymentID: deploymentID,
Database: options.Database,
Logger: logger.Named("telemetry"),
- URL: cfg.Telemetry.URL.Value(),
- Wildcard: cfg.WildcardAccessURL.String() != "",
- DERPServerRelayURL: cfg.DERP.Server.RelayURL.String(),
+ URL: vals.Telemetry.URL.Value(),
+ Wildcard: vals.WildcardAccessURL.String() != "",
+ DERPServerRelayURL: vals.DERP.Server.RelayURL.String(),
GitAuth: gitAuth,
- GitHubOAuth: cfg.OAuth2.Github.ClientID != "",
- OIDCAuth: cfg.OIDC.ClientID != "",
- OIDCIssuerURL: cfg.OIDC.IssuerURL.String(),
- Prometheus: cfg.Prometheus.Enable.Value(),
- STUN: len(cfg.DERP.Server.STUNAddresses) != 0,
+ GitHubOAuth: vals.OAuth2.Github.ClientID != "",
+ OIDCAuth: vals.OIDC.ClientID != "",
+ OIDCIssuerURL: vals.OIDC.IssuerURL.String(),
+ Prometheus: vals.Prometheus.Enable.Value(),
+ STUN: len(vals.DERP.Server.STUNAddresses) != 0,
Tunnel: tunnel != nil,
})
if err != nil {
@@ -761,57 +835,36 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// This prevents the pprof import from being accidentally deleted.
_ = pprof.Handler
- if cfg.Pprof.Enable {
+ if vals.Pprof.Enable {
//nolint:revive
- defer ServeHandler(ctx, logger, nil, cfg.Pprof.Address.String(), "pprof")()
- }
- if cfg.Prometheus.Enable {
- options.PrometheusRegistry.MustRegister(collectors.NewGoCollector())
- options.PrometheusRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
-
- closeUsersFunc, err := prometheusmetrics.ActiveUsers(ctx, options.PrometheusRegistry, options.Database, 0)
- if err != nil {
- return xerrors.Errorf("register active users prometheus metric: %w", err)
- }
- defer closeUsersFunc()
-
- closeWorkspacesFunc, err := prometheusmetrics.Workspaces(ctx, options.PrometheusRegistry, options.Database, 0)
+ defer ServeHandler(ctx, logger, nil, vals.Pprof.Address.String(), "pprof")()
+ }
+ if vals.Prometheus.Enable {
+ closeFn, err := enablePrometheus(
+ ctx,
+ logger.Named("prometheus"),
+ vals,
+ options,
+ )
if err != nil {
- return xerrors.Errorf("register workspaces prometheus metric: %w", err)
- }
- defer closeWorkspacesFunc()
-
- if cfg.Prometheus.CollectAgentStats {
- closeAgentStatsFunc, err := prometheusmetrics.AgentStats(ctx, logger, options.PrometheusRegistry, options.Database, time.Now(), 0)
- if err != nil {
- return xerrors.Errorf("register agent stats prometheus metric: %w", err)
- }
- defer closeAgentStatsFunc()
-
- metricsAggregator, err := prometheusmetrics.NewMetricsAggregator(logger, options.PrometheusRegistry, 0)
- if err != nil {
- return xerrors.Errorf("can't initialize metrics aggregator: %w", err)
- }
-
- cancelMetricsAggregator := metricsAggregator.Run(ctx)
- defer cancelMetricsAggregator()
-
- options.UpdateAgentMetrics = metricsAggregator.Update
- err = options.PrometheusRegistry.Register(metricsAggregator)
- if err != nil {
- return xerrors.Errorf("can't register metrics aggregator as collector: %w", err)
- }
+ return xerrors.Errorf("enable prometheus: %w", err)
}
+ defer closeFn()
+ }
- //nolint:revive
- defer ServeHandler(ctx, logger, promhttp.InstrumentMetricHandler(
- options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}),
- ), cfg.Prometheus.Address.String(), "prometheus")()
+ if vals.Swagger.Enable {
+ options.SwaggerEndpoint = vals.Swagger.Enable.Value()
}
- if cfg.Swagger.Enable {
- options.SwaggerEndpoint = cfg.Swagger.Enable.Value()
+ batcher, closeBatcher, err := batchstats.New(ctx,
+ batchstats.WithLogger(options.Logger.Named("batchstats")),
+ batchstats.WithStore(options.Database),
+ )
+ if err != nil {
+ return xerrors.Errorf("failed to create agent stats batcher: %w", err)
}
+ options.StatsBatcher = batcher
+ defer closeBatcher()
closeCheckInactiveUsersFunc := dormancy.CheckInactiveUsers(ctx, logger, options.Database)
defer closeCheckInactiveUsersFunc()
@@ -824,7 +877,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return xerrors.Errorf("create coder API: %w", err)
}
- if cfg.Prometheus.Enable {
+ if vals.Prometheus.Enable {
// Agent metrics require reference to the tailnet coordinator, so must be initiated after Coder API.
closeAgentsFunc, err := prometheusmetrics.Agents(ctx, logger, options.PrometheusRegistry, coderAPI.Database, &coderAPI.TailnetCoordinator, coderAPI.DERPMap, coderAPI.Options.AgentInactiveDisconnectTimeout, 0)
if err != nil {
@@ -872,10 +925,10 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
var provisionerdWaitGroup sync.WaitGroup
defer provisionerdWaitGroup.Wait()
provisionerdMetrics := provisionerd.NewMetrics(options.PrometheusRegistry)
- for i := int64(0); i < cfg.Provisioner.Daemons.Value(); i++ {
+ for i := int64(0); i < vals.Provisioner.Daemons.Value(); i++ {
daemonCacheDir := filepath.Join(cacheDir, fmt.Sprintf("provisioner-%d", i))
daemon, err := newProvisionerDaemon(
- ctx, coderAPI, provisionerdMetrics, logger, cfg, daemonCacheDir, errCh, &provisionerdWaitGroup,
+ ctx, coderAPI, provisionerdMetrics, logger, vals, daemonCacheDir, errCh, &provisionerdWaitGroup,
)
if err != nil {
return xerrors.Errorf("create provisioner daemon: %w", err)
@@ -894,8 +947,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// Wrap the server in middleware that redirects to the access URL if
// the request is not to a local IP.
var handler http.Handler = coderAPI.RootHandler
- if cfg.RedirectToAccessURL {
- handler = redirectToAccessURL(handler, cfg.AccessURL.Value(), tunnel != nil, appHostnameRegex)
+ if vals.RedirectToAccessURL {
+ handler = redirectToAccessURL(handler, vals.AccessURL.Value(), tunnel != nil, appHostnameRegex)
}
// ReadHeaderTimeout is purposefully not enabled. It caused some
@@ -952,12 +1005,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return xerrors.Errorf("notify systemd: %w", err)
}
- autobuildTicker := time.NewTicker(cfg.AutobuildPollInterval.Value())
+ autobuildTicker := time.NewTicker(vals.AutobuildPollInterval.Value())
defer autobuildTicker.Stop()
autobuildExecutor := autobuild.NewExecutor(ctx, options.Database, coderAPI.TemplateScheduleStore, logger, autobuildTicker.C)
autobuildExecutor.Run()
- hangDetectorTicker := time.NewTicker(cfg.JobHangDetectorInterval.Value())
+ hangDetectorTicker := time.NewTicker(vals.JobHangDetectorInterval.Value())
defer hangDetectorTicker.Stop()
hangDetector := unhanger.New(ctx, options.Database, options.Pubsub, logger, hangDetectorTicker.C)
hangDetector.Start()
@@ -1016,9 +1069,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
go func() {
defer wg.Done()
- if ok, _ := inv.ParsedFlags().GetBool(varVerbose); ok {
- cliui.Infof(inv.Stdout, "Shutting down provisioner daemon %d...\n", id)
- }
+ r.Verbosef(inv, "Shutting down provisioner daemon %d...", id)
err := shutdownWithTimeout(provisionerDaemon.Shutdown, 5*time.Second)
if err != nil {
cliui.Errorf(inv.Stderr, "Failed to shutdown provisioner daemon %d: %s\n", id, err)
@@ -1029,9 +1080,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
cliui.Errorf(inv.Stderr, "Close provisioner daemon %d: %s\n", id, err)
return
}
- if ok, _ := inv.ParsedFlags().GetBool(varVerbose); ok {
- cliui.Infof(inv.Stdout, "Gracefully shut down provisioner daemon %d\n", id)
- }
+ r.Verbosef(inv, "Gracefully shut down provisioner daemon %d", id)
}()
}
wg.Wait()
@@ -1272,7 +1321,11 @@ func newProvisionerDaemon(
defer wg.Done()
defer cancel()
- err := echo.Serve(ctx, afero.NewOsFs(), &provisionersdk.ServeOptions{Listener: echoServer})
+ err := echo.Serve(ctx, &provisionersdk.ServeOptions{
+ Listener: echoServer,
+ WorkDirectory: workDir,
+ Logger: logger.Named("echo"),
+ })
if err != nil {
select {
case errCh <- err:
@@ -1304,10 +1357,11 @@ func newProvisionerDaemon(
err := terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
- Listener: terraformServer,
+ Listener: terraformServer,
+ Logger: logger.Named("terraform"),
+ WorkDirectory: workDir,
},
CachePath: tfDir,
- Logger: logger,
Tracer: tracer,
})
if err != nil && !xerrors.Is(err, context.Canceled) {
@@ -1327,14 +1381,13 @@ func newProvisionerDaemon(
// in provisionerdserver.go to learn more!
return coderAPI.CreateInMemoryProvisionerDaemon(ctx, debounce)
}, &provisionerd.Options{
- Logger: logger,
+ Logger: logger.Named("provisionerd"),
JobPollInterval: cfg.Provisioner.DaemonPollInterval.Value(),
JobPollJitter: cfg.Provisioner.DaemonPollJitter.Value(),
JobPollDebounce: debounce,
UpdateInterval: time.Second,
ForceCancelInterval: cfg.Provisioner.ForceCancelInterval.Value(),
Provisioners: provisioners,
- WorkDirectory: workDir,
TracerProvider: coderAPI.TracerProvider,
Metrics: &metrics,
}), nil
@@ -1481,6 +1534,33 @@ func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles
return tlsConfig, nil
}
+func configureOIDCPKI(orig *oauth2.Config, keyFile string, certFile string) (*oauthpki.Config, error) {
+ // Read the files
+ keyData, err := os.ReadFile(keyFile)
+ if err != nil {
+ return nil, xerrors.Errorf("read oidc client key file: %w", err)
+ }
+
+ var certData []byte
+ // According to the spec, this is not required. So do not require it on the initial loading
+ // of the PKI config.
+ if certFile != "" {
+ certData, err = os.ReadFile(certFile)
+ if err != nil {
+ return nil, xerrors.Errorf("read oidc client cert file: %w", err)
+ }
+ }
+
+ return oauthpki.NewOauth2PKIConfig(oauthpki.ConfigParams{
+ ClientID: orig.ClientID,
+ TokenURL: orig.Endpoint.TokenURL,
+ Scopes: orig.Scopes,
+ PemEncodedKey: keyData,
+ PemEncodedCert: certData,
+ Config: orig,
+ })
+}
+
func configureCAPool(tlsClientCAFile string, tlsConfig *tls.Config) error {
if tlsClientCAFile != "" {
caPool := x509.NewCertPool()
@@ -1786,7 +1866,7 @@ func (f *debugFilterSink) compile(res []string) error {
func (f *debugFilterSink) LogEntry(ctx context.Context, ent slog.SinkEntry) {
if ent.Level == slog.LevelDebug {
logName := strings.Join(ent.LoggerNames, ".")
- if f.re != nil && !f.re.MatchString(logName) {
+ if f.re != nil && !f.re.MatchString(logName) && !f.re.MatchString(ent.Message) {
return
}
}
diff --git a/cli/server_createadminuser.go b/cli/server_createadminuser.go
index fbdfed6b8016e..8f146f8f95ead 100644
--- a/cli/server_createadminuser.go
+++ b/cli/server_createadminuser.go
@@ -12,14 +12,14 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/gitsshkey"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/userpassword"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/gitsshkey"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/userpassword"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) newCreateAdminUserCommand() *clibase.Cmd {
@@ -51,7 +51,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *clibase.Cmd {
defer cancel()
if newUserDBURL == "" {
- cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)\n", cfg.PostgresPath())
+ cliui.Infof(inv.Stdout, "Using built-in PostgreSQL (%s)", cfg.PostgresPath())
url, closePg, err := startBuiltinPostgres(ctx, cfg, logger)
if err != nil {
return err
diff --git a/cli/server_createadminuser_test.go b/cli/server_createadminuser_test.go
index 4daca225d71f7..8859a3df806c3 100644
--- a/cli/server_createadminuser_test.go
+++ b/cli/server_createadminuser_test.go
@@ -11,13 +11,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/postgres"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/userpassword"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/postgres"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/userpassword"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
//nolint:paralleltest, tparallel
diff --git a/cli/server_slim.go b/cli/server_slim.go
index 4703f20b7669f..f5f17794f3f56 100644
--- a/cli/server_slim.go
+++ b/cli/server_slim.go
@@ -8,9 +8,9 @@ import (
"io"
"os"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd"
)
func (r *RootCmd) Server(_ func(context.Context, *coderd.Options) (*coderd.API, io.Closer, error)) *clibase.Cmd {
diff --git a/cli/server_test.go b/cli/server_test.go
index ee00499c4d2a6..7b38bb76f9e15 100644
--- a/cli/server_test.go
+++ b/cli/server_test.go
@@ -34,16 +34,16 @@ import (
"go.uber.org/goleak"
"gopkg.in/yaml.v3"
- "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/config"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database/postgres"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/config"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database/postgres"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestReadGitAuthProvidersFromEnv(t *testing.T) {
@@ -1309,6 +1309,7 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
+ "--provisioner-daemons-echo",
"--log-human", fiName,
)
clitest.Start(t, root)
@@ -1326,6 +1327,7 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
+ "--provisioner-daemons-echo",
"--log-human", fi,
)
clitest.Start(t, root)
@@ -1343,6 +1345,7 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
+ "--provisioner-daemons-echo",
"--log-json", fi,
)
clitest.Start(t, root)
@@ -1363,6 +1366,7 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
+ "--provisioner-daemons-echo",
"--log-stackdriver", fi,
)
// Attach pty so we get debug output from the command if this test
@@ -1397,6 +1401,7 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
+ "--provisioner-daemons-echo",
"--log-human", fi1,
"--log-json", fi2,
"--log-stackdriver", fi3,
@@ -1491,31 +1496,6 @@ func TestServer(t *testing.T) {
w.RequireSuccess()
})
})
- t.Run("DisableDERP", func(t *testing.T) {
- t.Parallel()
-
- // Make sure that $CODER_DERP_SERVER_STUN_ADDRESSES can be set to
- // disable STUN.
-
- inv, cfg := clitest.New(t,
- "server",
- "--in-memory",
- "--http-address", ":0",
- "--access-url", "https://example.com",
- )
- inv.Environ.Set("CODER_DERP_SERVER_STUN_ADDRESSES", "disable")
- ptytest.New(t).Attach(inv)
- clitest.Start(t, inv)
- gotURL := waitAccessURL(t, cfg)
- client := codersdk.New(gotURL)
-
- ctx := testutil.Context(t, testutil.WaitMedium)
- _ = coderdtest.CreateFirstUser(t, client)
- gotConfig, err := client.DeploymentConfig(ctx)
- require.NoError(t, err)
-
- require.Len(t, gotConfig.Values.DERP.Server.STUNAddresses, 0)
- })
}
func TestServer_Production(t *testing.T) {
diff --git a/cli/show.go b/cli/show.go
index 3dff78fcaefdc..477c6e0ffbb60 100644
--- a/cli/show.go
+++ b/cli/show.go
@@ -3,9 +3,9 @@ package cli
import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) show() *clibase.Cmd {
diff --git a/cli/show_test.go b/cli/show_test.go
index 6f5faaa3fde11..ccbe182cc7ed9 100644
--- a/cli/show_test.go
+++ b/cli/show_test.go
@@ -5,10 +5,9 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestShow(t *testing.T) {
@@ -17,11 +16,7 @@ func TestShow(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
- version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- ProvisionPlan: provisionCompleteWithAgent,
- })
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgent())
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
diff --git a/cli/speedtest.go b/cli/speedtest.go
index 150605b3330ce..ca6c5e50a6f05 100644
--- a/cli/speedtest.go
+++ b/cli/speedtest.go
@@ -11,9 +11,9 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) speedtest() *clibase.Cmd {
@@ -85,14 +85,14 @@ func (r *RootCmd) speedtest() *clibase.Cmd {
}
peer := status.Peer[status.Peers()[0]]
if !p2p && direct {
- cliui.Infof(inv.Stdout, "Waiting for a direct connection... (%dms via %s)\n", dur.Milliseconds(), peer.Relay)
+ cliui.Infof(inv.Stdout, "Waiting for a direct connection... (%dms via %s)", dur.Milliseconds(), peer.Relay)
continue
}
via := peer.Relay
if via == "" {
via = "direct"
}
- cliui.Infof(inv.Stdout, "%dms via %s\n", dur.Milliseconds(), via)
+ cliui.Infof(inv.Stdout, "%dms via %s", dur.Milliseconds(), via)
break
}
} else {
@@ -107,7 +107,7 @@ func (r *RootCmd) speedtest() *clibase.Cmd {
default:
return xerrors.Errorf("invalid direction: %q", direction)
}
- cliui.Infof(inv.Stdout, "Starting a %ds %s test...\n", int(duration.Seconds()), tsDir)
+ cliui.Infof(inv.Stdout, "Starting a %ds %s test...", int(duration.Seconds()), tsDir)
results, err := conn.Speedtest(ctx, tsDir, duration)
if err != nil {
return err
diff --git a/cli/speedtest_test.go b/cli/speedtest_test.go
index b05e3689347a3..5fc9aedbe2e79 100644
--- a/cli/speedtest_test.go
+++ b/cli/speedtest_test.go
@@ -9,14 +9,14 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestSpeedtest(t *testing.T) {
diff --git a/cli/ssh.go b/cli/ssh.go
index 2db1f4b4e2cb4..4455b8987cc5f 100644
--- a/cli/ssh.go
+++ b/cli/ssh.go
@@ -26,12 +26,12 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/autobuild/notify"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/autobuild/notify"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
"github.com/coder/retry"
)
@@ -40,7 +40,6 @@ var (
autostopNotifyCountdown = []time.Duration{30 * time.Minute}
)
-//nolint:gocyclo
func (r *RootCmd) ssh() *clibase.Cmd {
var (
stdio bool
diff --git a/cli/ssh_internal_test.go b/cli/ssh_internal_test.go
index d9624f393dfa6..07a6a3c5802f2 100644
--- a/cli/ssh_internal_test.go
+++ b/cli/ssh_internal_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
const (
diff --git a/cli/ssh_test.go b/cli/ssh_test.go
index e933839e9ba48..971dc2873ffdc 100644
--- a/cli/ssh_test.go
+++ b/cli/ssh_test.go
@@ -29,18 +29,18 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func setupWorkspaceForAgent(t *testing.T, mutate func([]*proto.Agent) []*proto.Agent) (*codersdk.Client, codersdk.Workspace, string) {
@@ -56,10 +56,10 @@ func setupWorkspaceForAgent(t *testing.T, mutate func([]*proto.Agent) []*proto.A
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "dev",
Type: "google_compute_instance",
diff --git a/cli/start.go b/cli/start.go
index 5bd35867fd105..cde5152e14dc2 100644
--- a/cli/start.go
+++ b/cli/start.go
@@ -6,26 +6,11 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
-// workspaceParameterFlags are used by "start", "restart", and "update".
-type workspaceParameterFlags struct {
- buildOptions bool
-}
-
-func (wpf *workspaceParameterFlags) options() []clibase.Option {
- return clibase.OptionSet{
- {
- Flag: "build-options",
- Description: "Prompt for one-time build options defined with ephemeral parameters.",
- Value: clibase.BoolOf(&wpf.buildOptions),
- },
- }
-}
-
func (r *RootCmd) start() *clibase.Cmd {
var parameterFlags workspaceParameterFlags
@@ -38,21 +23,36 @@ func (r *RootCmd) start() *clibase.Cmd {
clibase.RequireNArgs(1),
r.InitClient(client),
),
- Options: append(parameterFlags.options(), cliui.SkipPromptOption()),
+ Options: append(parameterFlags.cliBuildOptions(), cliui.SkipPromptOption()),
Handler: func(inv *clibase.Invocation) error {
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return err
}
+ lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
+ if err != nil {
+ return err
+ }
+
template, err := client.Template(inv.Context(), workspace.TemplateID)
if err != nil {
return err
}
- buildParams, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
- Template: template,
- BuildOptions: parameterFlags.buildOptions,
+ buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
+ if err != nil {
+ return xerrors.Errorf("unable to parse build options: %w", err)
+ }
+
+ buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
+ Action: WorkspaceStart,
+ Template: template,
+
+ LastBuildParameters: lastBuildParameters,
+
+ PromptBuildOptions: parameterFlags.promptBuildOptions,
+ BuildOptions: buildOptions,
})
if err != nil {
return err
@@ -60,7 +60,7 @@ func (r *RootCmd) start() *clibase.Cmd {
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
- RichParameterValues: buildParams.richParameters,
+ RichParameterValues: buildParameters,
})
if err != nil {
return err
@@ -79,16 +79,21 @@ func (r *RootCmd) start() *clibase.Cmd {
}
type prepStartWorkspaceArgs struct {
- Template codersdk.Template
- BuildOptions bool
+ Action WorkspaceCLIAction
+ Template codersdk.Template
+
+ LastBuildParameters []codersdk.WorkspaceBuildParameter
+
+ PromptBuildOptions bool
+ BuildOptions []codersdk.WorkspaceBuildParameter
}
-func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args prepStartWorkspaceArgs) (*buildParameters, error) {
+func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args prepStartWorkspaceArgs) ([]codersdk.WorkspaceBuildParameter, error) {
ctx := inv.Context()
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
if err != nil {
- return nil, err
+ return nil, xerrors.Errorf("get template version: %w", err)
}
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
@@ -96,30 +101,9 @@ func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args p
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
}
- richParameters := make([]codersdk.WorkspaceBuildParameter, 0)
- if !args.BuildOptions {
- return &buildParameters{
- richParameters: richParameters,
- }, nil
- }
-
- for _, templateVersionParameter := range templateVersionParameters {
- if !templateVersionParameter.Ephemeral {
- continue
- }
-
- parameterValue, err := cliui.RichParameter(inv, templateVersionParameter)
- if err != nil {
- return nil, err
- }
-
- richParameters = append(richParameters, codersdk.WorkspaceBuildParameter{
- Name: templateVersionParameter.Name,
- Value: parameterValue,
- })
- }
-
- return &buildParameters{
- richParameters: richParameters,
- }, nil
+ resolver := new(ParameterResolver).
+ WithLastBuildParameters(args.LastBuildParameters).
+ WithPromptBuildOptions(args.PromptBuildOptions).
+ WithBuildOptions(args.BuildOptions)
+ return resolver.Resolve(inv, args.Action, templateVersionParameters)
}
diff --git a/cli/start_test.go b/cli/start_test.go
index a302fe2ac1c40..dff4048f3e765 100644
--- a/cli/start_test.go
+++ b/cli/start_test.go
@@ -1,25 +1,31 @@
package cli_test
import (
+ "fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
const (
ephemeralParameterName = "ephemeral_parameter"
ephemeralParameterDescription = "This is ephemeral parameter"
ephemeralParameterValue = "3"
+
+ immutableParameterName = "immutable_parameter"
+ immutableParameterDescription = "This is immutable parameter"
+ immutableParameterValue = "abc"
)
func TestStart(t *testing.T) {
@@ -27,10 +33,10 @@ func TestStart(t *testing.T) {
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
+ ProvisionPlan: []*proto.Response{
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{
Name: ephemeralParameterName,
@@ -43,11 +49,7 @@ func TestStart(t *testing.T) {
},
},
},
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- }},
+ ProvisionApply: echo.ApplyComplete,
}
t.Run("BuildOptions", func(t *testing.T) {
@@ -99,4 +101,118 @@ func TestStart(t *testing.T) {
Value: ephemeralParameterValue,
})
})
+
+ t.Run("BuildOptionFlags", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
+
+ inv, root := clitest.New(t, "start", workspace.Name,
+ "--build-option", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue))
+ clitest.SetupConfig(t, client, root)
+ doneChan := make(chan struct{})
+ pty := ptytest.New(t).Attach(inv)
+ go func() {
+ defer close(doneChan)
+ err := inv.Run()
+ assert.NoError(t, err)
+ }()
+
+ pty.ExpectMatch("workspace has been started")
+ <-doneChan
+
+ // Verify if build option is set
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
+ defer cancel()
+
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
+ require.NoError(t, err)
+ actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
+ require.NoError(t, err)
+ require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
+ Name: ephemeralParameterName,
+ Value: ephemeralParameterValue,
+ })
+ })
+}
+
+func TestStartWithParameters(t *testing.T) {
+ t.Parallel()
+
+ echoResponses := &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: []*proto.Response{
+ {
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Parameters: []*proto.RichParameter{
+ {
+ Name: immutableParameterName,
+ Description: immutableParameterDescription,
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ ProvisionApply: echo.ApplyComplete,
+ }
+
+ t.Run("DoNotAskForImmutables", func(t *testing.T) {
+ t.Parallel()
+
+ // Create the workspace
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
+ cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {
+ Name: immutableParameterName,
+ Value: immutableParameterValue,
+ },
+ }
+ })
+ coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
+
+ // Stop the workspace
+ workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
+ coderdtest.AwaitWorkspaceBuildJob(t, client, workspaceBuild.ID)
+
+ // Start the workspace again
+ inv, root := clitest.New(t, "start", workspace.Name)
+ clitest.SetupConfig(t, client, root)
+ doneChan := make(chan struct{})
+ pty := ptytest.New(t).Attach(inv)
+ go func() {
+ defer close(doneChan)
+ err := inv.Run()
+ assert.NoError(t, err)
+ }()
+
+ pty.ExpectMatch("workspace has been started")
+ <-doneChan
+
+ // Verify if immutable parameter is set
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
+ defer cancel()
+
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
+ require.NoError(t, err)
+ actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
+ require.NoError(t, err)
+ require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
+ Name: immutableParameterName,
+ Value: immutableParameterValue,
+ })
+ })
}
diff --git a/cli/stat.go b/cli/stat.go
index 3e32c4187f93b..a2a79fdd39571 100644
--- a/cli/stat.go
+++ b/cli/stat.go
@@ -7,36 +7,48 @@ import (
"github.com/spf13/afero"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/clistat"
- "github.com/coder/coder/cli/cliui"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/clistat"
+ "github.com/coder/coder/v2/cli/cliui"
)
-func (r *RootCmd) stat() *clibase.Cmd {
- fs := afero.NewReadOnlyFs(afero.NewOsFs())
- defaultCols := []string{
- "host_cpu",
- "host_memory",
- "home_disk",
- "container_cpu",
- "container_memory",
- }
- formatter := cliui.NewOutputFormatter(
- cliui.TableFormat([]statsRow{}, defaultCols),
- cliui.JSONFormat(),
- )
- st, err := clistat.New(clistat.WithFS(fs))
- if err != nil {
- panic(xerrors.Errorf("initialize workspace stats collector: %w", err))
+func initStatterMW(tgt **clistat.Statter, fs afero.Fs) clibase.MiddlewareFunc {
+ return func(next clibase.HandlerFunc) clibase.HandlerFunc {
+ return func(i *clibase.Invocation) error {
+ var err error
+ stat, err := clistat.New(clistat.WithFS(fs))
+ if err != nil {
+ return xerrors.Errorf("initialize workspace stats collector: %w", err)
+ }
+ *tgt = stat
+ return next(i)
+ }
}
+}
+func (r *RootCmd) stat() *clibase.Cmd {
+ var (
+ st *clistat.Statter
+ fs = afero.NewReadOnlyFs(afero.NewOsFs())
+ formatter = cliui.NewOutputFormatter(
+ cliui.TableFormat([]statsRow{}, []string{
+ "host_cpu",
+ "host_memory",
+ "home_disk",
+ "container_cpu",
+ "container_memory",
+ }),
+ cliui.JSONFormat(),
+ )
+ )
cmd := &clibase.Cmd{
- Use: "stat",
- Short: "Show resource usage for the current workspace.",
+ Use: "stat",
+ Short: "Show resource usage for the current workspace.",
+ Middleware: initStatterMW(&st, fs),
Children: []*clibase.Cmd{
- r.statCPU(st, fs),
- r.statMem(st, fs),
- r.statDisk(st),
+ r.statCPU(fs),
+ r.statMem(fs),
+ r.statDisk(fs),
},
Handler: func(inv *clibase.Invocation) error {
var sr statsRow
@@ -118,12 +130,16 @@ func (r *RootCmd) stat() *clibase.Cmd {
return cmd
}
-func (*RootCmd) statCPU(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
- var hostArg bool
- formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
+func (*RootCmd) statCPU(fs afero.Fs) *clibase.Cmd {
+ var (
+ hostArg bool
+ st *clistat.Statter
+ formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
+ )
cmd := &clibase.Cmd{
- Use: "cpu",
- Short: "Show CPU usage, in cores.",
+ Use: "cpu",
+ Short: "Show CPU usage, in cores.",
+ Middleware: initStatterMW(&st, fs),
Options: clibase.OptionSet{
{
Flag: "host",
@@ -135,9 +151,9 @@ func (*RootCmd) statCPU(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
var cs *clistat.Result
var err error
if ok, _ := clistat.IsContainerized(fs); ok && !hostArg {
- cs, err = s.ContainerCPU()
+ cs, err = st.ContainerCPU()
} else {
- cs, err = s.HostCPU()
+ cs, err = st.HostCPU()
}
if err != nil {
return err
@@ -155,13 +171,17 @@ func (*RootCmd) statCPU(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
return cmd
}
-func (*RootCmd) statMem(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
- var hostArg bool
- var prefixArg string
- formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
+func (*RootCmd) statMem(fs afero.Fs) *clibase.Cmd {
+ var (
+ hostArg bool
+ prefixArg string
+ st *clistat.Statter
+ formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
+ )
cmd := &clibase.Cmd{
- Use: "mem",
- Short: "Show memory usage, in gigabytes.",
+ Use: "mem",
+ Short: "Show memory usage, in gigabytes.",
+ Middleware: initStatterMW(&st, fs),
Options: clibase.OptionSet{
{
Flag: "host",
@@ -185,9 +205,9 @@ func (*RootCmd) statMem(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
var ms *clistat.Result
var err error
if ok, _ := clistat.IsContainerized(fs); ok && !hostArg {
- ms, err = s.ContainerMemory(pfx)
+ ms, err = st.ContainerMemory(pfx)
} else {
- ms, err = s.HostMemory(pfx)
+ ms, err = st.HostMemory(pfx)
}
if err != nil {
return err
@@ -205,13 +225,17 @@ func (*RootCmd) statMem(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
return cmd
}
-func (*RootCmd) statDisk(s *clistat.Statter) *clibase.Cmd {
- var pathArg string
- var prefixArg string
- formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
+func (*RootCmd) statDisk(fs afero.Fs) *clibase.Cmd {
+ var (
+ pathArg string
+ prefixArg string
+ st *clistat.Statter
+ formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
+ )
cmd := &clibase.Cmd{
- Use: "disk",
- Short: "Show disk usage, in gigabytes.",
+ Use: "disk",
+ Short: "Show disk usage, in gigabytes.",
+ Middleware: initStatterMW(&st, fs),
Options: clibase.OptionSet{
{
Flag: "path",
@@ -233,8 +257,16 @@ func (*RootCmd) statDisk(s *clistat.Statter) *clibase.Cmd {
},
Handler: func(inv *clibase.Invocation) error {
pfx := clistat.ParsePrefix(prefixArg)
- ds, err := s.Disk(pfx, pathArg)
+ // Users may also call `coder stat disk `.
+ if len(inv.Args) > 0 {
+ pathArg = inv.Args[0]
+ }
+ ds, err := st.Disk(pfx, pathArg)
if err != nil {
+ if os.IsNotExist(err) {
+ //nolint:gocritic // fmt.Errorf produces a more concise error.
+ return fmt.Errorf("not found: %q", pathArg)
+ }
return err
}
diff --git a/cli/stat_test.go b/cli/stat_test.go
index d92574e339b89..74d7d109f98d5 100644
--- a/cli/stat_test.go
+++ b/cli/stat_test.go
@@ -9,9 +9,9 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clistat"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clistat"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/testutil"
)
// This just tests that the stat command is recognized and does not output
@@ -74,7 +74,7 @@ func TestStatCPUCmd(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
t.Cleanup(cancel)
- inv, _ := clitest.New(t, "stat", "cpu", "--output=text")
+ inv, _ := clitest.New(t, "stat", "cpu", "--output=text", "--host")
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
@@ -87,7 +87,7 @@ func TestStatCPUCmd(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
t.Cleanup(cancel)
- inv, _ := clitest.New(t, "stat", "cpu", "--output=json")
+ inv, _ := clitest.New(t, "stat", "cpu", "--output=json", "--host")
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
@@ -170,4 +170,16 @@ func TestStatDiskCmd(t *testing.T) {
require.NotZero(t, *tmp.Total)
require.Equal(t, "B", tmp.Unit)
})
+
+ t.Run("PosArg", func(t *testing.T) {
+ t.Parallel()
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
+ t.Cleanup(cancel)
+ inv, _ := clitest.New(t, "stat", "disk", "/this/path/does/not/exist", "--output=text")
+ buf := new(bytes.Buffer)
+ inv.Stdout = buf
+ err := inv.WithContext(ctx).Run()
+ require.Error(t, err)
+ require.Contains(t, err.Error(), `not found: "/this/path/does/not/exist"`)
+ })
}
diff --git a/cli/state.go b/cli/state.go
index dd18e56d90f41..8175cdaa68635 100644
--- a/cli/state.go
+++ b/cli/state.go
@@ -6,9 +6,9 @@ import (
"os"
"strconv"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) state() *clibase.Cmd {
diff --git a/cli/state_test.go b/cli/state_test.go
index 2a208fd64d25c..a240a6d2c81ae 100644
--- a/cli/state_test.go
+++ b/cli/state_test.go
@@ -10,10 +10,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
func TestStatePull(t *testing.T) {
@@ -25,9 +25,9 @@ func TestStatePull(t *testing.T) {
wantState := []byte("some state")
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
State: wantState,
},
},
@@ -53,9 +53,9 @@ func TestStatePull(t *testing.T) {
wantState := []byte("some state")
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
State: wantState,
},
},
@@ -83,7 +83,7 @@ func TestStatePush(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -108,7 +108,7 @@ func TestStatePush(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
diff --git a/cli/stop.go b/cli/stop.go
index 1dbf446ed2979..41265a859f489 100644
--- a/cli/stop.go
+++ b/cli/stop.go
@@ -4,9 +4,9 @@ import (
"fmt"
"time"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) stop() *clibase.Cmd {
diff --git a/cli/templatecreate.go b/cli/templatecreate.go
index 77a869bdc0518..638f790dd811d 100644
--- a/cli/templatecreate.go
+++ b/cli/templatecreate.go
@@ -14,12 +14,12 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisionerd"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisionerd"
)
func (r *RootCmd) templateCreate() *clibase.Cmd {
@@ -29,9 +29,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
variablesFile string
variables []string
disableEveryone bool
- defaultTTL time.Duration
- failureTTL time.Duration
- inactivityTTL time.Duration
+
+ defaultTTL time.Duration
+ failureTTL time.Duration
+ inactivityTTL time.Duration
+ maxTTL time.Duration
uploadFlags templateUploadFlags
)
@@ -44,7 +46,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
- if failureTTL != 0 || inactivityTTL != 0 {
+ if failureTTL != 0 || inactivityTTL != 0 || maxTTL != 0 {
// This call can be removed when workspace_actions is no longer experimental
experiments, exErr := client.Experiments(inv.Context())
if exErr != nil {
@@ -134,7 +136,8 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
VersionID: job.ID,
DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
- InactivityTTLMillis: ptr.Ref(inactivityTTL.Milliseconds()),
+ MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()),
+ TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
DisableEveryoneGroupAccess: disableEveryone,
}
@@ -182,22 +185,27 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
},
{
Flag: "default-ttl",
- Description: "Specify a default TTL for workspaces created from this template.",
+ Description: "Specify a default TTL for workspaces created from this template. It is the default time before shutdown - workspaces created from this template default to this value. Maps to \"Default autostop\" in the UI.",
Default: "24h",
Value: clibase.DurationOf(&defaultTTL),
},
{
Flag: "failure-ttl",
- Description: "Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).",
+ Description: "Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed \"start\" build before coder automatically schedules a \"stop\" build to cleanup.This licensed feature's default is 0h (off). Maps to \"Failure cleanup\"in the UI.",
Default: "0h",
Value: clibase.DurationOf(&failureTTL),
},
{
Flag: "inactivity-ttl",
- Description: "Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).",
+ Description: "Specify an inactivity TTL for workspaces created from this template. It is the amount of time the workspace is not used before it is be stopped and auto-locked. This includes across multiple builds (e.g. auto-starts and stops). This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.",
Default: "0h",
Value: clibase.DurationOf(&inactivityTTL),
},
+ {
+ Flag: "max-ttl",
+ Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.",
+ Value: clibase.DurationOf(&maxTTL),
+ },
{
Flag: "test.provisioner",
Description: "Customize the provisioner backend.",
diff --git a/cli/templatecreate_test.go b/cli/templatecreate_test.go
index 06e180f7dcd6c..ba5dad7b4ac6a 100644
--- a/cli/templatecreate_test.go
+++ b/cli/templatecreate_test.go
@@ -10,35 +10,61 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
-var provisionCompleteWithAgent = []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Resources: []*proto.Resource{
- {
- Type: "compute",
- Name: "main",
- Agents: []*proto.Agent{
+func completeWithAgent() *echo.Responses {
+ return &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: []*proto.Response{
+ {
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Resources: []*proto.Resource{
+ {
+ Type: "compute",
+ Name: "main",
+ Agents: []*proto.Agent{
+ {
+ Name: "smith",
+ OperatingSystem: "linux",
+ Architecture: "i386",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ProvisionApply: []*proto.Response{
+ {
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
+ Resources: []*proto.Resource{
{
- Name: "smith",
- OperatingSystem: "linux",
- Architecture: "i386",
+ Type: "compute",
+ Name: "main",
+ Agents: []*proto.Agent{
+ {
+ Name: "smith",
+ OperatingSystem: "linux",
+ Architecture: "i386",
+ },
+ },
},
},
},
},
},
},
- },
+ }
}
func TestTemplateCreate(t *testing.T) {
@@ -47,10 +73,7 @@ func TestTemplateCreate(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
coderdtest.CreateFirstUser(t, client)
- source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- })
+ source := clitest.CreateTemplateVersionSource(t, completeWithAgent())
args := []string{
"templates",
"create",
@@ -85,10 +108,7 @@ func TestTemplateCreate(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
coderdtest.CreateFirstUser(t, client)
- source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- })
+ source := clitest.CreateTemplateVersionSource(t, completeWithAgent())
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
args := []string{
"templates",
@@ -128,10 +148,7 @@ func TestTemplateCreate(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
coderdtest.CreateFirstUser(t, client)
- source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- })
+ source := clitest.CreateTemplateVersionSource(t, completeWithAgent())
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
args := []string{
"templates",
@@ -167,10 +184,7 @@ func TestTemplateCreate(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
coderdtest.CreateFirstUser(t, client)
- source, err := echo.Tar(&echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- })
+ source, err := echo.Tar(completeWithAgent())
require.NoError(t, err)
args := []string{
@@ -196,10 +210,7 @@ func TestTemplateCreate(t *testing.T) {
coderdtest.CreateFirstUser(t, client)
create := func() error {
- source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- })
+ source := clitest.CreateTemplateVersionSource(t, completeWithAgent())
args := []string{
"templates",
"create",
diff --git a/cli/templatedelete.go b/cli/templatedelete.go
index d954dbf44c081..9380279d6af96 100644
--- a/cli/templatedelete.go
+++ b/cli/templatedelete.go
@@ -7,9 +7,9 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) templateDelete() *clibase.Cmd {
diff --git a/cli/templatedelete_test.go b/cli/templatedelete_test.go
index 1f7c032b11d59..963ece08ab4dc 100644
--- a/cli/templatedelete_test.go
+++ b/cli/templatedelete_test.go
@@ -8,11 +8,11 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestTemplateDelete(t *testing.T) {
diff --git a/cli/templateedit.go b/cli/templateedit.go
index 6c8173c452817..329187ef7ae7c 100644
--- a/cli/templateedit.go
+++ b/cli/templateedit.go
@@ -8,9 +8,9 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) templateEdit() *clibase.Cmd {
@@ -104,7 +104,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
Weeks: restartRequirementWeeks,
},
FailureTTLMillis: failureTTL.Milliseconds(),
- InactivityTTLMillis: inactivityTTL.Milliseconds(),
+ TimeTilDormantMillis: inactivityTTL.Milliseconds(),
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
AllowUserAutostart: allowUserAutostart,
AllowUserAutostop: allowUserAutostop,
@@ -142,12 +142,12 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
},
{
Flag: "default-ttl",
- Description: "Edit the template default time before shutdown - workspaces created from this template default to this value.",
+ Description: "Edit the template default time before shutdown - workspaces created from this template default to this value. Maps to \"Default autostop\" in the UI.",
Value: clibase.DurationOf(&defaultTTL),
},
{
Flag: "max-ttl",
- Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.",
+ Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting, regardless of user activity. This is an enterprise-only feature. Maps to \"Max lifetime\" in the UI.",
Value: clibase.DurationOf(&maxTTL),
},
{
@@ -176,13 +176,13 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
},
{
Flag: "failure-ttl",
- Description: "Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).",
+ Description: "Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed \"start\" build before coder automatically schedules a \"stop\" build to cleanup.This licensed feature's default is 0h (off). Maps to \"Failure cleanup\" in the UI.",
Default: "0h",
Value: clibase.DurationOf(&failureTTL),
},
{
Flag: "inactivity-ttl",
- Description: "Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).",
+ Description: "Specify an inactivity TTL for workspaces created from this template. It is the amount of time the workspace is not used before it is be stopped and auto-locked. This includes across multiple builds (e.g. auto-starts and stops). This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.",
Default: "0h",
Value: clibase.DurationOf(&inactivityTTL),
},
diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go
index 87944cd5a0f60..0aff5166e9ca8 100644
--- a/cli/templateedit_test.go
+++ b/cli/templateedit_test.go
@@ -18,11 +18,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestTemplateEdit(t *testing.T) {
@@ -752,7 +752,7 @@ func TestTemplateEdit(t *testing.T) {
ctr.DefaultTTLMillis = nil
ctr.RestartRequirement = nil
ctr.FailureTTLMillis = nil
- ctr.InactivityTTLMillis = nil
+ ctr.TimeTilDormantMillis = nil
})
// Test the cli command with --allow-user-autostart.
@@ -798,7 +798,7 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis)
- assert.Equal(t, template.InactivityTTLMillis, updated.InactivityTTLMillis)
+ assert.Equal(t, template.TimeTilDormantMillis, updated.TimeTilDormantMillis)
})
t.Run("BlockedNotEntitled", func(t *testing.T) {
@@ -892,7 +892,7 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis)
- assert.Equal(t, template.InactivityTTLMillis, updated.InactivityTTLMillis)
+ assert.Equal(t, template.TimeTilDormantMillis, updated.TimeTilDormantMillis)
})
t.Run("Entitled", func(t *testing.T) {
t.Parallel()
@@ -990,7 +990,7 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis)
- assert.Equal(t, template.InactivityTTLMillis, updated.InactivityTTLMillis)
+ assert.Equal(t, template.TimeTilDormantMillis, updated.TimeTilDormantMillis)
})
})
}
diff --git a/cli/templateinit.go b/cli/templateinit.go
index b42e555fde074..47addbf05e347 100644
--- a/cli/templateinit.go
+++ b/cli/templateinit.go
@@ -12,11 +12,11 @@ import (
"golang.org/x/exp/maps"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/examples"
- "github.com/coder/coder/provisionersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/examples"
+ "github.com/coder/coder/v2/provisionersdk"
)
func (*RootCmd) templateInit() *clibase.Cmd {
diff --git a/cli/templateinit_test.go b/cli/templateinit_test.go
index ba99f76e95ece..f8172df25f560 100644
--- a/cli/templateinit_test.go
+++ b/cli/templateinit_test.go
@@ -6,8 +6,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestTemplateInit(t *testing.T) {
diff --git a/cli/templatelist.go b/cli/templatelist.go
index ba17bf218e695..6d95521dad321 100644
--- a/cli/templatelist.go
+++ b/cli/templatelist.go
@@ -5,9 +5,9 @@ import (
"github.com/fatih/color"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) templateList() *clibase.Cmd {
diff --git a/cli/templatelist_test.go b/cli/templatelist_test.go
index 8e29c8f49edbc..d639b7b1ebfe2 100644
--- a/cli/templatelist_test.go
+++ b/cli/templatelist_test.go
@@ -9,11 +9,11 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestTemplateList(t *testing.T) {
diff --git a/cli/templateplan.go b/cli/templateplan.go
deleted file mode 100644
index bc99d2f4e3cdf..0000000000000
--- a/cli/templateplan.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package cli
-
-import (
- "github.com/coder/coder/cli/clibase"
-)
-
-func (*RootCmd) templatePlan() *clibase.Cmd {
- return &clibase.Cmd{
- Use: "plan ",
- Middleware: clibase.Chain(
- clibase.RequireNArgs(1),
- ),
- Short: "Plan a template push from the current directory",
- Handler: func(inv *clibase.Invocation) error {
- return nil
- },
- }
-}
diff --git a/cli/templatepull.go b/cli/templatepull.go
index 5994807325713..eb772379b9611 100644
--- a/cli/templatepull.go
+++ b/cli/templatepull.go
@@ -9,9 +9,9 @@ import (
"github.com/codeclysm/extract/v3"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) templatePull() *clibase.Cmd {
@@ -83,7 +83,7 @@ func (r *RootCmd) templatePull() *clibase.Cmd {
}
if dest == "" {
- dest = templateName + "/"
+ dest = templateName
}
err = os.MkdirAll(dest, 0o750)
diff --git a/cli/templatepull_test.go b/cli/templatepull_test.go
index 1fdfe80d6ef50..5a5e7bc6c9e06 100644
--- a/cli/templatepull_test.go
+++ b/cli/templatepull_test.go
@@ -13,11 +13,11 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty/ptytest"
)
// dirSum calculates a checksum of the files in a directory.
@@ -40,174 +40,227 @@ func dirSum(t *testing.T, dir string) string {
return hex.EncodeToString(sum.Sum(nil))
}
-func TestTemplatePull(t *testing.T) {
+func TestTemplatePull_NoName(t *testing.T) {
t.Parallel()
- t.Run("NoName", func(t *testing.T) {
- t.Parallel()
+ inv, _ := clitest.New(t, "templates", "pull")
+ err := inv.Run()
+ require.Error(t, err)
+}
- inv, _ := clitest.New(t, "templates", "pull")
- err := inv.Run()
- require.Error(t, err)
- })
+// Stdout tests that 'templates pull' pulls down the latest template
+// and writes it to stdout.
+func TestTemplatePull_Stdout(t *testing.T) {
+ t.Parallel()
- // Stdout tests that 'templates pull' pulls down the latest template
- // and writes it to stdout.
- t.Run("Stdout", func(t *testing.T) {
- t.Parallel()
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
- client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
- user := coderdtest.CreateFirstUser(t, client)
+ // Create an initial template bundle.
+ source1 := genTemplateVersionSource()
+ // Create an updated template bundle. This will be used to ensure
+ // that templates are correctly returned in order from latest to oldest.
+ source2 := genTemplateVersionSource()
- // Create an initial template bundle.
- source1 := genTemplateVersionSource()
- // Create an updated template bundle. This will be used to ensure
- // that templates are correctly returned in order from latest to oldest.
- source2 := genTemplateVersionSource()
+ expected, err := echo.Tar(source2)
+ require.NoError(t, err)
- expected, err := echo.Tar(source2)
- require.NoError(t, err)
+ version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID)
- version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1)
- _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID)
- template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID)
+ // Update the template version so that we can assert that templates
+ // are being sorted correctly.
+ _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID)
- // Update the template version so that we can assert that templates
- // are being sorted correctly.
- _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID)
+ inv, root := clitest.New(t, "templates", "pull", "--tar", template.Name)
+ clitest.SetupConfig(t, client, root)
- inv, root := clitest.New(t, "templates", "pull", "--tar", template.Name)
- clitest.SetupConfig(t, client, root)
+ var buf bytes.Buffer
+ inv.Stdout = &buf
- var buf bytes.Buffer
- inv.Stdout = &buf
+ err = inv.Run()
+ require.NoError(t, err)
- err = inv.Run()
- require.NoError(t, err)
+ require.True(t, bytes.Equal(expected, buf.Bytes()), "tar files differ")
+}
- require.True(t, bytes.Equal(expected, buf.Bytes()), "tar files differ")
- })
+// ToDir tests that 'templates pull' pulls down the latest template
+// and writes it to the correct directory.
+func TestTemplatePull_ToDir(t *testing.T) {
+ t.Parallel()
- // ToDir tests that 'templates pull' pulls down the latest template
- // and writes it to the correct directory.
- t.Run("ToDir", func(t *testing.T) {
- t.Parallel()
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
- client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
- user := coderdtest.CreateFirstUser(t, client)
+ // Create an initial template bundle.
+ source1 := genTemplateVersionSource()
+ // Create an updated template bundle. This will be used to ensure
+ // that templates are correctly returned in order from latest to oldest.
+ source2 := genTemplateVersionSource()
- // Create an initial template bundle.
- source1 := genTemplateVersionSource()
- // Create an updated template bundle. This will be used to ensure
- // that templates are correctly returned in order from latest to oldest.
- source2 := genTemplateVersionSource()
+ expected, err := echo.Tar(source2)
+ require.NoError(t, err)
- expected, err := echo.Tar(source2)
- require.NoError(t, err)
+ version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID)
- version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1)
- _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID)
- template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID)
+ // Update the template version so that we can assert that templates
+ // are being sorted correctly.
+ _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID)
- // Update the template version so that we can assert that templates
- // are being sorted correctly.
- _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID)
+ dir := t.TempDir()
- dir := t.TempDir()
+ expectedDest := filepath.Join(dir, "expected")
+ actualDest := filepath.Join(dir, "actual")
+ ctx := context.Background()
- expectedDest := filepath.Join(dir, "expected")
- actualDest := filepath.Join(dir, "actual")
- ctx := context.Background()
+ err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil)
+ require.NoError(t, err)
- err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil)
- require.NoError(t, err)
+ inv, root := clitest.New(t, "templates", "pull", template.Name, actualDest)
+ clitest.SetupConfig(t, client, root)
- inv, root := clitest.New(t, "templates", "pull", template.Name, actualDest)
- clitest.SetupConfig(t, client, root)
+ ptytest.New(t).Attach(inv)
- ptytest.New(t).Attach(inv)
+ require.NoError(t, inv.Run())
- require.NoError(t, inv.Run())
+ require.Equal(t,
+ dirSum(t, expectedDest),
+ dirSum(t, actualDest),
+ )
+}
- require.Equal(t,
- dirSum(t, expectedDest),
- dirSum(t, actualDest),
- )
- })
+// ToDir tests that 'templates pull' pulls down the latest template
+// and writes it to a directory with the name of the template if the path is not implicitly supplied.
+// nolint: paralleltest
+func TestTemplatePull_ToImplicit(t *testing.T) {
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
- // FolderConflict tests that 'templates pull' fails when a folder with has
- // existing
- t.Run("FolderConflict", func(t *testing.T) {
- t.Parallel()
+ // Create an initial template bundle.
+ source1 := genTemplateVersionSource()
+ // Create an updated template bundle. This will be used to ensure
+ // that templates are correctly returned in order from latest to oldest.
+ source2 := genTemplateVersionSource()
- client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
- user := coderdtest.CreateFirstUser(t, client)
+ expected, err := echo.Tar(source2)
+ require.NoError(t, err)
- // Create an initial template bundle.
- source1 := genTemplateVersionSource()
- // Create an updated template bundle. This will be used to ensure
- // that templates are correctly returned in order from latest to oldest.
- source2 := genTemplateVersionSource()
+ version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID)
- expected, err := echo.Tar(source2)
- require.NoError(t, err)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID)
- version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1)
- _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID)
+ // Update the template version so that we can assert that templates
+ // are being sorted correctly.
+ _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID)
- template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID)
+ // create a tempdir and change the working directory to it for the duration of the test (cannot run in parallel)
+ dir := t.TempDir()
+ wd, err := os.Getwd()
+ require.NoError(t, err)
+ err = os.Chdir(dir)
+ require.NoError(t, err)
+ defer func() {
+ err := os.Chdir(wd)
+ require.NoError(t, err, "if this fails, it can break other subsequent tests due to wrong working directory")
+ }()
- // Update the template version so that we can assert that templates
- // are being sorted correctly.
- _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID)
+ expectedDest := filepath.Join(dir, "expected")
+ actualDest := filepath.Join(dir, template.Name)
- dir := t.TempDir()
+ ctx := context.Background()
- expectedDest := filepath.Join(dir, "expected")
- conflictDest := filepath.Join(dir, "conflict")
+ err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil)
+ require.NoError(t, err)
- err = os.MkdirAll(conflictDest, 0o700)
- require.NoError(t, err)
+ inv, root := clitest.New(t, "templates", "pull", template.Name)
+ clitest.SetupConfig(t, client, root)
- err = os.WriteFile(
- filepath.Join(conflictDest, "conflict-file"),
- []byte("conflict"), 0o600,
- )
- require.NoError(t, err)
+ ptytest.New(t).Attach(inv)
- ctx := context.Background()
+ require.NoError(t, inv.Run())
- err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil)
- require.NoError(t, err)
+ require.Equal(t,
+ dirSum(t, expectedDest),
+ dirSum(t, actualDest),
+ )
+}
+
+// FolderConflict tests that 'templates pull' fails when a folder with has
+// existing
+func TestTemplatePull_FolderConflict(t *testing.T) {
+ t.Parallel()
- inv, root := clitest.New(t, "templates", "pull", template.Name, conflictDest)
- clitest.SetupConfig(t, client, root)
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
- pty := ptytest.New(t).Attach(inv)
+ // Create an initial template bundle.
+ source1 := genTemplateVersionSource()
+ // Create an updated template bundle. This will be used to ensure
+ // that templates are correctly returned in order from latest to oldest.
+ source2 := genTemplateVersionSource()
- waiter := clitest.StartWithWaiter(t, inv)
+ expected, err := echo.Tar(source2)
+ require.NoError(t, err)
- pty.ExpectMatch("not empty")
- pty.WriteLine("no")
+ version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, source1)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID)
- waiter.RequireError()
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID)
- ents, err := os.ReadDir(conflictDest)
- require.NoError(t, err)
+ // Update the template version so that we can assert that templates
+ // are being sorted correctly.
+ _ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, source2, template.ID)
+
+ dir := t.TempDir()
+
+ expectedDest := filepath.Join(dir, "expected")
+ conflictDest := filepath.Join(dir, "conflict")
+
+ err = os.MkdirAll(conflictDest, 0o700)
+ require.NoError(t, err)
+
+ err = os.WriteFile(
+ filepath.Join(conflictDest, "conflict-file"),
+ []byte("conflict"), 0o600,
+ )
+ require.NoError(t, err)
+
+ ctx := context.Background()
+
+ err = extract.Tar(ctx, bytes.NewReader(expected), expectedDest, nil)
+ require.NoError(t, err)
+
+ inv, root := clitest.New(t, "templates", "pull", template.Name, conflictDest)
+ clitest.SetupConfig(t, client, root)
+
+ pty := ptytest.New(t).Attach(inv)
+
+ waiter := clitest.StartWithWaiter(t, inv)
+
+ pty.ExpectMatch("not empty")
+ pty.WriteLine("no")
+
+ waiter.RequireError()
+
+ ents, err := os.ReadDir(conflictDest)
+ require.NoError(t, err)
- require.Len(t, ents, 1, "conflict folder should have single conflict file")
- })
+ require.Len(t, ents, 1, "conflict folder should have single conflict file")
}
// genTemplateVersionSource returns a unique bundle that can be used to create
// a template version source.
func genTemplateVersionSource() *echo.Responses {
return &echo.Responses{
- Parse: []*proto.Parse_Response{
+ Parse: []*proto.Response{
{
- Type: &proto.Parse_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Output: uuid.NewString(),
},
@@ -215,11 +268,11 @@ func genTemplateVersionSource() *echo.Responses {
},
{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{},
+ Type: &proto.Response_Parse{
+ Parse: &proto.ParseComplete{},
},
},
},
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
}
}
diff --git a/cli/templatepush.go b/cli/templatepush.go
index 52342874144b7..7e676780f7a82 100644
--- a/cli/templatepush.go
+++ b/cli/templatepush.go
@@ -11,11 +11,11 @@ import (
"github.com/briandowns/spinner"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisionersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisionersdk"
)
// templateUploadFlags is shared by `templates create` and `templates push`.
diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go
index 88a1ce250543c..4c41597802bb2 100644
--- a/cli/templatepush_test.go
+++ b/cli/templatepush_test.go
@@ -13,14 +13,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestTemplatePush(t *testing.T) {
@@ -38,7 +38,7 @@ func TestTemplatePush(t *testing.T) {
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example")
clitest.SetupConfig(t, client, root)
@@ -82,7 +82,7 @@ func TestTemplatePush(t *testing.T) {
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
wantMessage := strings.Repeat("a", 72)
@@ -121,7 +121,7 @@ func TestTemplatePush(t *testing.T) {
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
@@ -168,7 +168,7 @@ func TestTemplatePush(t *testing.T) {
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
@@ -211,7 +211,7 @@ func TestTemplatePush(t *testing.T) {
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl")))
@@ -248,7 +248,7 @@ func TestTemplatePush(t *testing.T) {
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
inv, root := clitest.New(t, "templates", "push", template.Name, "--activate=false", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", "example")
clitest.SetupConfig(t, client, root)
@@ -293,7 +293,7 @@ func TestTemplatePush(t *testing.T) {
// Test the cli command.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID,
@@ -340,7 +340,7 @@ func TestTemplatePush(t *testing.T) {
source, err := echo.Tar(&echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
})
require.NoError(t, err)
@@ -619,10 +619,7 @@ func TestTemplatePush(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
- source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionApply: provisionCompleteWithAgent,
- })
+ source := clitest.CreateTemplateVersionSource(t, completeWithAgent())
const templateName = "my-template"
args := []string{
@@ -665,16 +662,16 @@ func TestTemplatePush(t *testing.T) {
func createEchoResponsesWithTemplateVariables(templateVariables []*proto.TemplateVariable) *echo.Responses {
return &echo.Responses{
- Parse: []*proto.Parse_Response{
+ Parse: []*proto.Response{
{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
+ Type: &proto.Response_Parse{
+ Parse: &proto.ParseComplete{
TemplateVariables: templateVariables,
},
},
},
},
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
}
}
diff --git a/cli/templates.go b/cli/templates.go
index ad347a138ba91..7ded6a7e5ee2b 100644
--- a/cli/templates.go
+++ b/cli/templates.go
@@ -5,9 +5,9 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) templates() *clibase.Cmd {
@@ -37,7 +37,6 @@ func (r *RootCmd) templates() *clibase.Cmd {
r.templateEdit(),
r.templateInit(),
r.templateList(),
- r.templatePlan(),
r.templatePush(),
r.templateVersions(),
r.templateDelete(),
diff --git a/cli/templatevariables.go b/cli/templatevariables.go
index 888b8a04e30f5..801e65cb8d82f 100644
--- a/cli/templatevariables.go
+++ b/cli/templatevariables.go
@@ -7,7 +7,7 @@ import (
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
func loadVariableValuesFromFile(variablesFile string) ([]codersdk.VariableValue, error) {
diff --git a/cli/templateversions.go b/cli/templateversions.go
index ed7688d3f3108..622854c4afedc 100644
--- a/cli/templateversions.go
+++ b/cli/templateversions.go
@@ -8,9 +8,9 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) templateVersions() *clibase.Cmd {
diff --git a/cli/templateversions_test.go b/cli/templateversions_test.go
index f3be61322c29a..c7c4da549528c 100644
--- a/cli/templateversions_test.go
+++ b/cli/templateversions_test.go
@@ -5,9 +5,9 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestTemplateVersions(t *testing.T) {
diff --git a/cli/testdata/coder_--help.golden b/cli/testdata/coder_--help.golden
index e074756680e84..6e988a9f568fd 100644
--- a/cli/testdata/coder_--help.golden
+++ b/cli/testdata/coder_--help.golden
@@ -62,6 +62,11 @@ variables or flags.
Additional HTTP headers added to all requests. Provide as key=value.
Can be specified multiple times.
+ --header-command string, $CODER_HEADER_COMMAND
+ An external command that outputs additional HTTP headers added to all
+ requests. The command must output each header as `key=value` on its
+ own line.
+
--no-feature-warning bool, $CODER_NO_FEATURE_WARNING
Suppress warnings about unlicensed features.
diff --git a/cli/testdata/coder_create_--help.golden b/cli/testdata/coder_create_--help.golden
index 6c8bcc46908c9..2e080b6e85ca7 100644
--- a/cli/testdata/coder_create_--help.golden
+++ b/cli/testdata/coder_create_--help.golden
@@ -7,6 +7,9 @@ Create a workspace
[40m [0m[91;40m$ coder create /[0m[40m [0m
[1mOptions[0m
+ --parameter string-array, $CODER_RICH_PARAMETER
+ Rich parameter value in the format "name=value".
+
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.
diff --git a/cli/testdata/coder_list_--help.golden b/cli/testdata/coder_list_--help.golden
index a9bb8218ba1c0..e4e6f5dd3524a 100644
--- a/cli/testdata/coder_list_--help.golden
+++ b/cli/testdata/coder_list_--help.golden
@@ -11,7 +11,7 @@ Aliases: ls
-c, --column string-array (default: workspace,template,status,healthy,last built,outdated,starts at,stops after)
Columns to display in table output. Available columns: workspace,
template, status, healthy, last built, outdated, starts at, stops
- after.
+ after, daily cost.
-o, --output string (default: table)
Output format. Available formats: table, json.
diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden
index 49e51d408285c..2e317f996047b 100644
--- a/cli/testdata/coder_list_--output_json.golden
+++ b/cli/testdata/coder_list_--output_json.golden
@@ -11,6 +11,7 @@
"template_display_name": "",
"template_icon": "",
"template_allow_user_cancel_workspace_jobs": false,
+ "template_active_version_id": "[version ID]",
"latest_build": {
"id": "[workspace build ID]",
"created_at": "[timestamp]",
@@ -52,7 +53,7 @@
"ttl_ms": 28800000,
"last_used_at": "[timestamp]",
"deleting_at": null,
- "locked_at": null,
+ "dormant_at": null,
"health": {
"healthy": true,
"failing_agents": []
diff --git a/cli/testdata/coder_restart_--help.golden b/cli/testdata/coder_restart_--help.golden
index c2079b9065dca..e16a6f9ff7e99 100644
--- a/cli/testdata/coder_restart_--help.golden
+++ b/cli/testdata/coder_restart_--help.golden
@@ -3,6 +3,9 @@ Usage: coder restart [flags]
Restart a workspace
[1mOptions[0m
+ --build-option string-array, $CODER_BUILD_OPTION
+ Build option value in the format "name=value".
+
--build-options bool
Prompt for one-time build options defined with ephemeral parameters.
diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden
index cb7ca61b4913a..d3a5d74bcddbe 100644
--- a/cli/testdata/coder_server_--help.golden
+++ b/cli/testdata/coder_server_--help.golden
@@ -172,21 +172,25 @@ backed by Tailscale and WireGuard.
URL to fetch a DERP mapping on startup. See:
https://tailscale.com/kb/1118/custom-derp-servers/.
+ --derp-force-websockets bool, $CODER_DERP_FORCE_WEBSOCKETS
+ Force clients and agents to always use WebSocket to connect to DERP
+ relay servers. By default, DERP uses `Upgrade: derp`, which may cause
+ issues with some reverse proxies. Clients may automatically fallback
+ to WebSocket if they detect an issue with `Upgrade: derp`, but this
+ does not work in all situations.
+
--derp-server-enable bool, $CODER_DERP_SERVER_ENABLE (default: true)
Whether to enable or disable the embedded DERP relay server.
- --derp-server-region-code string, $CODER_DERP_SERVER_REGION_CODE (default: coder)
- Region code to use for the embedded DERP server.
-
- --derp-server-region-id int, $CODER_DERP_SERVER_REGION_ID (default: 999)
- Region ID to use for the embedded DERP server.
-
--derp-server-region-name string, $CODER_DERP_SERVER_REGION_NAME (default: Coder Embedded Relay)
Region name that for the embedded DERP server.
- --derp-server-stun-addresses string-array, $CODER_DERP_SERVER_STUN_ADDRESSES (default: stun.l.google.com:19302)
- Addresses for STUN servers to establish P2P connections. Use special
- value 'disable' to turn off STUN.
+ --derp-server-stun-addresses string-array, $CODER_DERP_SERVER_STUN_ADDRESSES (default: stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302)
+ Addresses for STUN servers to establish P2P connections. It's
+ recommended to have at least two STUN servers to give users the best
+ chance of connecting P2P to workspaces. Each STUN server will get it's
+ own DERP region, with region IDs starting at `--derp-server-region-id
+ + 1`. Use special value 'disable' to turn off STUN completely.
[1mNetworking / HTTP Options[0m
--disable-password-auth bool, $CODER_DISABLE_PASSWORD_AUTH
@@ -298,15 +302,28 @@ can safely ignore these settings.
GitHub.
[1mOIDC Options[0m
+ --oidc-group-auto-create bool, $CODER_OIDC_GROUP_AUTO_CREATE (default: false)
+ Automatically creates missing groups from a user's groups claim.
+
--oidc-allow-signups bool, $CODER_OIDC_ALLOW_SIGNUPS (default: true)
Whether new users can sign up with OIDC.
--oidc-auth-url-params struct[map[string]string], $CODER_OIDC_AUTH_URL_PARAMS (default: {"access_type": "offline"})
OIDC auth URL parameters to pass to the upstream provider.
+ --oidc-client-cert-file string, $CODER_OIDC_CLIENT_CERT_FILE
+ Pem encoded certificate file to use for oauth2 PKI/JWT authorization.
+ The public certificate that accompanies oidc-client-key-file. A
+ standard x509 certificate is expected.
+
--oidc-client-id string, $CODER_OIDC_CLIENT_ID
Client ID to use for Login with OIDC.
+ --oidc-client-key-file string, $CODER_OIDC_CLIENT_KEY_FILE
+ Pem encoded RSA private key to use for oauth2 PKI/JWT authorization.
+ This can be used instead of oidc-client-secret if your IDP supports
+ it.
+
--oidc-client-secret string, $CODER_OIDC_CLIENT_SECRET
Client secret to use for Login with OIDC.
@@ -334,6 +351,11 @@ can safely ignore these settings.
--oidc-issuer-url string, $CODER_OIDC_ISSUER_URL
Issuer URL to use for Login with OIDC.
+ --oidc-group-regex-filter regexp, $CODER_OIDC_GROUP_REGEX_FILTER (default: .*)
+ If provided any group name not matching the regex is ignored. This
+ allows for filtering out groups that are not needed. This filter is
+ applied after the group mapping.
+
--oidc-scopes string-array, $CODER_OIDC_SCOPES (default: openid,profile,email)
Scopes to grant when authenticating with OIDC.
@@ -373,6 +395,10 @@ updating, and deleting workspace resources.
--provisioner-daemon-poll-jitter duration, $CODER_PROVISIONER_DAEMON_POLL_JITTER (default: 100ms)
Random jitter added to the poll interval.
+ --provisioner-daemon-psk string, $CODER_PROVISIONER_DAEMON_PSK
+ Pre-shared key to authenticate external provisioner daemons to Coder
+ server.
+
--provisioner-daemons int, $CODER_PROVISIONER_DAEMONS (default: 3)
Number of provisioner daemons to create on start. If builds are stuck
in queued state for a long time, consider increasing this.
diff --git a/cli/testdata/coder_start_--help.golden b/cli/testdata/coder_start_--help.golden
index aa447240e9bbb..b03c9975925f4 100644
--- a/cli/testdata/coder_start_--help.golden
+++ b/cli/testdata/coder_start_--help.golden
@@ -3,6 +3,9 @@ Usage: coder start [flags]
Start a workspace
[1mOptions[0m
+ --build-option string-array, $CODER_BUILD_OPTION
+ Build option value in the format "name=value".
+
--build-options bool
Prompt for one-time build options defined with ephemeral parameters.
diff --git a/cli/testdata/coder_templates_--help.golden b/cli/testdata/coder_templates_--help.golden
index 0bcc6c7978df7..352695e26fb57 100644
--- a/cli/testdata/coder_templates_--help.golden
+++ b/cli/testdata/coder_templates_--help.golden
@@ -24,7 +24,6 @@ Templates are written in standard Terraform and describe the infrastructure for
edit Edit the metadata of a template by name.
init Get started with a templated template.
list List all the templates available for the organization
- plan Plan a template push from the current directory
pull Download the latest version of a template to a path.
push Push a new template version from the current directory or as
specified by flag
diff --git a/cli/testdata/coder_templates_create_--help.golden b/cli/testdata/coder_templates_create_--help.golden
index a88fe64bdeba3..ce71793cebc27 100644
--- a/cli/testdata/coder_templates_create_--help.golden
+++ b/cli/testdata/coder_templates_create_--help.golden
@@ -4,14 +4,18 @@ Create a template from the current directory or as specified by flag
[1mOptions[0m
--default-ttl duration (default: 24h)
- Specify a default TTL for workspaces created from this template.
+ Specify a default TTL for workspaces created from this template. It is
+ the default time before shutdown - workspaces created from this
+ template default to this value. Maps to "Default autostop" in the UI.
-d, --directory string (default: .)
Specify the directory to create from, use '-' to read tar from stdin.
--failure-ttl duration (default: 0h)
- Specify a failure TTL for workspaces created from this template. This
- licensed feature's default is 0h (off).
+ Specify a failure TTL for workspaces created from this template. It is
+ the amount of time after a failed "start" build before coder
+ automatically schedules a "stop" build to cleanup.This licensed
+ feature's default is 0h (off). Maps to "Failure cleanup"in the UI.
--ignore-lockfile bool (default: false)
Ignore warnings about not having a .terraform.lock.hcl file present in
@@ -19,7 +23,15 @@ Create a template from the current directory or as specified by flag
--inactivity-ttl duration (default: 0h)
Specify an inactivity TTL for workspaces created from this template.
- This licensed feature's default is 0h (off).
+ It is the amount of time the workspace is not used before it is be
+ stopped and auto-locked. This includes across multiple builds (e.g.
+ auto-starts and stops). This licensed feature's default is 0h (off).
+ Maps to "Dormancy threshold" in the UI.
+
+ --max-ttl duration
+ Edit the template maximum time before shutdown - workspaces created
+ from this template must shutdown within the given duration after
+ starting. This is an enterprise-only feature.
-m, --message string
Specify a message describing the changes in this version of the
diff --git a/cli/testdata/coder_templates_edit_--help.golden b/cli/testdata/coder_templates_edit_--help.golden
index 09c0b7209e78a..19dfcd2953c33 100644
--- a/cli/testdata/coder_templates_edit_--help.golden
+++ b/cli/testdata/coder_templates_edit_--help.golden
@@ -16,7 +16,8 @@ Edit the metadata of a template by name.
--default-ttl duration
Edit the template default time before shutdown - workspaces created
- from this template default to this value.
+ from this template default to this value. Maps to "Default autostop"
+ in the UI.
--description string
Edit the template description.
@@ -25,20 +26,26 @@ Edit the metadata of a template by name.
Edit the template display name.
--failure-ttl duration (default: 0h)
- Specify a failure TTL for workspaces created from this template. This
- licensed feature's default is 0h (off).
+ Specify a failure TTL for workspaces created from this template. It is
+ the amount of time after a failed "start" build before coder
+ automatically schedules a "stop" build to cleanup.This licensed
+ feature's default is 0h (off). Maps to "Failure cleanup" in the UI.
--icon string
Edit the template icon path.
--inactivity-ttl duration (default: 0h)
Specify an inactivity TTL for workspaces created from this template.
- This licensed feature's default is 0h (off).
+ It is the amount of time the workspace is not used before it is be
+ stopped and auto-locked. This includes across multiple builds (e.g.
+ auto-starts and stops). This licensed feature's default is 0h (off).
+ Maps to "Dormancy threshold" in the UI.
--max-ttl duration
Edit the template maximum time before shutdown - workspaces created
from this template must shutdown within the given duration after
- starting. This is an enterprise-only feature.
+ starting, regardless of user activity. This is an enterprise-only
+ feature. Maps to "Max lifetime" in the UI.
--name string
Edit the template name.
diff --git a/cli/testdata/coder_update_--help.golden b/cli/testdata/coder_update_--help.golden
index 40e899cd37348..669bda831caa6 100644
--- a/cli/testdata/coder_update_--help.golden
+++ b/cli/testdata/coder_update_--help.golden
@@ -9,9 +9,15 @@ Use --always-prompt to change the parameter values of the workspace.
Always prompt all parameters. Does not pull parameter values from
existing workspace.
+ --build-option string-array, $CODER_BUILD_OPTION
+ Build option value in the format "name=value".
+
--build-options bool
Prompt for one-time build options defined with ephemeral parameters.
+ --parameter string-array, $CODER_RICH_PARAMETER
+ Rich parameter value in the format "name=value".
+
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.
diff --git a/cli/testdata/coder_users_create_--help.golden b/cli/testdata/coder_users_create_--help.golden
index bb94cac633bc0..275e89803d4c6 100644
--- a/cli/testdata/coder_users_create_--help.golden
+++ b/cli/testdata/coder_users_create_--help.golden
@@ -1,15 +1,15 @@
Usage: coder users create [flags]
[1mOptions[0m
- --disable-login bool
- Disabling login for a user prevents the user from authenticating via
- password or IdP login. Authentication requires an API key/token
- generated by an admin. Be careful when using this flag as it can lock
- the user out of their account.
-
-e, --email string
Specifies an email address for the new user.
+ --login-type string
+ Optionally specify the login type for the user. Valid values are:
+ password, none, github, oidc. Using 'none' prevents the user from
+ authenticating and requires an API key/token to be generated by an
+ admin.
+
-p, --password string
Specifies a password for the new user.
diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden
index c7a8df03414e6..166e9f02d9465 100644
--- a/cli/testdata/server-config.yaml.golden
+++ b/cli/testdata/server-config.yaml.golden
@@ -111,11 +111,20 @@ networking:
# Region name that for the embedded DERP server.
# (default: Coder Embedded Relay, type: string)
regionName: Coder Embedded Relay
- # Addresses for STUN servers to establish P2P connections. Use special value
- # 'disable' to turn off STUN.
- # (default: stun.l.google.com:19302, type: string-array)
+ # Addresses for STUN servers to establish P2P connections. It's recommended to
+ # have at least two STUN servers to give users the best chance of connecting P2P
+ # to workspaces. Each STUN server will get it's own DERP region, with region IDs
+ # starting at `--derp-server-region-id + 1`. Use special value 'disable' to turn
+ # off STUN completely.
+ # (default:
+ # stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302,
+ # type: string-array)
stunAddresses:
- stun.l.google.com:19302
+ - stun1.l.google.com:19302
+ - stun2.l.google.com:19302
+ - stun3.l.google.com:19302
+ - stun4.l.google.com:19302
# An HTTP URL that is accessible by other replicas to relay DERP traffic. Required
# for high availability.
# (default: , type: url)
@@ -127,6 +136,12 @@ networking:
# this change has been made, but new connections will still be proxied regardless.
# (default: , type: bool)
blockDirect: false
+ # Force clients and agents to always use WebSocket to connect to DERP relay
+ # servers. By default, DERP uses `Upgrade: derp`, which may cause issues with some
+ # reverse proxies. Clients may automatically fallback to WebSocket if they detect
+ # an issue with `Upgrade: derp`, but this does not work in all situations.
+ # (default: , type: bool)
+ forceWebSockets: false
# URL to fetch a DERP mapping on startup. See:
# https://tailscale.com/kb/1118/custom-derp-servers/.
# (default: , type: string)
@@ -235,6 +250,15 @@ oidc:
# Client ID to use for Login with OIDC.
# (default: , type: string)
clientID: ""
+ # Pem encoded RSA private key to use for oauth2 PKI/JWT authorization. This can be
+ # used instead of oidc-client-secret if your IDP supports it.
+ # (default: , type: string)
+ oidcClientKeyFile: ""
+ # Pem encoded certificate file to use for oauth2 PKI/JWT authorization. The public
+ # certificate that accompanies oidc-client-key-file. A standard x509 certificate
+ # is expected.
+ # (default: , type: string)
+ oidcClientCertFile: ""
# Email domains that clients logging in with OIDC must match.
# (default: , type: string-array)
emailDomain: []
@@ -271,6 +295,14 @@ oidc:
# for when OIDC providers only return group IDs.
# (default: {}, type: struct[map[string]string])
groupMapping: {}
+ # Automatically creates missing groups from a user's groups claim.
+ # (default: false, type: bool)
+ enableGroupAutoCreate: false
+ # If provided any group name not matching the regex is ignored. This allows for
+ # filtering out groups that are not needed. This filter is applied after the group
+ # mapping.
+ # (default: .*, type: regexp)
+ groupRegexFilter: .*
# This field must be set if using the user roles sync feature. Set this to the
# name of the claim used to store the user's role. The roles should be sent as an
# array of strings.
@@ -327,6 +359,9 @@ provisioning:
# Time to force cancel provisioning tasks that are stuck.
# (default: 10m0s, type: duration)
forceCancelInterval: 10m0s
+ # Pre-shared key to authenticate external provisioner daemons to Coder server.
+ # (default: , type: string)
+ daemonPSK: ""
# Enable one or more experiments. These are not ready for production. Separate
# multiple experiments with commas, or enter '*' to opt-in to all available
# experiments.
diff --git a/cli/tokens.go b/cli/tokens.go
index 1f34287c4e9a3..579a15fc5f1fe 100644
--- a/cli/tokens.go
+++ b/cli/tokens.go
@@ -8,9 +8,9 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) tokens() *clibase.Cmd {
diff --git a/cli/tokens_test.go b/cli/tokens_test.go
index f31dd847e0396..fdb062b959a3b 100644
--- a/cli/tokens_test.go
+++ b/cli/tokens_test.go
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestTokens(t *testing.T) {
diff --git a/cli/update.go b/cli/update.go
index 64710217bb996..cdff4b4a8df26 100644
--- a/cli/update.go
+++ b/cli/update.go
@@ -3,14 +3,15 @@ package cli
import (
"fmt"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/codersdk"
+ "golang.org/x/xerrors"
+
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) update() *clibase.Cmd {
var (
- richParameterFile string
- alwaysPrompt bool
+ alwaysPrompt bool
parameterFlags workspaceParameterFlags
)
@@ -30,33 +31,45 @@ func (r *RootCmd) update() *clibase.Cmd {
if err != nil {
return err
}
- if !workspace.Outdated && !alwaysPrompt && !parameterFlags.buildOptions {
+ if !workspace.Outdated && !alwaysPrompt && !parameterFlags.promptBuildOptions && len(parameterFlags.buildOptions) == 0 {
_, _ = fmt.Fprintf(inv.Stdout, "Workspace isn't outdated!\n")
return nil
}
+
+ buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
+ if err != nil {
+ return err
+ }
+
template, err := client.Template(inv.Context(), workspace.TemplateID)
if err != nil {
return err
}
- var existingRichParams []codersdk.WorkspaceBuildParameter
- if !alwaysPrompt {
- existingRichParams, err = client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
- if err != nil {
- return err
- }
+ lastBuildParameters, err := client.WorkspaceBuildParameters(inv.Context(), workspace.LatestBuild.ID)
+ if err != nil {
+ return err
+ }
+
+ cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
+ if err != nil {
+ return xerrors.Errorf("can't parse given parameter values: %w", err)
}
- buildParams, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
- Template: template,
- ExistingRichParams: existingRichParams,
- RichParameterFile: richParameterFile,
- NewWorkspaceName: workspace.Name,
+ buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
+ Action: WorkspaceUpdate,
+ Template: template,
+ NewWorkspaceName: workspace.Name,
+ WorkspaceID: workspace.LatestBuild.ID,
- UpdateWorkspace: true,
- WorkspaceID: workspace.LatestBuild.ID,
+ LastBuildParameters: lastBuildParameters,
- BuildOptions: parameterFlags.buildOptions,
+ PromptBuildOptions: parameterFlags.promptBuildOptions,
+ BuildOptions: buildOptions,
+
+ PromptRichParameters: alwaysPrompt,
+ RichParameters: cliRichParameters,
+ RichParameterFile: parameterFlags.richParameterFile,
})
if err != nil {
return err
@@ -65,7 +78,7 @@ func (r *RootCmd) update() *clibase.Cmd {
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransitionStart,
- RichParameterValues: buildParams.richParameters,
+ RichParameterValues: buildParameters,
})
if err != nil {
return err
@@ -92,13 +105,8 @@ func (r *RootCmd) update() *clibase.Cmd {
Description: "Always prompt all parameters. Does not pull parameter values from existing workspace.",
Value: clibase.BoolOf(&alwaysPrompt),
},
- {
- Flag: "rich-parameter-file",
- Description: "Specify a file path with values for rich parameters defined in the template.",
- Env: "CODER_RICH_PARAMETER_FILE",
- Value: clibase.StringOf(&richParameterFile),
- },
}
- cmd.Options = append(cmd.Options, parameterFlags.options()...)
+ cmd.Options = append(cmd.Options, parameterFlags.cliBuildOptions()...)
+ cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
return cmd
}
diff --git a/cli/update_test.go b/cli/update_test.go
index 886adf9bea264..38b042d2813f0 100644
--- a/cli/update_test.go
+++ b/cli/update_test.go
@@ -9,14 +9,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestUpdate(t *testing.T) {
@@ -57,8 +57,8 @@ func TestUpdate(t *testing.T) {
version2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
+ ProvisionPlan: echo.PlanComplete,
}, template.ID)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version2.ID)
@@ -100,28 +100,13 @@ func TestUpdateWithRichParameters(t *testing.T) {
immutableParameterValue = "4"
)
- echoResponses := &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: []*proto.RichParameter{
- {Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
- {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
- {Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
- {Name: ephemeralParameterName, Description: ephemeralParameterDescription, Mutable: true, Ephemeral: true},
- },
- },
- },
- },
- },
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- }},
- }
+ echoResponses := prepareEchoResponses([]*proto.RichParameter{
+ {Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
+ {Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
+ {Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
+ {Name: ephemeralParameterName, Description: ephemeralParameterDescription, Mutable: true, Ephemeral: true},
+ },
+ )
t.Run("ImmutableCannotBeCustomized", func(t *testing.T) {
t.Parallel()
@@ -159,7 +144,7 @@ func TestUpdateWithRichParameters(t *testing.T) {
matches := []string{
firstParameterDescription, firstParameterValue,
- fmt.Sprintf("Parameter %q is not mutable, so can't be customized after workspace creation.", immutableParameterName), "",
+ fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", immutableParameterName), "",
secondParameterDescription, secondParameterValue,
}
for i := 0; i < len(matches); i += 2 {
@@ -236,6 +221,55 @@ func TestUpdateWithRichParameters(t *testing.T) {
Value: ephemeralParameterValue,
})
})
+
+ t.Run("BuildOptionFlags", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+
+ const workspaceName = "my-workspace"
+
+ inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y",
+ "--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
+ "--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue),
+ "--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue))
+ clitest.SetupConfig(t, client, root)
+ err := inv.Run()
+ assert.NoError(t, err)
+
+ inv, root = clitest.New(t, "update", workspaceName,
+ "--build-option", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue))
+ clitest.SetupConfig(t, client, root)
+
+ doneChan := make(chan struct{})
+ pty := ptytest.New(t).Attach(inv)
+ go func() {
+ defer close(doneChan)
+ err := inv.Run()
+ assert.NoError(t, err)
+ }()
+
+ pty.ExpectMatch("Planning workspace")
+ <-doneChan
+
+ // Verify if build option is set
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
+ defer cancel()
+
+ workspace, err := client.WorkspaceByOwnerAndName(ctx, user.UserID.String(), workspaceName, codersdk.WorkspaceOptions{})
+ require.NoError(t, err)
+ actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
+ require.NoError(t, err)
+ require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
+ Name: ephemeralParameterName,
+ Value: ephemeralParameterValue,
+ })
+ })
}
func TestUpdateValidateRichParameters(t *testing.T) {
@@ -264,28 +298,6 @@ func TestUpdateValidateRichParameters(t *testing.T) {
{Name: boolParameterName, Type: "bool", Mutable: true},
}
- prepareEchoResponses := func(richParameters []*proto.RichParameter) *echo.Responses {
- return &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: richParameters,
- },
- },
- },
- },
- ProvisionApply: []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- },
- },
- }
- }
-
t.Run("ValidateString", func(t *testing.T) {
t.Parallel()
@@ -544,15 +556,258 @@ func TestUpdateValidateRichParameters(t *testing.T) {
assert.NoError(t, err)
}()
+ pty.ExpectMatch("Planning workspace...")
+ <-doneChan
+ })
+
+ t.Run("ParameterOptionChanged", func(t *testing.T) {
+ t.Parallel()
+
+ // Create template and workspace
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+
+ templateParameters := []*proto.RichParameter{
+ {Name: stringParameterName, Type: "string", Mutable: true, Required: true, Options: []*proto.RichParameterOption{
+ {Name: "First option", Description: "This is first option", Value: "1st"},
+ {Name: "Second option", Description: "This is second option", Value: "2nd"},
+ {Name: "Third option", Description: "This is third option", Value: "3rd"},
+ }},
+ }
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(templateParameters))
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+
+ inv, root := clitest.New(t, "create", "my-workspace", "--yes", "--template", template.Name, "--parameter", fmt.Sprintf("%s=%s", stringParameterName, "2nd"))
+ clitest.SetupConfig(t, client, root)
+ err := inv.Run()
+ require.NoError(t, err)
+
+ // Update template
+ updatedTemplateParameters := []*proto.RichParameter{
+ {Name: stringParameterName, Type: "string", Mutable: true, Required: true, Options: []*proto.RichParameterOption{
+ {Name: "first_option", Description: "This is first option", Value: "1"},
+ {Name: "second_option", Description: "This is second option", Value: "2"},
+ {Name: "third_option", Description: "This is third option", Value: "3"},
+ }},
+ }
+
+ updatedVersion := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(updatedTemplateParameters), template.ID)
+ coderdtest.AwaitTemplateVersionJob(t, client, updatedVersion.ID)
+ err = client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
+ ID: updatedVersion.ID,
+ })
+ require.NoError(t, err)
+
+ // Update the workspace
+ inv, root = clitest.New(t, "update", "my-workspace")
+ clitest.SetupConfig(t, client, root)
+
+ pty := ptytest.New(t).Attach(inv)
+ clitest.Start(t, inv)
+
matches := []string{
- "added_parameter", "",
- `Enter a value (default: "foobar")`, "abc",
+ stringParameterName, "second_option",
+ "Planning workspace...", "",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
- pty.WriteLine(value)
+
+ if value != "" {
+ pty.WriteLine(value)
+ }
+ }
+ })
+
+ t.Run("ParameterOptionDisappeared", func(t *testing.T) {
+ t.Parallel()
+
+ // Create template and workspace
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+
+ templateParameters := []*proto.RichParameter{
+ {Name: stringParameterName, Type: "string", Mutable: true, Required: true, Options: []*proto.RichParameterOption{
+ {Name: "First option", Description: "This is first option", Value: "1st"},
+ {Name: "Second option", Description: "This is second option", Value: "2nd"},
+ {Name: "Third option", Description: "This is third option", Value: "3rd"},
+ }},
+ }
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(templateParameters))
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+
+ inv, root := clitest.New(t, "create", "my-workspace", "--yes", "--template", template.Name, "--parameter", fmt.Sprintf("%s=%s", stringParameterName, "2nd"))
+ clitest.SetupConfig(t, client, root)
+ err := inv.Run()
+ require.NoError(t, err)
+
+ // Update template - 2nd option disappeared, 4th option added
+ updatedTemplateParameters := []*proto.RichParameter{
+ {Name: stringParameterName, Type: "string", Mutable: true, Required: true, Options: []*proto.RichParameterOption{
+ {Name: "First option", Description: "This is first option", Value: "1st"},
+ {Name: "Third option", Description: "This is third option", Value: "3rd"},
+ {Name: "Fourth option", Description: "This is fourth option", Value: "4th"},
+ }},
+ }
+
+ updatedVersion := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(updatedTemplateParameters), template.ID)
+ coderdtest.AwaitTemplateVersionJob(t, client, updatedVersion.ID)
+ err = client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
+ ID: updatedVersion.ID,
+ })
+ require.NoError(t, err)
+
+ // Update the workspace
+ inv, root = clitest.New(t, "update", "my-workspace")
+ clitest.SetupConfig(t, client, root)
+ pty := ptytest.New(t).Attach(inv)
+ clitest.Start(t, inv)
+
+ matches := []string{
+ stringParameterName, "Third option",
+ "Planning workspace...", "",
+ }
+ for i := 0; i < len(matches); i += 2 {
+ match := matches[i]
+ value := matches[i+1]
+ pty.ExpectMatch(match)
+
+ if value != "" {
+ pty.WriteLine(value)
+ }
+ }
+ })
+
+ t.Run("ImmutableRequiredParameterExists_MutableRequiredParameterAdded", func(t *testing.T) {
+ t.Parallel()
+
+ // Create template and workspace
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+
+ templateParameters := []*proto.RichParameter{
+ {Name: stringParameterName, Type: "string", Mutable: false, Required: true, Options: []*proto.RichParameterOption{
+ {Name: "First option", Description: "This is first option", Value: "1st"},
+ {Name: "Second option", Description: "This is second option", Value: "2nd"},
+ {Name: "Third option", Description: "This is third option", Value: "3rd"},
+ }},
+ }
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(templateParameters))
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+
+ inv, root := clitest.New(t, "create", "my-workspace", "--yes", "--template", template.Name, "--parameter", fmt.Sprintf("%s=%s", stringParameterName, "2nd"))
+ clitest.SetupConfig(t, client, root)
+ err := inv.Run()
+ require.NoError(t, err)
+
+ // Update template: add required, mutable parameter
+ const mutableParameterName = "foobar"
+ updatedTemplateParameters := []*proto.RichParameter{
+ templateParameters[0],
+ {Name: mutableParameterName, Type: "string", Mutable: true, Required: true},
+ }
+
+ updatedVersion := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(updatedTemplateParameters), template.ID)
+ coderdtest.AwaitTemplateVersionJob(t, client, updatedVersion.ID)
+ err = client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
+ ID: updatedVersion.ID,
+ })
+ require.NoError(t, err)
+
+ // Update the workspace
+ inv, root = clitest.New(t, "update", "my-workspace")
+ clitest.SetupConfig(t, client, root)
+ doneChan := make(chan struct{})
+ pty := ptytest.New(t).Attach(inv)
+ go func() {
+ defer close(doneChan)
+ err := inv.Run()
+ assert.NoError(t, err)
+ }()
+
+ matches := []string{
+ mutableParameterName, "hello",
+ "Planning workspace...", "",
+ }
+ for i := 0; i < len(matches); i += 2 {
+ match := matches[i]
+ value := matches[i+1]
+ pty.ExpectMatch(match)
+
+ if value != "" {
+ pty.WriteLine(value)
+ }
+ }
+ <-doneChan
+ })
+
+ t.Run("MutableRequiredParameterExists_ImmutableRequiredParameterAdded", func(t *testing.T) {
+ t.Parallel()
+
+ // Create template and workspace
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+
+ templateParameters := []*proto.RichParameter{
+ {Name: stringParameterName, Type: "string", Mutable: true, Required: true, Options: []*proto.RichParameterOption{
+ {Name: "First option", Description: "This is first option", Value: "1st"},
+ {Name: "Second option", Description: "This is second option", Value: "2nd"},
+ {Name: "Third option", Description: "This is third option", Value: "3rd"},
+ }},
+ }
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(templateParameters))
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+
+ inv, root := clitest.New(t, "create", "my-workspace", "--yes", "--template", template.Name, "--parameter", fmt.Sprintf("%s=%s", stringParameterName, "2nd"))
+ clitest.SetupConfig(t, client, root)
+ err := inv.Run()
+ require.NoError(t, err)
+
+ // Update template: add required, immutable parameter
+ updatedTemplateParameters := []*proto.RichParameter{
+ templateParameters[0],
+ {Name: immutableParameterName, Type: "string", Mutable: false, Required: true, Options: []*proto.RichParameterOption{
+ {Name: "fir", Description: "This is first option for immutable parameter", Value: "I"},
+ {Name: "sec", Description: "This is second option for immutable parameter", Value: "II"},
+ {Name: "thi", Description: "This is third option for immutable parameter", Value: "III"},
+ }},
+ }
+
+ updatedVersion := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(updatedTemplateParameters), template.ID)
+ coderdtest.AwaitTemplateVersionJob(t, client, updatedVersion.ID)
+ err = client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
+ ID: updatedVersion.ID,
+ })
+ require.NoError(t, err)
+
+ // Update the workspace
+ inv, root = clitest.New(t, "update", "my-workspace")
+ clitest.SetupConfig(t, client, root)
+ doneChan := make(chan struct{})
+ pty := ptytest.New(t).Attach(inv)
+ go func() {
+ defer close(doneChan)
+ err := inv.Run()
+ assert.NoError(t, err)
+ }()
+
+ matches := []string{
+ immutableParameterName, "thi",
+ "Planning workspace...", "",
+ }
+ for i := 0; i < len(matches); i += 2 {
+ match := matches[i]
+ value := matches[i+1]
+ pty.ExpectMatch(match)
+
+ if value != "" {
+ pty.WriteLine(value)
+ }
}
<-doneChan
})
diff --git a/cli/usercreate.go b/cli/usercreate.go
index b38bbb2d6401f..768e87d826783 100644
--- a/cli/usercreate.go
+++ b/cli/usercreate.go
@@ -2,14 +2,15 @@ package cli
import (
"fmt"
+ "strings"
"github.com/go-playground/validator/v10"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
)
func (r *RootCmd) userCreate() *clibase.Cmd {
@@ -18,6 +19,7 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
username string
password string
disableLogin bool
+ loginType string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
@@ -54,7 +56,18 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
return err
}
}
- if password == "" && !disableLogin {
+ userLoginType := codersdk.LoginTypePassword
+ if disableLogin && loginType != "" {
+ return xerrors.New("You cannot specify both --disable-login and --login-type")
+ }
+ if disableLogin {
+ userLoginType = codersdk.LoginTypeNone
+ } else if loginType != "" {
+ userLoginType = codersdk.LoginType(loginType)
+ }
+
+ if password == "" && userLoginType == codersdk.LoginTypePassword {
+ // Generate a random password
password, err = cryptorand.StringCharset(cryptorand.Human, 20)
if err != nil {
return err
@@ -66,14 +79,22 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
Username: username,
Password: password,
OrganizationID: organization.ID,
- DisableLogin: disableLogin,
+ UserLoginType: userLoginType,
})
if err != nil {
return err
}
- authenticationMethod := `Your password is: ` + cliui.DefaultStyles.Field.Render(password)
- if disableLogin {
+
+ authenticationMethod := ""
+ switch codersdk.LoginType(strings.ToLower(string(userLoginType))) {
+ case codersdk.LoginTypePassword:
+ authenticationMethod = `Your password is: ` + cliui.DefaultStyles.Field.Render(password)
+ case codersdk.LoginTypeNone:
authenticationMethod = "Login has been disabled for this user. Contact your administrator to authenticate."
+ case codersdk.LoginTypeGithub:
+ authenticationMethod = `Login is authenticated through GitHub.`
+ case codersdk.LoginTypeOIDC:
+ authenticationMethod = `Login is authenticated through the configured OIDC provider.`
}
_, _ = fmt.Fprintln(inv.Stderr, `A new user has been created!
@@ -111,11 +132,22 @@ Create a workspace `+cliui.DefaultStyles.Code.Render("coder create")+`!`)
Value: clibase.StringOf(&password),
},
{
- Flag: "disable-login",
- Description: "Disabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. " +
+ Flag: "disable-login",
+ Hidden: true,
+ Description: "Deprecated: Use '--login-type=none'. \nDisabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. " +
"Be careful when using this flag as it can lock the user out of their account.",
Value: clibase.BoolOf(&disableLogin),
},
+ {
+ Flag: "login-type",
+ Description: fmt.Sprintf("Optionally specify the login type for the user. Valid values are: %s. "+
+ "Using 'none' prevents the user from authenticating and requires an API key/token to be generated by an admin.",
+ strings.Join([]string{
+ string(codersdk.LoginTypePassword), string(codersdk.LoginTypeNone), string(codersdk.LoginTypeGithub), string(codersdk.LoginTypeOIDC),
+ }, ", ",
+ )),
+ Value: clibase.StringOf(&loginType),
+ },
}
return cmd
}
diff --git a/cli/usercreate_test.go b/cli/usercreate_test.go
index 01e2137a9e53b..5726cc84d25b5 100644
--- a/cli/usercreate_test.go
+++ b/cli/usercreate_test.go
@@ -5,9 +5,9 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestUserCreate(t *testing.T) {
diff --git a/cli/userlist.go b/cli/userlist.go
index 0f05578a9fe61..ce50a12849fa3 100644
--- a/cli/userlist.go
+++ b/cli/userlist.go
@@ -8,9 +8,9 @@ import (
"github.com/jedib0t/go-pretty/v6/table"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) userList() *clibase.Cmd {
diff --git a/cli/userlist_test.go b/cli/userlist_test.go
index 71c58f38147a7..d6c80d0b7c95f 100644
--- a/cli/userlist_test.go
+++ b/cli/userlist_test.go
@@ -9,10 +9,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestUserList(t *testing.T) {
diff --git a/cli/users.go b/cli/users.go
index 76615d05d7f04..92d79635cf1ba 100644
--- a/cli/users.go
+++ b/cli/users.go
@@ -1,8 +1,8 @@
package cli
import (
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) users() *clibase.Cmd {
diff --git a/cli/userstatus.go b/cli/userstatus.go
index 6a2ada1a7cd19..ac3bbaa0929a6 100644
--- a/cli/userstatus.go
+++ b/cli/userstatus.go
@@ -6,9 +6,9 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
// createUserStatusCommand sets a user status.
diff --git a/cli/userstatus_test.go b/cli/userstatus_test.go
index 348559e10de5d..b288a483e0117 100644
--- a/cli/userstatus_test.go
+++ b/cli/userstatus_test.go
@@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
)
func TestUserStatus(t *testing.T) {
diff --git a/cli/util.go b/cli/util.go
index 777335d0a7d80..e0fe340e45ca2 100644
--- a/cli/util.go
+++ b/cli/util.go
@@ -8,8 +8,8 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/util/tz"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/util/tz"
)
var (
diff --git a/cli/version.go b/cli/version.go
index fb33749f004f9..84e45fb74fe22 100644
--- a/cli/version.go
+++ b/cli/version.go
@@ -5,9 +5,9 @@ import (
"strings"
"time"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
)
// versionInfo wraps the stuff we get from buildinfo so that it's
diff --git a/cli/version_test.go b/cli/version_test.go
index 4267e46f9ad69..20068d29bf124 100644
--- a/cli/version_test.go
+++ b/cli/version_test.go
@@ -10,8 +10,8 @@ import (
"github.com/muesli/termenv"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/testutil"
)
// We need to override the global color profile to test escape codes.
diff --git a/cli/vscodessh.go b/cli/vscodessh.go
index 136a0d727c17a..7e856df96983b 100644
--- a/cli/vscodessh.go
+++ b/cli/vscodessh.go
@@ -20,8 +20,8 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/codersdk"
)
// vscodeSSH is used by the Coder VS Code extension to establish
@@ -86,7 +86,7 @@ func (r *RootCmd) vscodeSSH() *clibase.Cmd {
client.SetSessionToken(string(sessionToken))
// This adds custom headers to the request!
- err = r.setClient(client, serverURL)
+ err = r.setClient(ctx, client, serverURL)
if err != nil {
return xerrors.Errorf("set client: %w", err)
}
diff --git a/cli/vscodessh_test.go b/cli/vscodessh_test.go
index a134903005c4a..2c1afd6135587 100644
--- a/cli/vscodessh_test.go
+++ b/cli/vscodessh_test.go
@@ -11,13 +11,13 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
// TestVSCodeSSH ensures the agent connects properly with SSH
diff --git a/cmd/cliui/main.go b/cmd/cliui/main.go
index 60e8f6536f7a8..a3badedee9f01 100644
--- a/cmd/cliui/main.go
+++ b/cmd/cliui/main.go
@@ -15,10 +15,10 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
)
func main() {
diff --git a/cmd/coder/main.go b/cmd/coder/main.go
index 7ca0f63d0e70a..5d1cea2f8097d 100644
--- a/cmd/coder/main.go
+++ b/cmd/coder/main.go
@@ -3,7 +3,7 @@ package main
import (
_ "time/tzdata"
- "github.com/coder/coder/cli"
+ "github.com/coder/coder/v2/cli"
)
func main() {
diff --git a/coderd/activitybump.go b/coderd/activitybump.go
index 972d59a31f93b..6abc73ebdc0e4 100644
--- a/coderd/activitybump.go
+++ b/coderd/activitybump.go
@@ -10,7 +10,7 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
)
// activityBumpWorkspace automatically bumps the workspace's auto-off timer
diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go
index b9c757e98986e..8ce018a4e9a20 100644
--- a/coderd/activitybump_test.go
+++ b/coderd/activitybump_test.go
@@ -9,16 +9,15 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
func TestWorkspaceActivityBump(t *testing.T) {
@@ -60,25 +59,9 @@ func TestWorkspaceActivityBump(t *testing.T) {
ttlMillis := int64(ttl / time.Millisecond)
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Resources: []*proto.Resource{{
- Name: "example",
- Type: "aws_instance",
- Agents: []*proto.Agent{{
- Id: uuid.NewString(),
- Name: "agent",
- Auth: &proto.Agent_Token{
- Token: agentToken,
- },
- }},
- }},
- },
- },
- }},
+ Parse: echo.ParseComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ProvisionApplyWithAgent(agentToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 2c115a30c3261..58624b22a908f 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -787,12 +787,12 @@ const docTemplate = `{
"tags": [
"Enterprise"
],
- "summary": "Get group by name",
- "operationId": "get-group-by-name",
+ "summary": "Get group by ID",
+ "operationId": "get-group-by-id",
"parameters": [
{
"type": "string",
- "description": "Group name",
+ "description": "Group id",
"name": "group",
"in": "path",
"required": true
@@ -845,6 +845,9 @@ const docTemplate = `{
"CoderSessionToken": []
}
],
+ "consumes": [
+ "application/json"
+ ],
"produces": [
"application/json"
],
@@ -860,6 +863,15 @@ const docTemplate = `{
"name": "group",
"in": "path",
"required": true
+ },
+ {
+ "description": "Patch group request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.PatchGroupRequest"
+ }
}
],
"responses": {
@@ -1012,6 +1024,31 @@ const docTemplate = `{
}
}
},
+ "/licenses/refresh-entitlements": {
+ "post": {
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Organizations"
+ ],
+ "summary": "Update license entitlements",
+ "operationId": "update-license-entitlements",
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ }
+ }
+ }
+ },
"/licenses/{id}": {
"delete": {
"security": [
@@ -5479,6 +5516,42 @@ const docTemplate = `{
}
}
},
+ "/workspaceproxies/me/app-stats": {
+ "post": {
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "tags": [
+ "Enterprise"
+ ],
+ "summary": "Report workspace app stats",
+ "operationId": "report-workspace-app-stats",
+ "parameters": [
+ {
+ "description": "Report app stats request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/wsproxysdk.ReportAppStatsRequest"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
"/workspaceproxies/me/coordinate": {
"get": {
"security": [
@@ -6009,7 +6082,7 @@ const docTemplate = `{
}
}
},
- "/workspaces/{workspace}/extend": {
+ "/workspaces/{workspace}/dormant": {
"put": {
"security": [
{
@@ -6025,8 +6098,8 @@ const docTemplate = `{
"tags": [
"Workspaces"
],
- "summary": "Extend workspace deadline by ID",
- "operationId": "extend-workspace-deadline-by-id",
+ "summary": "Update workspace dormancy status by id.",
+ "operationId": "update-workspace-dormancy-status-by-id",
"parameters": [
{
"type": "string",
@@ -6037,12 +6110,12 @@ const docTemplate = `{
"required": true
},
{
- "description": "Extend deadline update request",
+ "description": "Make a workspace dormant or active",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.PutExtendWorkspaceRequest"
+ "$ref": "#/definitions/codersdk.UpdateWorkspaceDormancy"
}
}
],
@@ -6050,13 +6123,13 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.Workspace"
}
}
}
}
},
- "/workspaces/{workspace}/lock": {
+ "/workspaces/{workspace}/extend": {
"put": {
"security": [
{
@@ -6072,8 +6145,8 @@ const docTemplate = `{
"tags": [
"Workspaces"
],
- "summary": "Update workspace lock by id.",
- "operationId": "update-workspace-lock-by-id",
+ "summary": "Extend workspace deadline by ID",
+ "operationId": "extend-workspace-deadline-by-id",
"parameters": [
{
"type": "string",
@@ -6084,12 +6157,12 @@ const docTemplate = `{
"required": true
},
{
- "description": "Lock or unlock a workspace",
+ "description": "Extend deadline update request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateWorkspaceLock"
+ "$ref": "#/definitions/codersdk.PutExtendWorkspaceRequest"
}
}
],
@@ -6343,6 +6416,9 @@ const docTemplate = `{
"$ref": "#/definitions/codersdk.WorkspaceApp"
}
},
+ "derp_force_websockets": {
+ "type": "boolean"
+ },
"derpmap": {
"$ref": "#/definitions/tailcfg.DERPMap"
},
@@ -6447,8 +6523,11 @@ const docTemplate = `{
"expanded_directory": {
"type": "string"
},
- "subsystem": {
- "$ref": "#/definitions/codersdk.AgentSubsystem"
+ "subsystems": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.AgentSubsystem"
+ }
},
"version": {
"type": "string"
@@ -6624,6 +6703,9 @@ const docTemplate = `{
}
}
},
+ "clibase.Regexp": {
+ "type": "object"
+ },
"clibase.Struct-array_codersdk_GitAuthConfig": {
"type": "object",
"properties": {
@@ -6900,10 +6982,14 @@ const docTemplate = `{
"codersdk.AgentSubsystem": {
"type": "string",
"enum": [
- "envbox"
+ "envbox",
+ "envbuilder",
+ "exectrace"
],
"x-enum-varnames": [
- "AgentSubsystemEnvbox"
+ "AgentSubsystemEnvbox",
+ "AgentSubsystemEnvbuilder",
+ "AgentSubsystemExectrace"
]
},
"codersdk.AppHostResponse": {
@@ -7308,6 +7394,10 @@ const docTemplate = `{
"description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.",
"type": "integer"
},
+ "delete_ttl_ms": {
+ "description": "TimeTilDormantAutoDeleteMillis allows optionally specifying the max lifetime before Coder\npermanently deletes dormant workspaces created from this template.",
+ "type": "integer"
+ },
"description": {
"description": "Description is a description of what the template contains. It must be\nless than 128 bytes.",
"type": "string"
@@ -7320,6 +7410,10 @@ const docTemplate = `{
"description": "DisplayName is the displayed name of the template.",
"type": "string"
},
+ "dormant_ttl_ms": {
+ "description": "TimeTilDormantMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.",
+ "type": "integer"
+ },
"failure_ttl_ms": {
"description": "FailureTTLMillis allows optionally specifying the max lifetime before Coder\nstops all resources for failed workspaces created from this template.",
"type": "integer"
@@ -7328,14 +7422,6 @@ const docTemplate = `{
"description": "Icon is a relative path or external URL that specifies\nan icon to be displayed in the dashboard.",
"type": "string"
},
- "inactivity_ttl_ms": {
- "description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.",
- "type": "integer"
- },
- "locked_ttl_ms": {
- "description": "LockedTTLMillis allows optionally specifying the max lifetime before Coder\npermanently deletes locked workspaces created from this template.",
- "type": "integer"
- },
"max_ttl_ms": {
"description": "TODO(@dean): remove max_ttl once restart_requirement is matured",
"type": "integer"
@@ -7526,13 +7612,21 @@ const docTemplate = `{
],
"properties": {
"disable_login": {
- "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.",
+ "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.\nDeprecated: Set UserLoginType=LoginTypeDisabled instead.",
"type": "boolean"
},
"email": {
"type": "string",
"format": "email"
},
+ "login_type": {
+ "description": "UserLoginType defaults to LoginTypePassword.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/codersdk.LoginType"
+ }
+ ]
+ },
"organization_id": {
"type": "string",
"format": "uuid"
@@ -7690,6 +7784,9 @@ const docTemplate = `{
"block_direct": {
"type": "boolean"
},
+ "force_websockets": {
+ "type": "boolean"
+ },
"path": {
"type": "string"
},
@@ -8002,6 +8099,10 @@ const docTemplate = `{
"has_license": {
"type": "boolean"
},
+ "refreshed_at": {
+ "type": "string",
+ "format": "date-time"
+ },
"require_telemetry": {
"type": "boolean"
},
@@ -8024,7 +8125,8 @@ const docTemplate = `{
"tailnet_pg_coordinator",
"single_tailnet",
"template_restart_requirement",
- "deployment_health_page"
+ "deployment_health_page",
+ "workspaces_batch_actions"
],
"x-enum-varnames": [
"ExperimentMoons",
@@ -8032,7 +8134,8 @@ const docTemplate = `{
"ExperimentTailnetPGCoordinator",
"ExperimentSingleTailnet",
"ExperimentTemplateRestartRequirement",
- "ExperimentDeploymentHealthPage"
+ "ExperimentDeploymentHealthPage",
+ "ExperimentWorkspacesBatchActions"
]
},
"codersdk.Feature": {
@@ -8272,9 +8375,23 @@ const docTemplate = `{
},
"quota_allowance": {
"type": "integer"
+ },
+ "source": {
+ "$ref": "#/definitions/codersdk.GroupSource"
}
}
},
+ "codersdk.GroupSource": {
+ "type": "string",
+ "enum": [
+ "user",
+ "oidc"
+ ],
+ "x-enum-varnames": [
+ "GroupSourceUser",
+ "GroupSourceOIDC"
+ ]
+ },
"codersdk.Healthcheck": {
"type": "object",
"properties": {
@@ -8329,11 +8446,9 @@ const docTemplate = `{
"codersdk.JobErrorCode": {
"type": "string",
"enum": [
- "MISSING_TEMPLATE_PARAMETER",
"REQUIRED_TEMPLATE_VARIABLES"
],
"x-enum-varnames": [
- "MissingTemplateParameter",
"RequiredTemplateVariables"
]
},
@@ -8423,6 +8538,7 @@ const docTemplate = `{
"codersdk.LoginType": {
"type": "string",
"enum": [
+ "",
"password",
"github",
"oidc",
@@ -8430,6 +8546,7 @@ const docTemplate = `{
"none"
],
"x-enum-varnames": [
+ "LoginTypeUnknown",
"LoginTypePassword",
"LoginTypeGithub",
"LoginTypeOIDC",
@@ -8566,9 +8683,16 @@ const docTemplate = `{
"auth_url_params": {
"type": "object"
},
+ "client_cert_file": {
+ "type": "string"
+ },
"client_id": {
"type": "string"
},
+ "client_key_file": {
+ "description": "ClientKeyFile \u0026 ClientCertFile are used in place of ClientSecret for PKI auth.",
+ "type": "string"
+ },
"client_secret": {
"type": "string"
},
@@ -8581,9 +8705,15 @@ const docTemplate = `{
"email_field": {
"type": "string"
},
+ "group_auto_create": {
+ "type": "boolean"
+ },
"group_mapping": {
"type": "object"
},
+ "group_regex_filter": {
+ "$ref": "#/definitions/clibase.Regexp"
+ },
"groups_field": {
"type": "string"
},
@@ -8678,6 +8808,35 @@ const docTemplate = `{
}
}
},
+ "codersdk.PatchGroupRequest": {
+ "type": "object",
+ "properties": {
+ "add_users": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "avatar_url": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "quota_allowance": {
+ "type": "integer"
+ },
+ "remove_users": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
"codersdk.PatchTemplateVersionRequest": {
"type": "object",
"properties": {
@@ -8753,6 +8912,9 @@ const docTemplate = `{
"daemon_poll_jitter": {
"type": "integer"
},
+ "daemon_psk": {
+ "type": "string"
+ },
"daemons": {
"type": "integer"
},
@@ -8820,7 +8982,6 @@ const docTemplate = `{
},
"error_code": {
"enum": [
- "MISSING_TEMPLATE_PARAMETER",
"REQUIRED_TEMPLATE_VARIABLES"
],
"allOf": [
@@ -9152,7 +9313,9 @@ const docTemplate = `{
"api_key",
"group",
"license",
- "convert_login"
+ "convert_login",
+ "workspace_proxy",
+ "organization"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -9164,7 +9327,9 @@ const docTemplate = `{
"ResourceTypeAPIKey",
"ResourceTypeGroup",
"ResourceTypeLicense",
- "ResourceTypeConvertLogin"
+ "ResourceTypeConvertLogin",
+ "ResourceTypeWorkspaceProxy",
+ "ResourceTypeOrganization"
]
},
"codersdk.Response": {
@@ -9375,7 +9540,7 @@ const docTemplate = `{
"type": "string"
},
"failure_ttl_ms": {
- "description": "FailureTTLMillis, InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.",
+ "description": "FailureTTLMillis, TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.",
"type": "integer"
},
"icon": {
@@ -9385,12 +9550,6 @@ const docTemplate = `{
"type": "string",
"format": "uuid"
},
- "inactivity_ttl_ms": {
- "type": "integer"
- },
- "locked_ttl_ms": {
- "type": "integer"
- },
"max_ttl_ms": {
"description": "TODO(@dean): remove max_ttl once restart_requirement is matured",
"type": "integer"
@@ -9416,6 +9575,12 @@ const docTemplate = `{
}
]
},
+ "time_til_dormant_autodelete_ms": {
+ "type": "integer"
+ },
+ "time_til_dormant_ms": {
+ "type": "integer"
+ },
"updated_at": {
"type": "string",
"format": "date-time"
@@ -9460,10 +9625,12 @@ const docTemplate = `{
"codersdk.TemplateAppsType": {
"type": "string",
"enum": [
- "builtin"
+ "builtin",
+ "app"
],
"x-enum-varnames": [
- "TemplateAppsTypeBuiltin"
+ "TemplateAppsTypeBuiltin",
+ "TemplateAppsTypeApp"
]
},
"codersdk.TemplateBuildTimeStats": {
@@ -9582,6 +9749,9 @@ const docTemplate = `{
"codersdk.TemplateParameterUsage": {
"type": "object",
"properties": {
+ "description": {
+ "type": "string"
+ },
"display_name": {
"type": "string"
},
@@ -9601,6 +9771,9 @@ const docTemplate = `{
"format": "uuid"
}
},
+ "type": {
+ "type": "string"
+ },
"values": {
"type": "array",
"items": {
@@ -10081,10 +10254,10 @@ const docTemplate = `{
}
}
},
- "codersdk.UpdateWorkspaceLock": {
+ "codersdk.UpdateWorkspaceDormancy": {
"type": "object",
"properties": {
- "lock": {
+ "dormant": {
"type": "boolean"
}
}
@@ -10337,7 +10510,12 @@ const docTemplate = `{
"format": "date-time"
},
"deleting_at": {
- "description": "DeletingAt indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil.\nWorkspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive.",
+ "description": "DeletingAt indicates the time at which the workspace will be permanently deleted.\nA workspace is eligible for deletion if it is dormant (a non-nil dormant_at value)\nand a value has been specified for time_til_dormant_autodelete on its template.",
+ "type": "string",
+ "format": "date-time"
+ },
+ "dormant_at": {
+ "description": "DormantAt being non-nil indicates a workspace that is dormant.\nA dormant workspace is no longer accessible must be activated.\nIt is subject to deletion if it breaches\nthe duration of the time_til_ field on its template.",
"type": "string",
"format": "date-time"
},
@@ -10360,11 +10538,6 @@ const docTemplate = `{
"latest_build": {
"$ref": "#/definitions/codersdk.WorkspaceBuild"
},
- "locked_at": {
- "description": "LockedAt being non-nil indicates a workspace that has been locked.\nA locked workspace is no longer accessible by a user and must be\nunlocked by an admin. It is subject to deletion if it breaches\nthe duration of the locked_ttl field on its template.",
- "type": "string",
- "format": "date-time"
- },
"name": {
"type": "string"
},
@@ -10382,6 +10555,10 @@ const docTemplate = `{
"owner_name": {
"type": "string"
},
+ "template_active_version_id": {
+ "type": "string",
+ "format": "uuid"
+ },
"template_allow_user_cancel_workspace_jobs": {
"type": "boolean"
},
@@ -10522,8 +10699,11 @@ const docTemplate = `{
"status": {
"$ref": "#/definitions/codersdk.WorkspaceAgentStatus"
},
- "subsystem": {
- "$ref": "#/definitions/codersdk.AgentSubsystem"
+ "subsystems": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.AgentSubsystem"
+ }
},
"troubleshooting_url": {
"type": "string"
@@ -10540,6 +10720,9 @@ const docTemplate = `{
"codersdk.WorkspaceAgentConnectionInfo": {
"type": "object",
"properties": {
+ "derp_force_websockets": {
+ "type": "boolean"
+ },
"derp_map": {
"$ref": "#/definitions/tailcfg.DERPMap"
},
@@ -11499,11 +11682,31 @@ const docTemplate = `{
}
}
},
+ "tailcfg.DERPHomeParams": {
+ "type": "object",
+ "properties": {
+ "regionScore": {
+ "description": "RegionScore scales latencies of DERP regions by a given scaling\nfactor when determining which region to use as the home\n(\"preferred\") DERP. Scores in the range (0, 1) will cause this\nregion to be proportionally more preferred, and scores in the range\n(1, ∞) will penalize a region.\n\nIf a region is not present in this map, it is treated as having a\nscore of 1.0.\n\nScores should not be 0 or negative; such scores will be ignored.\n\nA nil map means no change from the previous value (if any); an empty\nnon-nil map can be sent to reset all scores back to 1.0.",
+ "type": "object",
+ "additionalProperties": {
+ "type": "number"
+ }
+ }
+ }
+ },
"tailcfg.DERPMap": {
"type": "object",
"properties": {
+ "homeParams": {
+ "description": "HomeParams, if non-nil, is a change in home parameters.\n\nThe rest of the DEPRMap fields, if zero, means unchanged.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/tailcfg.DERPHomeParams"
+ }
+ ]
+ },
"omitDefaultRegions": {
- "description": "OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those\nspecified in this DERPMap. If there are none set outside of the defaults, this is a noop.",
+ "description": "OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those\nspecified in this DERPMap. If there are none set outside of the defaults, this is a noop.\n\nThis field is only meaningful if the Regions map is non-nil (indicating a change).",
"type": "boolean"
},
"regions": {
@@ -11518,6 +11721,10 @@ const docTemplate = `{
"tailcfg.DERPNode": {
"type": "object",
"properties": {
+ "canPort80": {
+ "description": "CanPort80 specifies whether this DERP node is accessible over HTTP\non port 80 specifically. This is used for captive portal checks.",
+ "type": "boolean"
+ },
"certName": {
"description": "CertName optionally specifies the expected TLS cert common\nname. If empty, HostName is used. If CertName is non-empty,\nHostName is only used for the TCP dial (if IPv4/IPv6 are\nnot present) + TLS ClientHello.",
"type": "string"
@@ -11670,6 +11877,39 @@ const docTemplate = `{
}
}
},
+ "workspaceapps.StatsReport": {
+ "type": "object",
+ "properties": {
+ "access_method": {
+ "$ref": "#/definitions/workspaceapps.AccessMethod"
+ },
+ "agent_id": {
+ "type": "string"
+ },
+ "requests": {
+ "type": "integer"
+ },
+ "session_ended_at": {
+ "description": "Updated periodically while app is in use active and when the last connection is closed.",
+ "type": "string"
+ },
+ "session_id": {
+ "type": "string"
+ },
+ "session_started_at": {
+ "type": "string"
+ },
+ "slug_or_port": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "workspace_id": {
+ "type": "string"
+ }
+ }
+ },
"wsproxysdk.AgentIsLegacyResponse": {
"type": "object",
"properties": {
@@ -11746,6 +11986,12 @@ const docTemplate = `{
"app_security_key": {
"type": "string"
},
+ "derp_force_websockets": {
+ "type": "boolean"
+ },
+ "derp_map": {
+ "$ref": "#/definitions/tailcfg.DERPMap"
+ },
"derp_mesh_key": {
"type": "string"
},
@@ -11760,6 +12006,17 @@ const docTemplate = `{
}
}
}
+ },
+ "wsproxysdk.ReportAppStatsRequest": {
+ "type": "object",
+ "properties": {
+ "stats": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/workspaceapps.StatsReport"
+ }
+ }
+ }
}
},
"securityDefinitions": {
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 7f7d12a51a53a..7342e5140598e 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -675,12 +675,12 @@
],
"produces": ["application/json"],
"tags": ["Enterprise"],
- "summary": "Get group by name",
- "operationId": "get-group-by-name",
+ "summary": "Get group by ID",
+ "operationId": "get-group-by-id",
"parameters": [
{
"type": "string",
- "description": "Group name",
+ "description": "Group id",
"name": "group",
"in": "path",
"required": true
@@ -729,6 +729,7 @@
"CoderSessionToken": []
}
],
+ "consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Enterprise"],
"summary": "Update group by name",
@@ -740,6 +741,15 @@
"name": "group",
"in": "path",
"required": true
+ },
+ {
+ "description": "Patch group request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/codersdk.PatchGroupRequest"
+ }
}
],
"responses": {
@@ -870,6 +880,27 @@
}
}
},
+ "/licenses/refresh-entitlements": {
+ "post": {
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "produces": ["application/json"],
+ "tags": ["Organizations"],
+ "summary": "Update license entitlements",
+ "operationId": "update-license-entitlements",
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "$ref": "#/definitions/codersdk.Response"
+ }
+ }
+ }
+ }
+ },
"/licenses/{id}": {
"delete": {
"security": [
@@ -4831,6 +4862,38 @@
}
}
},
+ "/workspaceproxies/me/app-stats": {
+ "post": {
+ "security": [
+ {
+ "CoderSessionToken": []
+ }
+ ],
+ "consumes": ["application/json"],
+ "tags": ["Enterprise"],
+ "summary": "Report workspace app stats",
+ "operationId": "report-workspace-app-stats",
+ "parameters": [
+ {
+ "description": "Report app stats request",
+ "name": "request",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/wsproxysdk.ReportAppStatsRequest"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ }
+ },
+ "x-apidocgen": {
+ "skip": true
+ }
+ }
+ },
"/workspaceproxies/me/coordinate": {
"get": {
"security": [
@@ -5303,7 +5366,7 @@
}
}
},
- "/workspaces/{workspace}/extend": {
+ "/workspaces/{workspace}/dormant": {
"put": {
"security": [
{
@@ -5313,8 +5376,8 @@
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Workspaces"],
- "summary": "Extend workspace deadline by ID",
- "operationId": "extend-workspace-deadline-by-id",
+ "summary": "Update workspace dormancy status by id.",
+ "operationId": "update-workspace-dormancy-status-by-id",
"parameters": [
{
"type": "string",
@@ -5325,12 +5388,12 @@
"required": true
},
{
- "description": "Extend deadline update request",
+ "description": "Make a workspace dormant or active",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.PutExtendWorkspaceRequest"
+ "$ref": "#/definitions/codersdk.UpdateWorkspaceDormancy"
}
}
],
@@ -5338,13 +5401,13 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.Response"
+ "$ref": "#/definitions/codersdk.Workspace"
}
}
}
}
},
- "/workspaces/{workspace}/lock": {
+ "/workspaces/{workspace}/extend": {
"put": {
"security": [
{
@@ -5354,8 +5417,8 @@
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": ["Workspaces"],
- "summary": "Update workspace lock by id.",
- "operationId": "update-workspace-lock-by-id",
+ "summary": "Extend workspace deadline by ID",
+ "operationId": "extend-workspace-deadline-by-id",
"parameters": [
{
"type": "string",
@@ -5366,12 +5429,12 @@
"required": true
},
{
- "description": "Lock or unlock a workspace",
+ "description": "Extend deadline update request",
"name": "request",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/codersdk.UpdateWorkspaceLock"
+ "$ref": "#/definitions/codersdk.PutExtendWorkspaceRequest"
}
}
],
@@ -5593,6 +5656,9 @@
"$ref": "#/definitions/codersdk.WorkspaceApp"
}
},
+ "derp_force_websockets": {
+ "type": "boolean"
+ },
"derpmap": {
"$ref": "#/definitions/tailcfg.DERPMap"
},
@@ -5697,8 +5763,11 @@
"expanded_directory": {
"type": "string"
},
- "subsystem": {
- "$ref": "#/definitions/codersdk.AgentSubsystem"
+ "subsystems": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.AgentSubsystem"
+ }
},
"version": {
"type": "string"
@@ -5874,6 +5943,9 @@
}
}
},
+ "clibase.Regexp": {
+ "type": "object"
+ },
"clibase.Struct-array_codersdk_GitAuthConfig": {
"type": "object",
"properties": {
@@ -6127,8 +6199,12 @@
},
"codersdk.AgentSubsystem": {
"type": "string",
- "enum": ["envbox"],
- "x-enum-varnames": ["AgentSubsystemEnvbox"]
+ "enum": ["envbox", "envbuilder", "exectrace"],
+ "x-enum-varnames": [
+ "AgentSubsystemEnvbox",
+ "AgentSubsystemEnvbuilder",
+ "AgentSubsystemExectrace"
+ ]
},
"codersdk.AppHostResponse": {
"type": "object",
@@ -6511,6 +6587,10 @@
"description": "DefaultTTLMillis allows optionally specifying the default TTL\nfor all workspaces created from this template.",
"type": "integer"
},
+ "delete_ttl_ms": {
+ "description": "TimeTilDormantAutoDeleteMillis allows optionally specifying the max lifetime before Coder\npermanently deletes dormant workspaces created from this template.",
+ "type": "integer"
+ },
"description": {
"description": "Description is a description of what the template contains. It must be\nless than 128 bytes.",
"type": "string"
@@ -6523,6 +6603,10 @@
"description": "DisplayName is the displayed name of the template.",
"type": "string"
},
+ "dormant_ttl_ms": {
+ "description": "TimeTilDormantMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.",
+ "type": "integer"
+ },
"failure_ttl_ms": {
"description": "FailureTTLMillis allows optionally specifying the max lifetime before Coder\nstops all resources for failed workspaces created from this template.",
"type": "integer"
@@ -6531,14 +6615,6 @@
"description": "Icon is a relative path or external URL that specifies\nan icon to be displayed in the dashboard.",
"type": "string"
},
- "inactivity_ttl_ms": {
- "description": "InactivityTTLMillis allows optionally specifying the max lifetime before Coder\nlocks inactive workspaces created from this template.",
- "type": "integer"
- },
- "locked_ttl_ms": {
- "description": "LockedTTLMillis allows optionally specifying the max lifetime before Coder\npermanently deletes locked workspaces created from this template.",
- "type": "integer"
- },
"max_ttl_ms": {
"description": "TODO(@dean): remove max_ttl once restart_requirement is matured",
"type": "integer"
@@ -6705,13 +6781,21 @@
"required": ["email", "username"],
"properties": {
"disable_login": {
- "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.",
+ "description": "DisableLogin sets the user's login type to 'none'. This prevents the user\nfrom being able to use a password or any other authentication method to login.\nDeprecated: Set UserLoginType=LoginTypeDisabled instead.",
"type": "boolean"
},
"email": {
"type": "string",
"format": "email"
},
+ "login_type": {
+ "description": "UserLoginType defaults to LoginTypePassword.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/codersdk.LoginType"
+ }
+ ]
+ },
"organization_id": {
"type": "string",
"format": "uuid"
@@ -6855,6 +6939,9 @@
"block_direct": {
"type": "boolean"
},
+ "force_websockets": {
+ "type": "boolean"
+ },
"path": {
"type": "string"
},
@@ -7163,6 +7250,10 @@
"has_license": {
"type": "boolean"
},
+ "refreshed_at": {
+ "type": "string",
+ "format": "date-time"
+ },
"require_telemetry": {
"type": "boolean"
},
@@ -7185,7 +7276,8 @@
"tailnet_pg_coordinator",
"single_tailnet",
"template_restart_requirement",
- "deployment_health_page"
+ "deployment_health_page",
+ "workspaces_batch_actions"
],
"x-enum-varnames": [
"ExperimentMoons",
@@ -7193,7 +7285,8 @@
"ExperimentTailnetPGCoordinator",
"ExperimentSingleTailnet",
"ExperimentTemplateRestartRequirement",
- "ExperimentDeploymentHealthPage"
+ "ExperimentDeploymentHealthPage",
+ "ExperimentWorkspacesBatchActions"
]
},
"codersdk.Feature": {
@@ -7428,9 +7521,17 @@
},
"quota_allowance": {
"type": "integer"
+ },
+ "source": {
+ "$ref": "#/definitions/codersdk.GroupSource"
}
}
},
+ "codersdk.GroupSource": {
+ "type": "string",
+ "enum": ["user", "oidc"],
+ "x-enum-varnames": ["GroupSourceUser", "GroupSourceOIDC"]
+ },
"codersdk.Healthcheck": {
"type": "object",
"properties": {
@@ -7477,11 +7578,8 @@
},
"codersdk.JobErrorCode": {
"type": "string",
- "enum": ["MISSING_TEMPLATE_PARAMETER", "REQUIRED_TEMPLATE_VARIABLES"],
- "x-enum-varnames": [
- "MissingTemplateParameter",
- "RequiredTemplateVariables"
- ]
+ "enum": ["REQUIRED_TEMPLATE_VARIABLES"],
+ "x-enum-varnames": ["RequiredTemplateVariables"]
},
"codersdk.License": {
"type": "object",
@@ -7556,8 +7654,9 @@
},
"codersdk.LoginType": {
"type": "string",
- "enum": ["password", "github", "oidc", "token", "none"],
+ "enum": ["", "password", "github", "oidc", "token", "none"],
"x-enum-varnames": [
+ "LoginTypeUnknown",
"LoginTypePassword",
"LoginTypeGithub",
"LoginTypeOIDC",
@@ -7686,9 +7785,16 @@
"auth_url_params": {
"type": "object"
},
+ "client_cert_file": {
+ "type": "string"
+ },
"client_id": {
"type": "string"
},
+ "client_key_file": {
+ "description": "ClientKeyFile \u0026 ClientCertFile are used in place of ClientSecret for PKI auth.",
+ "type": "string"
+ },
"client_secret": {
"type": "string"
},
@@ -7701,9 +7807,15 @@
"email_field": {
"type": "string"
},
+ "group_auto_create": {
+ "type": "boolean"
+ },
"group_mapping": {
"type": "object"
},
+ "group_regex_filter": {
+ "$ref": "#/definitions/clibase.Regexp"
+ },
"groups_field": {
"type": "string"
},
@@ -7793,6 +7905,35 @@
}
}
},
+ "codersdk.PatchGroupRequest": {
+ "type": "object",
+ "properties": {
+ "add_users": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "avatar_url": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "quota_allowance": {
+ "type": "integer"
+ },
+ "remove_users": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
"codersdk.PatchTemplateVersionRequest": {
"type": "object",
"properties": {
@@ -7863,6 +8004,9 @@
"daemon_poll_jitter": {
"type": "integer"
},
+ "daemon_psk": {
+ "type": "string"
+ },
"daemons": {
"type": "integer"
},
@@ -7929,7 +8073,7 @@
"type": "string"
},
"error_code": {
- "enum": ["MISSING_TEMPLATE_PARAMETER", "REQUIRED_TEMPLATE_VARIABLES"],
+ "enum": ["REQUIRED_TEMPLATE_VARIABLES"],
"allOf": [
{
"$ref": "#/definitions/codersdk.JobErrorCode"
@@ -8238,7 +8382,9 @@
"api_key",
"group",
"license",
- "convert_login"
+ "convert_login",
+ "workspace_proxy",
+ "organization"
],
"x-enum-varnames": [
"ResourceTypeTemplate",
@@ -8250,7 +8396,9 @@
"ResourceTypeAPIKey",
"ResourceTypeGroup",
"ResourceTypeLicense",
- "ResourceTypeConvertLogin"
+ "ResourceTypeConvertLogin",
+ "ResourceTypeWorkspaceProxy",
+ "ResourceTypeOrganization"
]
},
"codersdk.Response": {
@@ -8461,7 +8609,7 @@
"type": "string"
},
"failure_ttl_ms": {
- "description": "FailureTTLMillis, InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.",
+ "description": "FailureTTLMillis, TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their\nvalues are used if your license is entitled to use the advanced\ntemplate scheduling feature.",
"type": "integer"
},
"icon": {
@@ -8471,12 +8619,6 @@
"type": "string",
"format": "uuid"
},
- "inactivity_ttl_ms": {
- "type": "integer"
- },
- "locked_ttl_ms": {
- "type": "integer"
- },
"max_ttl_ms": {
"description": "TODO(@dean): remove max_ttl once restart_requirement is matured",
"type": "integer"
@@ -8500,6 +8642,12 @@
}
]
},
+ "time_til_dormant_autodelete_ms": {
+ "type": "integer"
+ },
+ "time_til_dormant_ms": {
+ "type": "integer"
+ },
"updated_at": {
"type": "string",
"format": "date-time"
@@ -8543,8 +8691,8 @@
},
"codersdk.TemplateAppsType": {
"type": "string",
- "enum": ["builtin"],
- "x-enum-varnames": ["TemplateAppsTypeBuiltin"]
+ "enum": ["builtin", "app"],
+ "x-enum-varnames": ["TemplateAppsTypeBuiltin", "TemplateAppsTypeApp"]
},
"codersdk.TemplateBuildTimeStats": {
"type": "object",
@@ -8662,6 +8810,9 @@
"codersdk.TemplateParameterUsage": {
"type": "object",
"properties": {
+ "description": {
+ "type": "string"
+ },
"display_name": {
"type": "string"
},
@@ -8681,6 +8832,9 @@
"format": "uuid"
}
},
+ "type": {
+ "type": "string"
+ },
"values": {
"type": "array",
"items": {
@@ -9120,10 +9274,10 @@
}
}
},
- "codersdk.UpdateWorkspaceLock": {
+ "codersdk.UpdateWorkspaceDormancy": {
"type": "object",
"properties": {
- "lock": {
+ "dormant": {
"type": "boolean"
}
}
@@ -9358,7 +9512,12 @@
"format": "date-time"
},
"deleting_at": {
- "description": "DeletingAt indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil.\nWorkspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive.",
+ "description": "DeletingAt indicates the time at which the workspace will be permanently deleted.\nA workspace is eligible for deletion if it is dormant (a non-nil dormant_at value)\nand a value has been specified for time_til_dormant_autodelete on its template.",
+ "type": "string",
+ "format": "date-time"
+ },
+ "dormant_at": {
+ "description": "DormantAt being non-nil indicates a workspace that is dormant.\nA dormant workspace is no longer accessible must be activated.\nIt is subject to deletion if it breaches\nthe duration of the time_til_ field on its template.",
"type": "string",
"format": "date-time"
},
@@ -9381,11 +9540,6 @@
"latest_build": {
"$ref": "#/definitions/codersdk.WorkspaceBuild"
},
- "locked_at": {
- "description": "LockedAt being non-nil indicates a workspace that has been locked.\nA locked workspace is no longer accessible by a user and must be\nunlocked by an admin. It is subject to deletion if it breaches\nthe duration of the locked_ttl field on its template.",
- "type": "string",
- "format": "date-time"
- },
"name": {
"type": "string"
},
@@ -9403,6 +9557,10 @@
"owner_name": {
"type": "string"
},
+ "template_active_version_id": {
+ "type": "string",
+ "format": "uuid"
+ },
"template_allow_user_cancel_workspace_jobs": {
"type": "boolean"
},
@@ -9543,8 +9701,11 @@
"status": {
"$ref": "#/definitions/codersdk.WorkspaceAgentStatus"
},
- "subsystem": {
- "$ref": "#/definitions/codersdk.AgentSubsystem"
+ "subsystems": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.AgentSubsystem"
+ }
},
"troubleshooting_url": {
"type": "string"
@@ -9561,6 +9722,9 @@
"codersdk.WorkspaceAgentConnectionInfo": {
"type": "object",
"properties": {
+ "derp_force_websockets": {
+ "type": "boolean"
+ },
"derp_map": {
"$ref": "#/definitions/tailcfg.DERPMap"
},
@@ -10483,11 +10647,31 @@
}
}
},
+ "tailcfg.DERPHomeParams": {
+ "type": "object",
+ "properties": {
+ "regionScore": {
+ "description": "RegionScore scales latencies of DERP regions by a given scaling\nfactor when determining which region to use as the home\n(\"preferred\") DERP. Scores in the range (0, 1) will cause this\nregion to be proportionally more preferred, and scores in the range\n(1, ∞) will penalize a region.\n\nIf a region is not present in this map, it is treated as having a\nscore of 1.0.\n\nScores should not be 0 or negative; such scores will be ignored.\n\nA nil map means no change from the previous value (if any); an empty\nnon-nil map can be sent to reset all scores back to 1.0.",
+ "type": "object",
+ "additionalProperties": {
+ "type": "number"
+ }
+ }
+ }
+ },
"tailcfg.DERPMap": {
"type": "object",
"properties": {
+ "homeParams": {
+ "description": "HomeParams, if non-nil, is a change in home parameters.\n\nThe rest of the DEPRMap fields, if zero, means unchanged.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/tailcfg.DERPHomeParams"
+ }
+ ]
+ },
"omitDefaultRegions": {
- "description": "OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those\nspecified in this DERPMap. If there are none set outside of the defaults, this is a noop.",
+ "description": "OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those\nspecified in this DERPMap. If there are none set outside of the defaults, this is a noop.\n\nThis field is only meaningful if the Regions map is non-nil (indicating a change).",
"type": "boolean"
},
"regions": {
@@ -10502,6 +10686,10 @@
"tailcfg.DERPNode": {
"type": "object",
"properties": {
+ "canPort80": {
+ "description": "CanPort80 specifies whether this DERP node is accessible over HTTP\non port 80 specifically. This is used for captive portal checks.",
+ "type": "boolean"
+ },
"certName": {
"description": "CertName optionally specifies the expected TLS cert common\nname. If empty, HostName is used. If CertName is non-empty,\nHostName is only used for the TCP dial (if IPv4/IPv6 are\nnot present) + TLS ClientHello.",
"type": "string"
@@ -10650,6 +10838,39 @@
}
}
},
+ "workspaceapps.StatsReport": {
+ "type": "object",
+ "properties": {
+ "access_method": {
+ "$ref": "#/definitions/workspaceapps.AccessMethod"
+ },
+ "agent_id": {
+ "type": "string"
+ },
+ "requests": {
+ "type": "integer"
+ },
+ "session_ended_at": {
+ "description": "Updated periodically while app is in use active and when the last connection is closed.",
+ "type": "string"
+ },
+ "session_id": {
+ "type": "string"
+ },
+ "session_started_at": {
+ "type": "string"
+ },
+ "slug_or_port": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "workspace_id": {
+ "type": "string"
+ }
+ }
+ },
"wsproxysdk.AgentIsLegacyResponse": {
"type": "object",
"properties": {
@@ -10726,6 +10947,12 @@
"app_security_key": {
"type": "string"
},
+ "derp_force_websockets": {
+ "type": "boolean"
+ },
+ "derp_map": {
+ "$ref": "#/definitions/tailcfg.DERPMap"
+ },
"derp_mesh_key": {
"type": "string"
},
@@ -10740,6 +10967,17 @@
}
}
}
+ },
+ "wsproxysdk.ReportAppStatsRequest": {
+ "type": "object",
+ "properties": {
+ "stats": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/workspaceapps.StatsReport"
+ }
+ }
+ }
}
},
"securityDefinitions": {
diff --git a/coderd/apikey.go b/coderd/apikey.go
index c3934d076cbdc..ba017819773cf 100644
--- a/coderd/apikey.go
+++ b/coderd/apikey.go
@@ -12,14 +12,14 @@ import (
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/apikey"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/apikey"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/codersdk"
)
// Creates a new token API key that effectively doesn't expire.
diff --git a/coderd/apikey/apikey.go b/coderd/apikey/apikey.go
index 8abba96421914..c21c28dd16967 100644
--- a/coderd/apikey/apikey.go
+++ b/coderd/apikey/apikey.go
@@ -10,9 +10,9 @@ import (
"github.com/sqlc-dev/pqtype"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
)
type CreateParams struct {
diff --git a/coderd/apikey/apikey_test.go b/coderd/apikey/apikey_test.go
index 89fc3a6fade02..eccb0cb81af6e 100644
--- a/coderd/apikey/apikey_test.go
+++ b/coderd/apikey/apikey_test.go
@@ -10,10 +10,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/coderd/apikey"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd/apikey"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
)
func TestGenerate(t *testing.T) {
diff --git a/coderd/apikey_test.go b/coderd/apikey_test.go
index 412f7bebae660..c77d396d3e43e 100644
--- a/coderd/apikey_test.go
+++ b/coderd/apikey_test.go
@@ -10,13 +10,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestTokenCRUD(t *testing.T) {
diff --git a/coderd/apiroot.go b/coderd/apiroot.go
index 6974ca5133e24..a0dee428e3970 100644
--- a/coderd/apiroot.go
+++ b/coderd/apiroot.go
@@ -3,8 +3,8 @@ package coderd
import (
"net/http"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary API root handler
diff --git a/coderd/audit.go b/coderd/audit.go
index 18eb155743c0e..e898e343b1e9f 100644
--- a/coderd/audit.go
+++ b/coderd/audit.go
@@ -15,14 +15,14 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/searchquery"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/searchquery"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Get audit logs
diff --git a/coderd/audit/audit.go b/coderd/audit/audit.go
index d3e83d19e85fb..4d256541d05f6 100644
--- a/coderd/audit/audit.go
+++ b/coderd/audit/audit.go
@@ -4,7 +4,7 @@ import (
"context"
"sync"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
)
type Auditor interface {
diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go
index 858334493cf8d..8cf0a1d0ddaf3 100644
--- a/coderd/audit/diff.go
+++ b/coderd/audit/diff.go
@@ -1,7 +1,7 @@
package audit
import (
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
)
// Auditable is mostly a marker interface. It contains a definitive list of all
diff --git a/coderd/audit/request.go b/coderd/audit/request.go
index aec89ef3d308e..434ff401f3339 100644
--- a/coderd/audit/request.go
+++ b/coderd/audit/request.go
@@ -13,9 +13,9 @@ import (
"github.com/sqlc-dev/pqtype"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/tracing"
)
type RequestParams struct {
diff --git a/coderd/audit_test.go b/coderd/audit_test.go
index cc8698775bd22..8d7ada74b3bef 100644
--- a/coderd/audit_test.go
+++ b/coderd/audit_test.go
@@ -10,10 +10,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
)
func TestAuditLogs(t *testing.T) {
diff --git a/coderd/authorize.go b/coderd/authorize.go
index 229c7e4624655..e8d4274ab89a0 100644
--- a/coderd/authorize.go
+++ b/coderd/authorize.go
@@ -8,10 +8,10 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
// AuthorizeFilter takes a list of objects and returns the filtered list of
diff --git a/coderd/authorize_test.go b/coderd/authorize_test.go
index 55214d10473a7..54462690be516 100644
--- a/coderd/authorize_test.go
+++ b/coderd/authorize_test.go
@@ -7,10 +7,10 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestCheckPermissions(t *testing.T) {
diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go
index f7176ae8cd721..f603d7895531d 100644
--- a/coderd/autobuild/lifecycle_executor.go
+++ b/coderd/autobuild/lifecycle_executor.go
@@ -12,12 +12,12 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/wsbuilder"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/wsbuilder"
+ "github.com/coder/coder/v2/codersdk"
)
// Executor automatically starts or stops workspaces.
@@ -175,35 +175,35 @@ func (e *Executor) runOnce(t time.Time) Stats {
}
}
- // Lock the workspace if it has breached the template's
+ // Transition the workspace to dormant if it has breached the template's
// threshold for inactivity.
if reason == database.BuildReasonAutolock {
- err = tx.UpdateWorkspaceLockedDeletingAt(e.ctx, database.UpdateWorkspaceLockedDeletingAtParams{
+ ws, err = tx.UpdateWorkspaceDormantDeletingAt(e.ctx, database.UpdateWorkspaceDormantDeletingAtParams{
ID: ws.ID,
- LockedAt: sql.NullTime{
+ DormantAt: sql.NullTime{
Time: database.Now(),
Valid: true,
},
})
if err != nil {
- log.Error(e.ctx, "unable to lock workspace",
+ log.Error(e.ctx, "unable to transition workspace to dormant",
slog.F("transition", nextTransition),
slog.Error(err),
)
return nil
}
- log.Info(e.ctx, "locked workspace",
+ log.Info(e.ctx, "dormant workspace",
slog.F("last_used_at", ws.LastUsedAt),
- slog.F("inactivity_ttl", templateSchedule.InactivityTTL),
+ slog.F("time_til_dormant", templateSchedule.TimeTilDormant),
slog.F("since_last_used_at", time.Since(ws.LastUsedAt)),
)
}
if reason == database.BuildReasonAutodelete {
log.Info(e.ctx, "deleted workspace",
- slog.F("locked_at", ws.LockedAt.Time),
- slog.F("locked_ttl", templateSchedule.LockedTTL),
+ slog.F("dormant_at", ws.DormantAt.Time),
+ slog.F("time_til_dormant_autodelete", templateSchedule.TimeTilDormantAutoDelete),
)
}
@@ -246,7 +246,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
// for this function to return a nil error as well as an empty transition.
// In such cases it means no provisioning should occur but the workspace
// may be "transitioning" to a new state (such as an inactive, stopped
-// workspace transitioning to the locked state).
+// workspace transitioning to the dormant state).
func getNextTransition(
ws database.Workspace,
latestBuild database.WorkspaceBuild,
@@ -265,13 +265,13 @@ func getNextTransition(
return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil
case isEligibleForFailedStop(latestBuild, latestJob, templateSchedule, currentTick):
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
- case isEligibleForLockedStop(ws, templateSchedule, currentTick):
+ case isEligibleForDormantStop(ws, templateSchedule, currentTick):
// Only stop started workspaces.
if latestBuild.Transition == database.WorkspaceTransitionStart {
return database.WorkspaceTransitionStop, database.BuildReasonAutolock, nil
}
// We shouldn't transition the workspace but we should still
- // lock it.
+ // make it dormant.
return "", database.BuildReasonAutolock, nil
case isEligibleForDelete(ws, templateSchedule, currentTick):
@@ -288,8 +288,8 @@ func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild
return false
}
- // If the workspace is locked we should not autostart it.
- if ws.LockedAt.Valid {
+ // If the workspace is dormant we should not autostart it.
+ if ws.DormantAt.Valid {
return false
}
@@ -322,8 +322,8 @@ func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild,
return false
}
- // If the workspace is locked we should not autostop it.
- if ws.LockedAt.Valid {
+ // If the workspace is dormant we should not autostop it.
+ if ws.DormantAt.Valid {
return false
}
@@ -334,23 +334,23 @@ func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild,
!currentTick.Before(build.Deadline)
}
-// isEligibleForLockedStop returns true if the workspace should be locked
+// isEligibleForDormantStop returns true if the workspace should be dormant
// for breaching the inactivity threshold of the template.
-func isEligibleForLockedStop(ws database.Workspace, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool {
- // Only attempt to lock workspaces not already locked.
- return !ws.LockedAt.Valid &&
- // The template must specify an inactivity TTL.
- templateSchedule.InactivityTTL > 0 &&
- // The workspace must breach the inactivity TTL.
- currentTick.Sub(ws.LastUsedAt) > templateSchedule.InactivityTTL
+func isEligibleForDormantStop(ws database.Workspace, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool {
+ // Only attempt against workspaces not already dormant.
+ return !ws.DormantAt.Valid &&
+ // The template must specify an time_til_dormant value.
+ templateSchedule.TimeTilDormant > 0 &&
+ // The workspace must breach the time_til_dormant value.
+ currentTick.Sub(ws.LastUsedAt) > templateSchedule.TimeTilDormant
}
func isEligibleForDelete(ws database.Workspace, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool {
- // Only attempt to delete locked workspaces.
- return ws.LockedAt.Valid && ws.DeletingAt.Valid &&
- // Locked workspaces should only be deleted if a locked_ttl is specified.
- templateSchedule.LockedTTL > 0 &&
- // The workspace must breach the locked_ttl.
+ // Only attempt to delete dormant workspaces.
+ return ws.DormantAt.Valid && ws.DeletingAt.Valid &&
+ // Dormant workspaces should only be deleted if a time_til_dormant_autodelete value is specified.
+ templateSchedule.TimeTilDormantAutoDelete > 0 &&
+ // The workspace must breach the time_til_dormant_autodelete value.
currentTick.After(ws.DeletingAt.Time)
}
diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go
index 7803c9fcd41da..7159f3b7d5665 100644
--- a/coderd/autobuild/lifecycle_executor_test.go
+++ b/coderd/autobuild/lifecycle_executor_test.go
@@ -13,14 +13,14 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/autobuild"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/autobuild"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
func TestExecutorAutostartOK(t *testing.T) {
@@ -683,8 +683,8 @@ func TestExecutorFailedWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionFailed,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyFailed,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
@@ -733,11 +733,11 @@ func TestExecutorInactiveWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
- ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
+ ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
@@ -766,22 +766,16 @@ func mustProvisionWorkspaceWithParameters(t *testing.T, client *codersdk.Client,
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
+ ProvisionPlan: []*proto.Response{
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
Parameters: richParameters,
},
},
},
},
- ProvisionApply: []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- },
- },
+ ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
diff --git a/coderd/autobuild/notify/notifier_test.go b/coderd/autobuild/notify/notifier_test.go
index fb547defd5b36..09e8158abaa99 100644
--- a/coderd/autobuild/notify/notifier_test.go
+++ b/coderd/autobuild/notify/notifier_test.go
@@ -9,7 +9,7 @@ import (
"go.uber.org/atomic"
"go.uber.org/goleak"
- "github.com/coder/coder/coderd/autobuild/notify"
+ "github.com/coder/coder/v2/coderd/autobuild/notify"
)
func TestNotifier(t *testing.T) {
diff --git a/coderd/awsidentity/awsidentity_test.go b/coderd/awsidentity/awsidentity_test.go
index 755079fc87b80..50aeec98aea22 100644
--- a/coderd/awsidentity/awsidentity_test.go
+++ b/coderd/awsidentity/awsidentity_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/awsidentity"
+ "github.com/coder/coder/v2/coderd/awsidentity"
)
const (
diff --git a/coderd/azureidentity/azureidentity_test.go b/coderd/azureidentity/azureidentity_test.go
index 08dda41976166..1ae35d0385429 100644
--- a/coderd/azureidentity/azureidentity_test.go
+++ b/coderd/azureidentity/azureidentity_test.go
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/azureidentity"
+ "github.com/coder/coder/v2/coderd/azureidentity"
)
func TestValidate(t *testing.T) {
diff --git a/coderd/batchstats/batcher.go b/coderd/batchstats/batcher.go
new file mode 100644
index 0000000000000..b3b881f2133e9
--- /dev/null
+++ b/coderd/batchstats/batcher.go
@@ -0,0 +1,296 @@
+package batchstats
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/google/uuid"
+ "golang.org/x/xerrors"
+
+ "cdr.dev/slog"
+ "cdr.dev/slog/sloggers/sloghuman"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+)
+
+const (
+ defaultBufferSize = 1024
+ defaultFlushInterval = time.Second
+)
+
+// Batcher holds a buffer of agent stats and periodically flushes them to
+// its configured store. It also updates the workspace's last used time.
+type Batcher struct {
+ store database.Store
+ log slog.Logger
+
+ mu sync.Mutex
+ // TODO: make this a buffered chan instead?
+ buf *database.InsertWorkspaceAgentStatsParams
+ // NOTE: we batch this separately as it's a jsonb field and
+ // pq.Array + unnest doesn't play nicely with this.
+ connectionsByProto []map[string]int64
+ batchSize int
+
+ // tickCh is used to periodically flush the buffer.
+ tickCh <-chan time.Time
+ ticker *time.Ticker
+ interval time.Duration
+ // flushLever is used to signal the flusher to flush the buffer immediately.
+ flushLever chan struct{}
+ flushForced atomic.Bool
+ // flushed is used during testing to signal that a flush has completed.
+ flushed chan<- int
+}
+
+// Option is a functional option for configuring a Batcher.
+type Option func(b *Batcher)
+
+// WithStore sets the store to use for storing stats.
+func WithStore(store database.Store) Option {
+ return func(b *Batcher) {
+ b.store = store
+ }
+}
+
+// WithBatchSize sets the number of stats to store in a batch.
+func WithBatchSize(size int) Option {
+ return func(b *Batcher) {
+ b.batchSize = size
+ }
+}
+
+// WithInterval sets the interval for flushes.
+func WithInterval(d time.Duration) Option {
+ return func(b *Batcher) {
+ b.interval = d
+ }
+}
+
+// WithLogger sets the logger to use for logging.
+func WithLogger(log slog.Logger) Option {
+ return func(b *Batcher) {
+ b.log = log
+ }
+}
+
+// New creates a new Batcher and starts it.
+func New(ctx context.Context, opts ...Option) (*Batcher, func(), error) {
+ b := &Batcher{}
+ b.log = slog.Make(sloghuman.Sink(os.Stderr))
+ b.flushLever = make(chan struct{}, 1) // Buffered so that it doesn't block.
+ for _, opt := range opts {
+ opt(b)
+ }
+
+ if b.store == nil {
+ return nil, nil, xerrors.Errorf("no store configured for batcher")
+ }
+
+ if b.interval == 0 {
+ b.interval = defaultFlushInterval
+ }
+
+ if b.batchSize == 0 {
+ b.batchSize = defaultBufferSize
+ }
+
+ if b.tickCh == nil {
+ b.ticker = time.NewTicker(b.interval)
+ b.tickCh = b.ticker.C
+ }
+
+ b.initBuf(b.batchSize)
+
+ cancelCtx, cancelFunc := context.WithCancel(ctx)
+ done := make(chan struct{})
+ go func() {
+ b.run(cancelCtx)
+ close(done)
+ }()
+
+ closer := func() {
+ cancelFunc()
+ if b.ticker != nil {
+ b.ticker.Stop()
+ }
+ <-done
+ }
+
+ return b, closer, nil
+}
+
+// Add adds a stat to the batcher for the given workspace and agent.
+func (b *Batcher) Add(
+ now time.Time,
+ agentID uuid.UUID,
+ templateID uuid.UUID,
+ userID uuid.UUID,
+ workspaceID uuid.UUID,
+ st agentsdk.Stats,
+) error {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ now = database.Time(now)
+
+ b.buf.ID = append(b.buf.ID, uuid.New())
+ b.buf.CreatedAt = append(b.buf.CreatedAt, now)
+ b.buf.AgentID = append(b.buf.AgentID, agentID)
+ b.buf.UserID = append(b.buf.UserID, userID)
+ b.buf.TemplateID = append(b.buf.TemplateID, templateID)
+ b.buf.WorkspaceID = append(b.buf.WorkspaceID, workspaceID)
+
+ // Store the connections by proto separately as it's a jsonb field. We marshal on flush.
+ // b.buf.ConnectionsByProto = append(b.buf.ConnectionsByProto, st.ConnectionsByProto)
+ b.connectionsByProto = append(b.connectionsByProto, st.ConnectionsByProto)
+
+ b.buf.ConnectionCount = append(b.buf.ConnectionCount, st.ConnectionCount)
+ b.buf.RxPackets = append(b.buf.RxPackets, st.RxPackets)
+ b.buf.RxBytes = append(b.buf.RxBytes, st.RxBytes)
+ b.buf.TxPackets = append(b.buf.TxPackets, st.TxPackets)
+ b.buf.TxBytes = append(b.buf.TxBytes, st.TxBytes)
+ b.buf.SessionCountVSCode = append(b.buf.SessionCountVSCode, st.SessionCountVSCode)
+ b.buf.SessionCountJetBrains = append(b.buf.SessionCountJetBrains, st.SessionCountJetBrains)
+ b.buf.SessionCountReconnectingPTY = append(b.buf.SessionCountReconnectingPTY, st.SessionCountReconnectingPTY)
+ b.buf.SessionCountSSH = append(b.buf.SessionCountSSH, st.SessionCountSSH)
+ b.buf.ConnectionMedianLatencyMS = append(b.buf.ConnectionMedianLatencyMS, st.ConnectionMedianLatencyMS)
+
+ // If the buffer is over 80% full, signal the flusher to flush immediately.
+ // We want to trigger flushes early to reduce the likelihood of
+ // accidentally growing the buffer over batchSize.
+ filled := float64(len(b.buf.ID)) / float64(b.batchSize)
+ if filled >= 0.8 && !b.flushForced.Load() {
+ b.flushLever <- struct{}{}
+ b.flushForced.Store(true)
+ }
+ return nil
+}
+
+// Run runs the batcher.
+func (b *Batcher) run(ctx context.Context) {
+ // nolint:gocritic // This is only ever used for one thing - inserting agent stats.
+ authCtx := dbauthz.AsSystemRestricted(ctx)
+ for {
+ select {
+ case <-b.tickCh:
+ b.flush(authCtx, false, "scheduled")
+ case <-b.flushLever:
+ // If the flush lever is depressed, flush the buffer immediately.
+ b.flush(authCtx, true, "reaching capacity")
+ case <-ctx.Done():
+ b.log.Debug(ctx, "context done, flushing before exit")
+
+ // We must create a new context here as the parent context is done.
+ ctxTimeout, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer cancel() //nolint:revive // We're returning, defer is fine.
+
+ // nolint:gocritic // This is only ever used for one thing - inserting agent stats.
+ b.flush(dbauthz.AsSystemRestricted(ctxTimeout), true, "exit")
+ return
+ }
+ }
+}
+
+// flush flushes the batcher's buffer.
+func (b *Batcher) flush(ctx context.Context, forced bool, reason string) {
+ b.mu.Lock()
+ b.flushForced.Store(true)
+ start := time.Now()
+ count := len(b.buf.ID)
+ defer func() {
+ b.flushForced.Store(false)
+ b.mu.Unlock()
+ if count > 0 {
+ elapsed := time.Since(start)
+ b.log.Debug(ctx, "flush complete",
+ slog.F("count", count),
+ slog.F("elapsed", elapsed),
+ slog.F("forced", forced),
+ slog.F("reason", reason),
+ )
+ }
+ // Notify that a flush has completed. This only happens in tests.
+ if b.flushed != nil {
+ select {
+ case <-ctx.Done():
+ close(b.flushed)
+ default:
+ b.flushed <- count
+ }
+ }
+ }()
+
+ if len(b.buf.ID) == 0 {
+ return
+ }
+
+ // marshal connections by proto
+ payload, err := json.Marshal(b.connectionsByProto)
+ if err != nil {
+ b.log.Error(ctx, "unable to marshal agent connections by proto, dropping data", slog.Error(err))
+ b.buf.ConnectionsByProto = json.RawMessage(`[]`)
+ } else {
+ b.buf.ConnectionsByProto = payload
+ }
+
+ err = b.store.InsertWorkspaceAgentStats(ctx, *b.buf)
+ elapsed := time.Since(start)
+ if err != nil {
+ b.log.Error(ctx, "error inserting workspace agent stats", slog.Error(err), slog.F("elapsed", elapsed))
+ return
+ }
+
+ b.resetBuf()
+}
+
+// initBuf resets the buffer. b MUST be locked.
+func (b *Batcher) initBuf(size int) {
+ b.buf = &database.InsertWorkspaceAgentStatsParams{
+ ID: make([]uuid.UUID, 0, b.batchSize),
+ CreatedAt: make([]time.Time, 0, b.batchSize),
+ UserID: make([]uuid.UUID, 0, b.batchSize),
+ WorkspaceID: make([]uuid.UUID, 0, b.batchSize),
+ TemplateID: make([]uuid.UUID, 0, b.batchSize),
+ AgentID: make([]uuid.UUID, 0, b.batchSize),
+ ConnectionsByProto: json.RawMessage("[]"),
+ ConnectionCount: make([]int64, 0, b.batchSize),
+ RxPackets: make([]int64, 0, b.batchSize),
+ RxBytes: make([]int64, 0, b.batchSize),
+ TxPackets: make([]int64, 0, b.batchSize),
+ TxBytes: make([]int64, 0, b.batchSize),
+ SessionCountVSCode: make([]int64, 0, b.batchSize),
+ SessionCountJetBrains: make([]int64, 0, b.batchSize),
+ SessionCountReconnectingPTY: make([]int64, 0, b.batchSize),
+ SessionCountSSH: make([]int64, 0, b.batchSize),
+ ConnectionMedianLatencyMS: make([]float64, 0, b.batchSize),
+ }
+
+ b.connectionsByProto = make([]map[string]int64, 0, size)
+}
+
+func (b *Batcher) resetBuf() {
+ b.buf.ID = b.buf.ID[:0]
+ b.buf.CreatedAt = b.buf.CreatedAt[:0]
+ b.buf.UserID = b.buf.UserID[:0]
+ b.buf.WorkspaceID = b.buf.WorkspaceID[:0]
+ b.buf.TemplateID = b.buf.TemplateID[:0]
+ b.buf.AgentID = b.buf.AgentID[:0]
+ b.buf.ConnectionsByProto = json.RawMessage(`[]`)
+ b.buf.ConnectionCount = b.buf.ConnectionCount[:0]
+ b.buf.RxPackets = b.buf.RxPackets[:0]
+ b.buf.RxBytes = b.buf.RxBytes[:0]
+ b.buf.TxPackets = b.buf.TxPackets[:0]
+ b.buf.TxBytes = b.buf.TxBytes[:0]
+ b.buf.SessionCountVSCode = b.buf.SessionCountVSCode[:0]
+ b.buf.SessionCountJetBrains = b.buf.SessionCountJetBrains[:0]
+ b.buf.SessionCountReconnectingPTY = b.buf.SessionCountReconnectingPTY[:0]
+ b.buf.SessionCountSSH = b.buf.SessionCountSSH[:0]
+ b.buf.ConnectionMedianLatencyMS = b.buf.ConnectionMedianLatencyMS[:0]
+ b.connectionsByProto = b.connectionsByProto[:0]
+}
diff --git a/coderd/batchstats/batcher_internal_test.go b/coderd/batchstats/batcher_internal_test.go
new file mode 100644
index 0000000000000..8c1c367f7db5b
--- /dev/null
+++ b/coderd/batchstats/batcher_internal_test.go
@@ -0,0 +1,226 @@
+package batchstats
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "cdr.dev/slog"
+ "cdr.dev/slog/sloggers/slogtest"
+
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/cryptorand"
+)
+
+func TestBatchStats(t *testing.T) {
+ t.Parallel()
+
+ // Given: a fresh batcher with no data
+ ctx, cancel := context.WithCancel(context.Background())
+ t.Cleanup(cancel)
+ log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
+ store, _ := dbtestutil.NewDB(t)
+
+ // Set up some test dependencies.
+ deps1 := setupDeps(t, store)
+ deps2 := setupDeps(t, store)
+ tick := make(chan time.Time)
+ flushed := make(chan int, 1)
+
+ b, closer, err := New(ctx,
+ WithStore(store),
+ WithLogger(log),
+ func(b *Batcher) {
+ b.tickCh = tick
+ b.flushed = flushed
+ },
+ )
+ require.NoError(t, err)
+ t.Cleanup(closer)
+
+ // Given: no data points are added for workspace
+ // When: it becomes time to report stats
+ t1 := database.Now()
+ // Signal a tick and wait for a flush to complete.
+ tick <- t1
+ f := <-flushed
+ require.Equal(t, 0, f, "expected no data to be flushed")
+ t.Logf("flush 1 completed")
+
+ // Then: it should report no stats.
+ stats, err := store.GetWorkspaceAgentStats(ctx, t1)
+ require.NoError(t, err, "should not error getting stats")
+ require.Empty(t, stats, "should have no stats for workspace")
+
+ // Given: a single data point is added for workspace
+ t2 := t1.Add(time.Second)
+ t.Logf("inserting 1 stat")
+ require.NoError(t, b.Add(t2.Add(time.Millisecond), deps1.Agent.ID, deps1.User.ID, deps1.Template.ID, deps1.Workspace.ID, randAgentSDKStats(t)))
+
+ // When: it becomes time to report stats
+ // Signal a tick and wait for a flush to complete.
+ tick <- t2
+ f = <-flushed // Wait for a flush to complete.
+ require.Equal(t, 1, f, "expected one stat to be flushed")
+ t.Logf("flush 2 completed")
+
+ // Then: it should report a single stat.
+ stats, err = store.GetWorkspaceAgentStats(ctx, t2)
+ require.NoError(t, err, "should not error getting stats")
+ require.Len(t, stats, 1, "should have stats for workspace")
+
+ // Given: a lot of data points are added for both workspaces
+ // (equal to batch size)
+ t3 := t2.Add(time.Second)
+ done := make(chan struct{})
+
+ go func() {
+ defer close(done)
+ t.Logf("inserting %d stats", defaultBufferSize)
+ for i := 0; i < defaultBufferSize; i++ {
+ if i%2 == 0 {
+ require.NoError(t, b.Add(t3.Add(time.Millisecond), deps1.Agent.ID, deps1.User.ID, deps1.Template.ID, deps1.Workspace.ID, randAgentSDKStats(t)))
+ } else {
+ require.NoError(t, b.Add(t3.Add(time.Millisecond), deps2.Agent.ID, deps2.User.ID, deps2.Template.ID, deps2.Workspace.ID, randAgentSDKStats(t)))
+ }
+ }
+ }()
+
+ // When: the buffer comes close to capacity
+ // Then: The buffer will force-flush once.
+ f = <-flushed
+ t.Logf("flush 3 completed")
+ require.Greater(t, f, 819, "expected at least 819 stats to be flushed (>=80% of buffer)")
+ // And we should finish inserting the stats
+ <-done
+
+ stats, err = store.GetWorkspaceAgentStats(ctx, t3)
+ require.NoError(t, err, "should not error getting stats")
+ require.Len(t, stats, 2, "should have stats for both workspaces")
+
+ // Ensures that a subsequent flush pushes all the remaining data
+ t4 := t3.Add(time.Second)
+ tick <- t4
+ f2 := <-flushed
+ t.Logf("flush 4 completed")
+ expectedCount := defaultBufferSize - f
+ require.Equal(t, expectedCount, f2, "did not flush expected remaining rows")
+
+ // Ensure that a subsequent flush does not push stale data.
+ t5 := t4.Add(time.Second)
+ tick <- t5
+ f = <-flushed
+ require.Zero(t, f, "expected zero stats to have been flushed")
+ t.Logf("flush 5 completed")
+
+ stats, err = store.GetWorkspaceAgentStats(ctx, t5)
+ require.NoError(t, err, "should not error getting stats")
+ require.Len(t, stats, 0, "should have no stats for workspace")
+
+ // Ensure that buf never grew beyond what we expect
+ require.Equal(t, defaultBufferSize, cap(b.buf.ID), "buffer grew beyond expected capacity")
+}
+
+// randAgentSDKStats returns a random agentsdk.Stats
+func randAgentSDKStats(t *testing.T, opts ...func(*agentsdk.Stats)) agentsdk.Stats {
+ t.Helper()
+ s := agentsdk.Stats{
+ ConnectionsByProto: map[string]int64{
+ "ssh": mustRandInt64n(t, 9) + 1,
+ "vscode": mustRandInt64n(t, 9) + 1,
+ "jetbrains": mustRandInt64n(t, 9) + 1,
+ "reconnecting_pty": mustRandInt64n(t, 9) + 1,
+ },
+ ConnectionCount: mustRandInt64n(t, 99) + 1,
+ ConnectionMedianLatencyMS: float64(mustRandInt64n(t, 99) + 1),
+ RxPackets: mustRandInt64n(t, 99) + 1,
+ RxBytes: mustRandInt64n(t, 99) + 1,
+ TxPackets: mustRandInt64n(t, 99) + 1,
+ TxBytes: mustRandInt64n(t, 99) + 1,
+ SessionCountVSCode: mustRandInt64n(t, 9) + 1,
+ SessionCountJetBrains: mustRandInt64n(t, 9) + 1,
+ SessionCountReconnectingPTY: mustRandInt64n(t, 9) + 1,
+ SessionCountSSH: mustRandInt64n(t, 9) + 1,
+ Metrics: []agentsdk.AgentMetric{},
+ }
+ for _, opt := range opts {
+ opt(&s)
+ }
+ return s
+}
+
+// deps is a set of test dependencies.
+type deps struct {
+ Agent database.WorkspaceAgent
+ Template database.Template
+ User database.User
+ Workspace database.Workspace
+}
+
+// setupDeps sets up a set of test dependencies.
+// It creates an organization, user, template, workspace, and agent
+// along with all the other miscellaneous plumbing required to link
+// them together.
+func setupDeps(t *testing.T, store database.Store) deps {
+ t.Helper()
+
+ org := dbgen.Organization(t, store, database.Organization{})
+ user := dbgen.User(t, store, database.User{})
+ _, err := store.InsertOrganizationMember(context.Background(), database.InsertOrganizationMemberParams{
+ OrganizationID: org.ID,
+ UserID: user.ID,
+ Roles: []string{rbac.RoleOrgMember(org.ID)},
+ })
+ require.NoError(t, err)
+ tv := dbgen.TemplateVersion(t, store, database.TemplateVersion{
+ OrganizationID: org.ID,
+ CreatedBy: user.ID,
+ })
+ tpl := dbgen.Template(t, store, database.Template{
+ CreatedBy: user.ID,
+ OrganizationID: org.ID,
+ ActiveVersionID: tv.ID,
+ })
+ ws := dbgen.Workspace(t, store, database.Workspace{
+ TemplateID: tpl.ID,
+ OwnerID: user.ID,
+ OrganizationID: org.ID,
+ LastUsedAt: time.Now().Add(-time.Hour),
+ })
+ pj := dbgen.ProvisionerJob(t, store, database.ProvisionerJob{
+ InitiatorID: user.ID,
+ OrganizationID: org.ID,
+ })
+ _ = dbgen.WorkspaceBuild(t, store, database.WorkspaceBuild{
+ TemplateVersionID: tv.ID,
+ WorkspaceID: ws.ID,
+ JobID: pj.ID,
+ })
+ res := dbgen.WorkspaceResource(t, store, database.WorkspaceResource{
+ Transition: database.WorkspaceTransitionStart,
+ JobID: pj.ID,
+ })
+ agt := dbgen.WorkspaceAgent(t, store, database.WorkspaceAgent{
+ ResourceID: res.ID,
+ })
+ return deps{
+ Agent: agt,
+ Template: tpl,
+ User: user,
+ Workspace: ws,
+ }
+}
+
+// mustRandInt64n returns a random int64 in the range [0, n).
+func mustRandInt64n(t *testing.T, n int64) int64 {
+ t.Helper()
+ i, err := cryptorand.Intn(int(n))
+ require.NoError(t, err)
+ return int64(i)
+}
diff --git a/coderd/client_test.go b/coderd/client_test.go
index 0ba31c8d32014..79002e767fb5d 100644
--- a/coderd/client_test.go
+++ b/coderd/client_test.go
@@ -6,8 +6,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
// Issue: https://github.com/coder/coder/issues/5249
diff --git a/coderd/coderd.go b/coderd/coderd.go
index d7b80ff273097..0338a020eae36 100644
--- a/coderd/coderd.go
+++ b/coderd/coderd.go
@@ -37,36 +37,37 @@ import (
"tailscale.com/util/singleflight"
// Used for swagger docs.
- _ "github.com/coder/coder/coderd/apidoc"
+ _ "github.com/coder/coder/v2/coderd/apidoc"
"cdr.dev/slog"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/awsidentity"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/gitsshkey"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/metricscache"
- "github.com/coder/coder/coderd/provisionerdserver"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/coderd/updatecheck"
- "github.com/coder/coder/coderd/util/slice"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/coderd/wsconncache"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisionerd/proto"
- "github.com/coder/coder/provisionersdk"
- "github.com/coder/coder/site"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/awsidentity"
+ "github.com/coder/coder/v2/coderd/batchstats"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/gitsshkey"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/metricscache"
+ "github.com/coder/coder/v2/coderd/provisionerdserver"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/updatecheck"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/coderd/wsconncache"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisionerd/proto"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/site"
+ "github.com/coder/coder/v2/tailnet"
)
// We must only ever instantiate one httpSwagger.Handler because of a data race
@@ -126,8 +127,8 @@ type Options struct {
BaseDERPMap *tailcfg.DERPMap
DERPMapUpdateFrequency time.Duration
SwaggerEndpoint bool
- SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
- SetUserSiteRoles func(ctx context.Context, tx database.Store, userID uuid.UUID, roles []string) error
+ SetUserGroups func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, groupNames []string, createMissingGroups bool) error
+ SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
@@ -160,6 +161,9 @@ type Options struct {
HTTPClient *http.Client
UpdateAgentMetrics func(ctx context.Context, username, workspaceName, agentName string, metrics []agentsdk.AgentMetric)
+ StatsBatcher *batchstats.Batcher
+
+ WorkspaceAppsStatsCollectorOptions workspaceapps.StatsCollectorOptions
}
// @title Coder API
@@ -258,16 +262,16 @@ func New(options *Options) *API {
options.TracerProvider = trace.NewNoopTracerProvider()
}
if options.SetUserGroups == nil {
- options.SetUserGroups = func(ctx context.Context, _ database.Store, userID uuid.UUID, groups []string) error {
- options.Logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license",
- slog.F("user_id", userID), slog.F("groups", groups),
+ options.SetUserGroups = func(ctx context.Context, logger slog.Logger, _ database.Store, userID uuid.UUID, groups []string, createMissingGroups bool) error {
+ logger.Warn(ctx, "attempted to assign OIDC groups without enterprise license",
+ slog.F("user_id", userID), slog.F("groups", groups), slog.F("create_missing_groups", createMissingGroups),
)
return nil
}
}
if options.SetUserSiteRoles == nil {
- options.SetUserSiteRoles = func(ctx context.Context, _ database.Store, userID uuid.UUID, roles []string) error {
- options.Logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license",
+ options.SetUserSiteRoles = func(ctx context.Context, logger slog.Logger, _ database.Store, userID uuid.UUID, roles []string) error {
+ logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license",
slog.F("user_id", userID), slog.F("roles", roles),
)
return nil
@@ -288,6 +292,10 @@ func New(options *Options) *API {
options.UserQuietHoursScheduleStore.Store(&v)
}
+ if options.StatsBatcher == nil {
+ panic("developer error: options.StatsBatcher is nil")
+ }
+
siteCacheDir := options.CacheDir
if siteCacheDir != "" {
siteCacheDir = filepath.Join(siteCacheDir, "site")
@@ -394,11 +402,13 @@ func New(options *Options) *API {
api.agentProvider, err = NewServerTailnet(api.ctx,
options.Logger,
options.DERPServer,
- options.BaseDERPMap,
+ api.DERPMap,
+ options.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
func(context.Context) (tailnet.MultiAgentConn, error) {
return (*api.TailnetCoordinator.Load()).ServeMultiAgent(uuid.New()), nil
},
wsconncache.New(api._dialWorkspaceAgentTailnet, 0),
+ api.TracerProvider,
)
if err != nil {
panic("failed to setup server tailnet: " + err.Error())
@@ -409,8 +419,17 @@ func New(options *Options) *API {
}
}
+ workspaceAppsLogger := options.Logger.Named("workspaceapps")
+ if options.WorkspaceAppsStatsCollectorOptions.Logger == nil {
+ named := workspaceAppsLogger.Named("stats_collector")
+ options.WorkspaceAppsStatsCollectorOptions.Logger = &named
+ }
+ if options.WorkspaceAppsStatsCollectorOptions.Reporter == nil {
+ options.WorkspaceAppsStatsCollectorOptions.Reporter = workspaceapps.NewStatsDBReporter(options.Database, workspaceapps.DefaultStatsDBReporterBatchSize)
+ }
+
api.workspaceAppServer = &workspaceapps.Server{
- Logger: options.Logger.Named("workspaceapps"),
+ Logger: workspaceAppsLogger,
DashboardURL: api.AccessURL,
AccessURL: api.AccessURL,
@@ -421,6 +440,7 @@ func New(options *Options) *API {
SignedTokenProvider: api.WorkspaceAppsProvider,
AgentProvider: api.agentProvider,
AppSecurityKey: options.AppSecurityKey,
+ StatsCollector: workspaceapps.NewStatsCollector(options.WorkspaceAppsStatsCollectorOptions),
DisablePathApps: options.DeploymentValues.DisablePathApps.Value(),
SecureAuthCookie: options.DeploymentValues.SecureAuthCookie.Value(),
@@ -462,6 +482,8 @@ func New(options *Options) *API {
cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value())
prometheusMW := httpmw.Prometheus(options.PrometheusRegistry)
+ api.statsBatcher = options.StatsBatcher
+
r.Use(
httpmw.Recover(api.Logger),
tracing.StatusWriterMiddleware,
@@ -683,7 +705,6 @@ func New(options *Options) *API {
r.Route("/github", func(r chi.Router) {
r.Use(
httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, nil),
- apiKeyMiddlewareOptional,
)
r.Get("/callback", api.userOAuth2Github)
})
@@ -691,7 +712,6 @@ func New(options *Options) *API {
r.Route("/oidc/callback", func(r chi.Router) {
r.Use(
httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, oidcAuthURLParams),
- apiKeyMiddlewareOptional,
)
r.Get("/", api.userOIDC)
})
@@ -833,7 +853,7 @@ func New(options *Options) *API {
})
r.Get("/watch", api.watchWorkspace)
r.Put("/extend", api.putExtendWorkspace)
- r.Put("/lock", api.putWorkspaceLock)
+ r.Put("/dormant", api.putWorkspaceDormant)
})
})
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
@@ -994,6 +1014,8 @@ type API struct {
healthCheckGroup *singleflight.Group[string, *healthcheck.Report]
healthCheckCache atomic.Pointer[healthcheck.Report]
+
+ statsBatcher *batchstats.Batcher
}
// Close waits for all WebSocket connections to drain before returning.
@@ -1009,6 +1031,7 @@ func (api *API) Close() error {
if api.updateChecker != nil {
api.updateChecker.Close()
}
+ _ = api.workspaceAppServer.Close()
coordinator := api.TailnetCoordinator.Load()
if coordinator != nil {
_ = (*coordinator).Close()
diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go
index 0cd69915d13dc..6edf4657cc903 100644
--- a/coderd/coderd_test.go
+++ b/coderd/coderd_test.go
@@ -2,13 +2,18 @@ package coderd_test
import (
"context"
+ "flag"
"io"
"net/http"
"net/netip"
"strconv"
+ "strings"
"sync"
+ "sync/atomic"
"testing"
+ "github.com/davecgh/go-spew/spew"
+ "github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
@@ -17,12 +22,20 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/testutil"
)
+// updateGoldenFiles is a flag that can be set to update golden files.
+var updateGoldenFiles = flag.Bool("update", false, "Update golden files")
+
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
@@ -115,6 +128,91 @@ func TestDERP(t *testing.T) {
w2.Close()
}
+func TestDERPForceWebSockets(t *testing.T) {
+ t.Parallel()
+
+ dv := coderdtest.DeploymentValues(t)
+ dv.DERP.Config.ForceWebSockets = true
+ dv.DERP.Config.BlockDirect = true // to ensure the test always uses DERP
+
+ // Manually create a server so we can influence the HTTP handler.
+ options := &coderdtest.Options{
+ DeploymentValues: dv,
+ }
+ setHandler, cancelFunc, serverURL, newOptions := coderdtest.NewOptions(t, options)
+ coderAPI := coderd.New(newOptions)
+ t.Cleanup(func() {
+ cancelFunc()
+ _ = coderAPI.Close()
+ })
+
+ // Set the HTTP handler to a custom one that ensures all /derp calls are
+ // WebSockets and not `Upgrade: derp`.
+ var upgradeCount int64
+ setHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ if strings.HasPrefix(r.URL.Path, "/derp") {
+ up := r.Header.Get("Upgrade")
+ if up != "" && up != "websocket" {
+ t.Errorf("expected Upgrade: websocket, got %q", up)
+ } else {
+ atomic.AddInt64(&upgradeCount, 1)
+ }
+ }
+
+ coderAPI.RootHandler.ServeHTTP(rw, r)
+ }))
+
+ // Start a provisioner daemon.
+ provisionerCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
+ t.Cleanup(func() {
+ _ = provisionerCloser.Close()
+ })
+
+ client := codersdk.New(serverURL)
+ t.Cleanup(func() {
+ client.HTTPClient.CloseIdleConnections()
+ })
+ user := coderdtest.CreateFirstUser(t, client)
+
+ gen, err := client.WorkspaceAgentConnectionInfoGeneric(context.Background())
+ require.NoError(t, err)
+ t.Log(spew.Sdump(gen))
+
+ authToken := uuid.NewString()
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
+ })
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
+
+ agentClient := agentsdk.New(client.URL)
+ agentClient.SetSessionToken(authToken)
+ agentCloser := agent.New(agent.Options{
+ Client: agentClient,
+ Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug),
+ })
+ defer func() {
+ _ = agentCloser.Close()
+ }()
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
+ conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
+ require.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ conn.AwaitReachable(ctx)
+
+ require.GreaterOrEqual(t, atomic.LoadInt64(&upgradeCount), int64(1), "expected at least one /derp call")
+}
+
func TestDERPLatencyCheck(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go
index 889e8f1ee485a..ce9faf1ace16f 100644
--- a/coderd/coderdtest/authorize.go
+++ b/coderd/coderdtest/authorize.go
@@ -15,12 +15,12 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/rbac/regosql"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac/regosql"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
)
// RBACAsserter is a helper for asserting that the correct RBAC checks are
diff --git a/coderd/coderdtest/authorize_test.go b/coderd/coderdtest/authorize_test.go
index 67bcf482def75..13a04200a9d2f 100644
--- a/coderd/coderdtest/authorize_test.go
+++ b/coderd/coderdtest/authorize_test.go
@@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/rbac"
)
func TestAuthzRecorder(t *testing.T) {
diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go
index 4d4fbb8c5e78e..5ef17af359ca7 100644
--- a/coderd/coderdtest/coderdtest.go
+++ b/coderd/coderdtest/coderdtest.go
@@ -31,16 +31,13 @@ import (
"time"
"cloud.google.com/go/compute/metadata"
- "github.com/coreos/go-oidc/v3/oidc"
"github.com/fullsailor/pkcs7"
- "github.com/golang-jwt/jwt"
+ "github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/prometheus/client_golang/prometheus"
- "github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "golang.org/x/oauth2"
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
@@ -51,37 +48,39 @@ import (
"tailscale.com/types/nettype"
"cdr.dev/slog"
+ "cdr.dev/slog/sloggers/sloghuman"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/autobuild"
- "github.com/coder/coder/coderd/awsidentity"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/gitsshkey"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/coderd/unhanger"
- "github.com/coder/coder/coderd/updatecheck"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionerd"
- provisionerdproto "github.com/coder/coder/provisionerd/proto"
- "github.com/coder/coder/provisionersdk"
- sdkproto "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/autobuild"
+ "github.com/coder/coder/v2/coderd/awsidentity"
+ "github.com/coder/coder/v2/coderd/batchstats"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/gitsshkey"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/coderd/unhanger"
+ "github.com/coder/coder/v2/coderd/updatecheck"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionerd"
+ provisionerdproto "github.com/coder/coder/v2/provisionerd/proto"
+ "github.com/coder/coder/v2/provisionersdk"
+ sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/testutil"
)
// AppSecurityKey is a 96-byte key used to sign JWTs and encrypt JWEs for
@@ -140,7 +139,10 @@ type Options struct {
SwaggerEndpoint bool
// Logger should only be overridden if you expect errors
// as part of your test.
- Logger *slog.Logger
+ Logger *slog.Logger
+ StatsBatcher *batchstats.Batcher
+
+ WorkspaceAppsStatsCollectorOptions workspaceapps.StatsCollectorOptions
}
// New constructs a codersdk client connected to an in-memory API instance.
@@ -241,6 +243,18 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
if options.FilesRateLimit == 0 {
options.FilesRateLimit = -1
}
+ if options.StatsBatcher == nil {
+ ctx, cancel := context.WithCancel(context.Background())
+ t.Cleanup(cancel)
+ batcher, closeBatcher, err := batchstats.New(ctx,
+ batchstats.WithStore(options.Database),
+ // Avoid cluttering up test output.
+ batchstats.WithLogger(slog.Make(sloghuman.Sink(io.Discard))),
+ )
+ require.NoError(t, err, "create stats batcher")
+ options.StatsBatcher = batcher
+ t.Cleanup(closeBatcher)
+ }
var templateScheduleStore atomic.Pointer[schedule.TemplateScheduleStore]
if options.TemplateScheduleStore == nil {
@@ -309,13 +323,13 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
stunAddresses []string
dvStunAddresses = options.DeploymentValues.DERP.Server.STUNAddresses.Value()
)
- if len(dvStunAddresses) == 0 || (len(dvStunAddresses) == 1 && dvStunAddresses[0] == "stun.l.google.com:19302") {
+ if len(dvStunAddresses) == 0 || dvStunAddresses[0] == "stun.l.google.com:19302" {
stunAddr, stunCleanup := stuntest.ServeWithPacketListener(t, nettype.Std{})
stunAddr.IP = net.ParseIP("127.0.0.1")
t.Cleanup(stunCleanup)
stunAddresses = []string{stunAddr.String()}
options.DeploymentValues.DERP.Server.STUNAddresses = stunAddresses
- } else if dvStunAddresses[0] != "disable" {
+ } else if dvStunAddresses[0] != tailnet.DisableSTUN {
stunAddresses = options.DeploymentValues.DERP.Server.STUNAddresses.Value()
}
@@ -379,36 +393,38 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
Pubsub: options.Pubsub,
GitAuthConfigs: options.GitAuthConfigs,
- Auditor: options.Auditor,
- AWSCertificates: options.AWSCertificates,
- AzureCertificates: options.AzureCertificates,
- GithubOAuth2Config: options.GithubOAuth2Config,
- RealIPConfig: options.RealIPConfig,
- OIDCConfig: options.OIDCConfig,
- GoogleTokenValidator: options.GoogleTokenValidator,
- SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
- DERPServer: derpServer,
- APIRateLimit: options.APIRateLimit,
- LoginRateLimit: options.LoginRateLimit,
- FilesRateLimit: options.FilesRateLimit,
- Authorizer: options.Authorizer,
- Telemetry: telemetry.NewNoop(),
- TemplateScheduleStore: &templateScheduleStore,
- TLSCertificates: options.TLSCertificates,
- TrialGenerator: options.TrialGenerator,
- TailnetCoordinator: options.Coordinator,
- BaseDERPMap: derpMap,
- DERPMapUpdateFrequency: 150 * time.Millisecond,
- MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval,
- AgentStatsRefreshInterval: options.AgentStatsRefreshInterval,
- DeploymentValues: options.DeploymentValues,
- UpdateCheckOptions: options.UpdateCheckOptions,
- SwaggerEndpoint: options.SwaggerEndpoint,
- AppSecurityKey: AppSecurityKey,
- SSHConfig: options.ConfigSSH,
- HealthcheckFunc: options.HealthcheckFunc,
- HealthcheckTimeout: options.HealthcheckTimeout,
- HealthcheckRefresh: options.HealthcheckRefresh,
+ Auditor: options.Auditor,
+ AWSCertificates: options.AWSCertificates,
+ AzureCertificates: options.AzureCertificates,
+ GithubOAuth2Config: options.GithubOAuth2Config,
+ RealIPConfig: options.RealIPConfig,
+ OIDCConfig: options.OIDCConfig,
+ GoogleTokenValidator: options.GoogleTokenValidator,
+ SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
+ DERPServer: derpServer,
+ APIRateLimit: options.APIRateLimit,
+ LoginRateLimit: options.LoginRateLimit,
+ FilesRateLimit: options.FilesRateLimit,
+ Authorizer: options.Authorizer,
+ Telemetry: telemetry.NewNoop(),
+ TemplateScheduleStore: &templateScheduleStore,
+ TLSCertificates: options.TLSCertificates,
+ TrialGenerator: options.TrialGenerator,
+ TailnetCoordinator: options.Coordinator,
+ BaseDERPMap: derpMap,
+ DERPMapUpdateFrequency: 150 * time.Millisecond,
+ MetricsCacheRefreshInterval: options.MetricsCacheRefreshInterval,
+ AgentStatsRefreshInterval: options.AgentStatsRefreshInterval,
+ DeploymentValues: options.DeploymentValues,
+ UpdateCheckOptions: options.UpdateCheckOptions,
+ SwaggerEndpoint: options.SwaggerEndpoint,
+ AppSecurityKey: AppSecurityKey,
+ SSHConfig: options.ConfigSSH,
+ HealthcheckFunc: options.HealthcheckFunc,
+ HealthcheckTimeout: options.HealthcheckTimeout,
+ HealthcheckRefresh: options.HealthcheckRefresh,
+ StatsBatcher: options.StatsBatcher,
+ WorkspaceAppsStatsCollectorOptions: options.WorkspaceAppsStatsCollectorOptions,
}
}
@@ -450,10 +466,13 @@ func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer {
_ = echoServer.Close()
cancelFunc()
})
- fs := afero.NewMemMapFs()
+ // seems t.TempDir() is not safe to call from a different goroutine
+ workDir := t.TempDir()
go func() {
- err := echo.Serve(ctx, fs, &provisionersdk.ServeOptions{
- Listener: echoServer,
+ err := echo.Serve(ctx, &provisionersdk.ServeOptions{
+ Listener: echoServer,
+ WorkDirectory: workDir,
+ Logger: coderAPI.Logger.Named("echo").Leveled(slog.LevelDebug),
})
assert.NoError(t, err)
}()
@@ -461,7 +480,6 @@ func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer {
closer := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
return coderAPI.CreateInMemoryProvisionerDaemon(ctx, 0)
}, &provisionerd.Options{
- Filesystem: fs,
Logger: coderAPI.Logger.Named("provisionerd").Leveled(slog.LevelDebug),
JobPollInterval: 50 * time.Millisecond,
UpdateInterval: 250 * time.Millisecond,
@@ -469,7 +487,6 @@ func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer {
Provisioners: provisionerd.Provisioners{
string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient),
},
- WorkDirectory: t.TempDir(),
})
t.Cleanup(func() {
_ = closer.Close()
@@ -487,19 +504,22 @@ func NewExternalProvisionerDaemon(t *testing.T, client *codersdk.Client, org uui
cancelFunc()
<-serveDone
})
- fs := afero.NewMemMapFs()
go func() {
defer close(serveDone)
- err := echo.Serve(ctx, fs, &provisionersdk.ServeOptions{
- Listener: echoServer,
+ err := echo.Serve(ctx, &provisionersdk.ServeOptions{
+ Listener: echoServer,
+ WorkDirectory: t.TempDir(),
})
assert.NoError(t, err)
}()
closer := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
- return client.ServeProvisionerDaemon(ctx, org, []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho}, tags)
+ return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Organization: org,
+ Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho},
+ Tags: tags,
+ })
}, &provisionerd.Options{
- Filesystem: fs,
Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug),
JobPollInterval: 50 * time.Millisecond,
UpdateInterval: 250 * time.Millisecond,
@@ -507,7 +527,6 @@ func NewExternalProvisionerDaemon(t *testing.T, client *codersdk.Client, org uui
Provisioners: provisionerd.Provisioners{
string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient),
},
- WorkDirectory: t.TempDir(),
})
t.Cleanup(func() {
_ = closer.Close()
@@ -568,14 +587,7 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
require.NoError(t, err)
var sessionToken string
- if !req.DisableLogin {
- login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
- Email: req.Email,
- Password: req.Password,
- })
- require.NoError(t, err)
- sessionToken = login.SessionToken
- } else {
+ if req.DisableLogin || req.UserLoginType == codersdk.LoginTypeNone {
// Cannot log in with a disabled login user. So make it an api key from
// the client making this user.
token, err := client.CreateToken(context.Background(), user.ID.String(), codersdk.CreateTokenRequest{
@@ -585,6 +597,13 @@ func createAnotherUserRetry(t *testing.T, client *codersdk.Client, organizationI
})
require.NoError(t, err)
sessionToken = token.Key
+ } else {
+ login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
+ Email: req.Email,
+ Password: req.Password,
+ })
+ require.NoError(t, err)
+ sessionToken = login.SessionToken
}
if user.Status == codersdk.UserStatusDormant {
@@ -999,105 +1018,6 @@ func NewAWSInstanceIdentity(t *testing.T, instanceID string) (awsidentity.Certif
}
}
-type OIDCConfig struct {
- key *rsa.PrivateKey
- issuer string
-}
-
-func NewOIDCConfig(t *testing.T, issuer string) *OIDCConfig {
- t.Helper()
-
- block, _ := pem.Decode([]byte(testRSAPrivateKey))
- pkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
- require.NoError(t, err)
-
- if issuer == "" {
- issuer = "https://coder.com"
- }
-
- return &OIDCConfig{
- key: pkey,
- issuer: issuer,
- }
-}
-
-func (*OIDCConfig) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string {
- return "/?state=" + url.QueryEscape(state)
-}
-
-func (*OIDCConfig) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource {
- return nil
-}
-
-func (*OIDCConfig) Exchange(_ context.Context, code string, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
- token, err := base64.StdEncoding.DecodeString(code)
- if err != nil {
- return nil, xerrors.Errorf("decode code: %w", err)
- }
- return (&oauth2.Token{
- AccessToken: "token",
- }).WithExtra(map[string]interface{}{
- "id_token": string(token),
- }), nil
-}
-
-func (o *OIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string {
- t.Helper()
-
- if _, ok := claims["exp"]; !ok {
- claims["exp"] = time.Now().Add(time.Hour).UnixMilli()
- }
-
- if _, ok := claims["iss"]; !ok {
- claims["iss"] = o.issuer
- }
-
- if _, ok := claims["sub"]; !ok {
- claims["sub"] = "testme"
- }
-
- signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(o.key)
- require.NoError(t, err)
-
- return base64.StdEncoding.EncodeToString([]byte(signed))
-}
-
-func (o *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims, opts ...func(cfg *coderd.OIDCConfig)) *coderd.OIDCConfig {
- // By default, the provider can be empty.
- // This means it won't support any endpoints!
- provider := &oidc.Provider{}
- if userInfoClaims != nil {
- resp, err := json.Marshal(userInfoClaims)
- require.NoError(t, err)
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write(resp)
- }))
- t.Cleanup(srv.Close)
- cfg := &oidc.ProviderConfig{
- UserInfoURL: srv.URL,
- }
- provider = cfg.NewProvider(context.Background())
- }
- cfg := &coderd.OIDCConfig{
- OAuth2Config: o,
- Verifier: oidc.NewVerifier(o.issuer, &oidc.StaticKeySet{
- PublicKeys: []crypto.PublicKey{o.key.Public()},
- }, &oidc.Config{
- SkipClientIDCheck: true,
- }),
- Provider: provider,
- UsernameField: "preferred_username",
- EmailField: "email",
- AuthURLParams: map[string]string{"access_type": "offline"},
- GroupField: "groups",
- }
- for _, opt := range opts {
- opt(cfg)
- }
- return cfg
-}
-
// NewAzureInstanceIdentity returns a metadata client and ID token validator for faking
// instance authentication for Azure.
func NewAzureInstanceIdentity(t *testing.T, instanceID string) (x509.VerifyOptions, *http.Client) {
@@ -1186,22 +1106,6 @@ func SDKError(t *testing.T, err error) *codersdk.Error {
return cerr
}
-const testRSAPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
-MIICXQIBAAKBgQDLets8+7M+iAQAqN/5BVyCIjhTQ4cmXulL+gm3v0oGMWzLupUS
-v8KPA+Tp7dgC/DZPfMLaNH1obBBhJ9DhS6RdS3AS3kzeFrdu8zFHLWF53DUBhS92
-5dCAEuJpDnNizdEhxTfoHrhuCmz8l2nt1pe5eUK2XWgd08Uc93h5ij098wIDAQAB
-AoGAHLaZeWGLSaen6O/rqxg2laZ+jEFbMO7zvOTruiIkL/uJfrY1kw+8RLIn+1q0
-wLcWcuEIHgKKL9IP/aXAtAoYh1FBvRPLkovF1NZB0Je/+CSGka6wvc3TGdvppZJe
-rKNcUvuOYLxkmLy4g9zuY5qrxFyhtIn2qZzXEtLaVOHzPQECQQDvN0mSajpU7dTB
-w4jwx7IRXGSSx65c+AsHSc1Rj++9qtPC6WsFgAfFN2CEmqhMbEUVGPv/aPjdyWk9
-pyLE9xR/AkEA2cGwyIunijE5v2rlZAD7C4vRgdcMyCf3uuPcgzFtsR6ZhyQSgLZ8
-YRPuvwm4cdPJMmO3YwBfxT6XGuSc2k8MjQJBAI0+b8prvpV2+DCQa8L/pjxp+VhR
-Xrq2GozrHrgR7NRokTB88hwFRJFF6U9iogy9wOx8HA7qxEbwLZuhm/4AhbECQC2a
-d8h4Ht09E+f3nhTEc87mODkl7WJZpHL6V2sORfeq/eIkds+H6CJ4hy5w/bSw8tjf
-sz9Di8sGIaUbLZI2rd0CQQCzlVwEtRtoNCyMJTTrkgUuNufLP19RZ5FpyXxBO5/u
-QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8
------END RSA PRIVATE KEY-----`
-
func DeploymentValues(t testing.TB) *codersdk.DeploymentValues {
var cfg codersdk.DeploymentValues
opts := cfg.Options()
diff --git a/coderd/coderdtest/coderdtest_test.go b/coderd/coderdtest/coderdtest_test.go
index c0187fc94d661..780b58a569478 100644
--- a/coderd/coderdtest/coderdtest_test.go
+++ b/coderd/coderdtest/coderdtest_test.go
@@ -5,7 +5,7 @@ import (
"go.uber.org/goleak"
- "github.com/coder/coder/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
)
func TestMain(m *testing.M) {
diff --git a/coderd/coderdtest/oidctest/helper.go b/coderd/coderdtest/oidctest/helper.go
new file mode 100644
index 0000000000000..11d9114be2ce8
--- /dev/null
+++ b/coderd/coderdtest/oidctest/helper.go
@@ -0,0 +1,103 @@
+package oidctest
+
+import (
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/golang-jwt/jwt/v4"
+ "github.com/stretchr/testify/require"
+
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
+)
+
+// LoginHelper helps with logging in a user and refreshing their oauth tokens.
+// It is mainly because refreshing oauth tokens is a bit tricky and requires
+// some database manipulation.
+type LoginHelper struct {
+ fake *FakeIDP
+ client *codersdk.Client
+}
+
+func NewLoginHelper(client *codersdk.Client, fake *FakeIDP) *LoginHelper {
+ if client == nil {
+ panic("client must not be nil")
+ }
+ if fake == nil {
+ panic("fake must not be nil")
+ }
+ return &LoginHelper{
+ fake: fake,
+ client: client,
+ }
+}
+
+// Login just helps by making an unauthenticated client and logging in with
+// the given claims. All Logins should be unauthenticated, so this is a
+// convenience method.
+func (h *LoginHelper) Login(t *testing.T, idTokenClaims jwt.MapClaims) (*codersdk.Client, *http.Response) {
+ t.Helper()
+ unauthenticatedClient := codersdk.New(h.client.URL)
+
+ return h.fake.Login(t, unauthenticatedClient, idTokenClaims)
+}
+
+// ExpireOauthToken expires the oauth token for the given user.
+func (*LoginHelper) ExpireOauthToken(t *testing.T, db database.Store, user *codersdk.Client) database.UserLink {
+ t.Helper()
+
+ //nolint:gocritic // Testing
+ ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitMedium))
+
+ id, _, err := httpmw.SplitAPIToken(user.SessionToken())
+ require.NoError(t, err)
+
+ // We need to get the OIDC link and update it in the database to force
+ // it to be expired.
+ key, err := db.GetAPIKeyByID(ctx, id)
+ require.NoError(t, err, "get api key")
+
+ link, err := db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
+ UserID: key.UserID,
+ LoginType: database.LoginTypeOIDC,
+ })
+ require.NoError(t, err, "get user link")
+
+ // Expire the oauth link for the given user.
+ updated, err := db.UpdateUserLink(ctx, database.UpdateUserLinkParams{
+ OAuthAccessToken: link.OAuthAccessToken,
+ OAuthRefreshToken: link.OAuthRefreshToken,
+ OAuthExpiry: time.Now().Add(time.Hour * -1),
+ UserID: link.UserID,
+ LoginType: link.LoginType,
+ })
+ require.NoError(t, err, "expire user link")
+
+ return updated
+}
+
+// ForceRefresh forces the client to refresh its oauth token. It does this by
+// expiring the oauth token, then doing an authenticated call. This will force
+// the API Key middleware to refresh the oauth token.
+//
+// A unit test assertion makes sure the refresh token is used.
+func (h *LoginHelper) ForceRefresh(t *testing.T, db database.Store, user *codersdk.Client, idToken jwt.MapClaims) {
+ t.Helper()
+
+ link := h.ExpireOauthToken(t, db, user)
+ // Updates the claims that the IDP will return. By default, it always
+ // uses the original claims for the original oauth token.
+ h.fake.UpdateRefreshClaims(link.OAuthRefreshToken, idToken)
+
+ t.Cleanup(func() {
+ require.True(t, h.fake.RefreshUsed(link.OAuthRefreshToken), "refresh token must be used, but has not. Did you forget to call the returned function from this call?")
+ })
+
+ // Do any authenticated call to force the refresh
+ _, err := user.User(testutil.Context(t, testutil.WaitShort), "me")
+ require.NoError(t, err, "user must be able to be fetched")
+}
diff --git a/coderd/coderdtest/oidctest/idp.go b/coderd/coderdtest/oidctest/idp.go
new file mode 100644
index 0000000000000..3ca8cadbc9ff9
--- /dev/null
+++ b/coderd/coderdtest/oidctest/idp.go
@@ -0,0 +1,793 @@
+package oidctest
+
+import (
+ "context"
+ "crypto"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/http/cookiejar"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/coder/coder/v2/coderd/util/syncmap"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/go-chi/chi/v5"
+ "github.com/go-jose/go-jose/v3"
+ "github.com/golang-jwt/jwt/v4"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/oauth2"
+ "golang.org/x/xerrors"
+
+ "cdr.dev/slog"
+ "cdr.dev/slog/sloggers/slogtest"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/codersdk"
+)
+
+// FakeIDP is a functional OIDC provider.
+// It only supports 1 OIDC client.
+type FakeIDP struct {
+ issuer string
+ key *rsa.PrivateKey
+ provider providerJSON
+ handler http.Handler
+ cfg *oauth2.Config
+
+ // clientID to be used by coderd
+ clientID string
+ clientSecret string
+ logger slog.Logger
+
+ // These maps are used to control the state of the IDP.
+ // That is the various access tokens, refresh tokens, states, etc.
+ codeToStateMap *syncmap.Map[string, string]
+ // Token -> Email
+ accessTokens *syncmap.Map[string, string]
+ // Refresh Token -> Email
+ refreshTokensUsed *syncmap.Map[string, bool]
+ refreshTokens *syncmap.Map[string, string]
+ stateToIDTokenClaims *syncmap.Map[string, jwt.MapClaims]
+ refreshIDTokenClaims *syncmap.Map[string, jwt.MapClaims]
+
+ // hooks
+ // hookValidRedirectURL can be used to reject a redirect url from the
+ // IDP -> Application. Almost all IDPs have the concept of
+ // "Authorized Redirect URLs". This can be used to emulate that.
+ hookValidRedirectURL func(redirectURL string) error
+ hookUserInfo func(email string) jwt.MapClaims
+ fakeCoderd func(req *http.Request) (*http.Response, error)
+ hookOnRefresh func(email string) error
+ // Custom authentication for the client. This is useful if you want
+ // to test something like PKI auth vs a client_secret.
+ hookAuthenticateClient func(t testing.TB, req *http.Request) (url.Values, error)
+ serve bool
+}
+
+type FakeIDPOpt func(idp *FakeIDP)
+
+func WithAuthorizedRedirectURL(hook func(redirectURL string) error) func(*FakeIDP) {
+ return func(f *FakeIDP) {
+ f.hookValidRedirectURL = hook
+ }
+}
+
+// WithRefreshHook is called when a refresh token is used. The email is
+// the email of the user that is being refreshed assuming the claims are correct.
+func WithRefreshHook(hook func(email string) error) func(*FakeIDP) {
+ return func(f *FakeIDP) {
+ f.hookOnRefresh = hook
+ }
+}
+
+func WithCustomClientAuth(hook func(t testing.TB, req *http.Request) (url.Values, error)) func(*FakeIDP) {
+ return func(f *FakeIDP) {
+ f.hookAuthenticateClient = hook
+ }
+}
+
+// WithLogging is optional, but will log some HTTP calls made to the IDP.
+func WithLogging(t testing.TB, options *slogtest.Options) func(*FakeIDP) {
+ return func(f *FakeIDP) {
+ f.logger = slogtest.Make(t, options)
+ }
+}
+
+// WithStaticUserInfo is optional, but will return the same user info for
+// every user on the /userinfo endpoint.
+func WithStaticUserInfo(info jwt.MapClaims) func(*FakeIDP) {
+ return func(f *FakeIDP) {
+ f.hookUserInfo = func(_ string) jwt.MapClaims {
+ return info
+ }
+ }
+}
+
+func WithDynamicUserInfo(userInfoFunc func(email string) jwt.MapClaims) func(*FakeIDP) {
+ return func(f *FakeIDP) {
+ f.hookUserInfo = userInfoFunc
+ }
+}
+
+// WithServing makes the IDP run an actual http server.
+func WithServing() func(*FakeIDP) {
+ return func(f *FakeIDP) {
+ f.serve = true
+ }
+}
+
+func WithIssuer(issuer string) func(*FakeIDP) {
+ return func(f *FakeIDP) {
+ f.issuer = issuer
+ }
+}
+
+const (
+ // nolint:gosec // It thinks this is a secret lol
+ tokenPath = "/oauth2/token"
+ authorizePath = "/oauth2/authorize"
+ keysPath = "/oauth2/keys"
+ userInfoPath = "/oauth2/userinfo"
+)
+
+func NewFakeIDP(t testing.TB, opts ...FakeIDPOpt) *FakeIDP {
+ t.Helper()
+
+ block, _ := pem.Decode([]byte(testRSAPrivateKey))
+ pkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+ require.NoError(t, err)
+
+ idp := &FakeIDP{
+ key: pkey,
+ clientID: uuid.NewString(),
+ clientSecret: uuid.NewString(),
+ logger: slog.Make(),
+ codeToStateMap: syncmap.New[string, string](),
+ accessTokens: syncmap.New[string, string](),
+ refreshTokens: syncmap.New[string, string](),
+ refreshTokensUsed: syncmap.New[string, bool](),
+ stateToIDTokenClaims: syncmap.New[string, jwt.MapClaims](),
+ refreshIDTokenClaims: syncmap.New[string, jwt.MapClaims](),
+ hookOnRefresh: func(_ string) error { return nil },
+ hookUserInfo: func(email string) jwt.MapClaims { return jwt.MapClaims{} },
+ hookValidRedirectURL: func(redirectURL string) error { return nil },
+ }
+
+ for _, opt := range opts {
+ opt(idp)
+ }
+
+ if idp.issuer == "" {
+ idp.issuer = "https://coder.com"
+ }
+
+ idp.handler = idp.httpHandler(t)
+ idp.updateIssuerURL(t, idp.issuer)
+ if idp.serve {
+ idp.realServer(t)
+ }
+
+ return idp
+}
+
+func (f *FakeIDP) updateIssuerURL(t testing.TB, issuer string) {
+ t.Helper()
+
+ u, err := url.Parse(issuer)
+ require.NoError(t, err, "invalid issuer URL")
+
+ f.issuer = issuer
+ // providerJSON is the JSON representation of the OpenID Connect provider
+ // These are all the urls that the IDP will respond to.
+ f.provider = providerJSON{
+ Issuer: issuer,
+ AuthURL: u.ResolveReference(&url.URL{Path: authorizePath}).String(),
+ TokenURL: u.ResolveReference(&url.URL{Path: tokenPath}).String(),
+ JWKSURL: u.ResolveReference(&url.URL{Path: keysPath}).String(),
+ UserInfoURL: u.ResolveReference(&url.URL{Path: userInfoPath}).String(),
+ Algorithms: []string{
+ "RS256",
+ },
+ }
+}
+
+// realServer turns the FakeIDP into a real http server.
+func (f *FakeIDP) realServer(t testing.TB) *httptest.Server {
+ t.Helper()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ srv := httptest.NewUnstartedServer(f.handler)
+ srv.Config.BaseContext = func(_ net.Listener) context.Context {
+ return ctx
+ }
+ srv.Start()
+ t.Cleanup(srv.CloseClientConnections)
+ t.Cleanup(srv.Close)
+ t.Cleanup(cancel)
+
+ f.updateIssuerURL(t, srv.URL)
+ return srv
+}
+
+// Login does the full OIDC flow starting at the "LoginButton".
+// The client argument is just to get the URL of the Coder instance.
+//
+// The client passed in is just to get the url of the Coder instance.
+// The actual client that is used is 100% unauthenticated and fresh.
+func (f *FakeIDP) Login(t testing.TB, client *codersdk.Client, idTokenClaims jwt.MapClaims, opts ...func(r *http.Request)) (*codersdk.Client, *http.Response) {
+ t.Helper()
+
+ client, resp := f.AttemptLogin(t, client, idTokenClaims, opts...)
+ require.Equal(t, http.StatusOK, resp.StatusCode, "client failed to login")
+ return client, resp
+}
+
+func (f *FakeIDP) AttemptLogin(t testing.TB, client *codersdk.Client, idTokenClaims jwt.MapClaims, opts ...func(r *http.Request)) (*codersdk.Client, *http.Response) {
+ t.Helper()
+ var err error
+
+ cli := f.HTTPClient(client.HTTPClient)
+ shallowCpyCli := *cli
+
+ if shallowCpyCli.Jar == nil {
+ shallowCpyCli.Jar, err = cookiejar.New(nil)
+ require.NoError(t, err, "failed to create cookie jar")
+ }
+
+ unauthenticated := codersdk.New(client.URL)
+ unauthenticated.HTTPClient = &shallowCpyCli
+
+ return f.LoginWithClient(t, unauthenticated, idTokenClaims, opts...)
+}
+
+// LoginWithClient reuses the context of the passed in client. This means the same
+// cookies will be used. This should be an unauthenticated client in most cases.
+//
+// This is a niche case, but it is needed for testing ConvertLoginType.
+func (f *FakeIDP) LoginWithClient(t testing.TB, client *codersdk.Client, idTokenClaims jwt.MapClaims, opts ...func(r *http.Request)) (*codersdk.Client, *http.Response) {
+ t.Helper()
+
+ coderOauthURL, err := client.URL.Parse("/api/v2/users/oidc/callback")
+ require.NoError(t, err)
+ f.SetRedirect(t, coderOauthURL.String())
+
+ cli := f.HTTPClient(client.HTTPClient)
+ cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+ // Store the idTokenClaims to the specific state request. This ties
+ // the claims 1:1 with a given authentication flow.
+ state := req.URL.Query().Get("state")
+ f.stateToIDTokenClaims.Store(state, idTokenClaims)
+ return nil
+ }
+
+ req, err := http.NewRequestWithContext(context.Background(), "GET", coderOauthURL.String(), nil)
+ require.NoError(t, err)
+ if cli.Jar == nil {
+ cli.Jar, err = cookiejar.New(nil)
+ require.NoError(t, err, "failed to create cookie jar")
+ }
+
+ for _, opt := range opts {
+ opt(req)
+ }
+
+ res, err := cli.Do(req)
+ require.NoError(t, err)
+
+ // If the coder session token exists, return the new authed client!
+ var user *codersdk.Client
+ cookies := cli.Jar.Cookies(client.URL)
+ for _, cookie := range cookies {
+ if cookie.Name == codersdk.SessionTokenCookie {
+ user = codersdk.New(client.URL)
+ user.SetSessionToken(cookie.Value)
+ }
+ }
+
+ t.Cleanup(func() {
+ if res.Body != nil {
+ _ = res.Body.Close()
+ }
+ })
+
+ return user, res
+}
+
+// OIDCCallback will emulate the IDP redirecting back to the Coder callback.
+// This is helpful if no Coderd exists because the IDP needs to redirect to
+// something.
+// Essentially this is used to fake the Coderd side of the exchange.
+// The flow starts at the user hitting the OIDC login page.
+func (f *FakeIDP) OIDCCallback(t testing.TB, state string, idTokenClaims jwt.MapClaims) (*http.Response, error) {
+ t.Helper()
+ if f.serve {
+ panic("cannot use OIDCCallback with WithServing. This is only for the in memory usage")
+ }
+
+ f.stateToIDTokenClaims.Store(state, idTokenClaims)
+
+ cli := f.HTTPClient(nil)
+ u := f.cfg.AuthCodeURL(state)
+ req, err := http.NewRequest("GET", u, nil)
+ require.NoError(t, err)
+
+ resp, err := cli.Do(req.WithContext(context.Background()))
+ require.NoError(t, err)
+
+ t.Cleanup(func() {
+ if resp.Body != nil {
+ _ = resp.Body.Close()
+ }
+ })
+ return resp, nil
+}
+
+type providerJSON struct {
+ Issuer string `json:"issuer"`
+ AuthURL string `json:"authorization_endpoint"`
+ TokenURL string `json:"token_endpoint"`
+ JWKSURL string `json:"jwks_uri"`
+ UserInfoURL string `json:"userinfo_endpoint"`
+ Algorithms []string `json:"id_token_signing_alg_values_supported"`
+}
+
+// newCode enforces the code exchanged is actually a valid code
+// created by the IDP.
+func (f *FakeIDP) newCode(state string) string {
+ code := uuid.NewString()
+ f.codeToStateMap.Store(code, state)
+ return code
+}
+
+// newToken enforces the access token exchanged is actually a valid access token
+// created by the IDP.
+func (f *FakeIDP) newToken(email string) string {
+ accessToken := uuid.NewString()
+ f.accessTokens.Store(accessToken, email)
+ return accessToken
+}
+
+func (f *FakeIDP) newRefreshTokens(email string) string {
+ refreshToken := uuid.NewString()
+ f.refreshTokens.Store(refreshToken, email)
+ return refreshToken
+}
+
+// authenticateBearerTokenRequest enforces the access token is valid.
+func (f *FakeIDP) authenticateBearerTokenRequest(t testing.TB, req *http.Request) (string, error) {
+ t.Helper()
+
+ auth := req.Header.Get("Authorization")
+ token := strings.TrimPrefix(auth, "Bearer ")
+ _, ok := f.accessTokens.Load(token)
+ if !ok {
+ return "", xerrors.New("invalid access token")
+ }
+ return token, nil
+}
+
+// authenticateOIDCClientRequest enforces the client_id and client_secret are valid.
+func (f *FakeIDP) authenticateOIDCClientRequest(t testing.TB, req *http.Request) (url.Values, error) {
+ t.Helper()
+
+ if f.hookAuthenticateClient != nil {
+ return f.hookAuthenticateClient(t, req)
+ }
+
+ data, err := io.ReadAll(req.Body)
+ if !assert.NoError(t, err, "read token request body") {
+ return nil, xerrors.Errorf("authenticate request, read body: %w", err)
+ }
+ values, err := url.ParseQuery(string(data))
+ if !assert.NoError(t, err, "parse token request values") {
+ return nil, xerrors.New("invalid token request")
+ }
+
+ if !assert.Equal(t, f.clientID, values.Get("client_id"), "client_id mismatch") {
+ return nil, xerrors.New("client_id mismatch")
+ }
+
+ if !assert.Equal(t, f.clientSecret, values.Get("client_secret"), "client_secret mismatch") {
+ return nil, xerrors.New("client_secret mismatch")
+ }
+
+ return values, nil
+}
+
+// encodeClaims is a helper func to convert claims to a valid JWT.
+func (f *FakeIDP) encodeClaims(t testing.TB, claims jwt.MapClaims) string {
+ t.Helper()
+
+ if _, ok := claims["exp"]; !ok {
+ claims["exp"] = time.Now().Add(time.Hour).UnixMilli()
+ }
+
+ if _, ok := claims["aud"]; !ok {
+ claims["aud"] = f.clientID
+ }
+
+ if _, ok := claims["iss"]; !ok {
+ claims["iss"] = f.issuer
+ }
+
+ signed, err := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(f.key)
+ require.NoError(t, err)
+
+ return signed
+}
+
+// httpHandler is the IDP http server.
+func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
+ t.Helper()
+
+ mux := chi.NewMux()
+ // This endpoint is required to initialize the OIDC provider.
+ // It is used to get the OIDC configuration.
+ mux.Get("/.well-known/openid-configuration", func(rw http.ResponseWriter, r *http.Request) {
+ f.logger.Info(r.Context(), "http OIDC config", slog.F("url", r.URL.String()))
+
+ _ = json.NewEncoder(rw).Encode(f.provider)
+ })
+
+ // Authorize is called when the user is redirected to the IDP to login.
+ // This is the browser hitting the IDP and the user logging into Google or
+ // w/e and clicking "Allow". They will be redirected back to the redirect
+ // when this is done.
+ mux.Handle(authorizePath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ f.logger.Info(r.Context(), "http call authorize", slog.F("url", r.URL.String()))
+
+ clientID := r.URL.Query().Get("client_id")
+ if !assert.Equal(t, f.clientID, clientID, "unexpected client_id") {
+ http.Error(rw, "invalid client_id", http.StatusBadRequest)
+ return
+ }
+
+ redirectURI := r.URL.Query().Get("redirect_uri")
+ state := r.URL.Query().Get("state")
+
+ scope := r.URL.Query().Get("scope")
+ assert.NotEmpty(t, scope, "scope is empty")
+
+ responseType := r.URL.Query().Get("response_type")
+ switch responseType {
+ case "code":
+ case "token":
+ t.Errorf("response_type %q not supported", responseType)
+ http.Error(rw, "invalid response_type", http.StatusBadRequest)
+ return
+ default:
+ t.Errorf("unexpected response_type %q", responseType)
+ http.Error(rw, "invalid response_type", http.StatusBadRequest)
+ return
+ }
+
+ err := f.hookValidRedirectURL(redirectURI)
+ if err != nil {
+ t.Errorf("not authorized redirect_uri by custom hook %q: %s", redirectURI, err.Error())
+ http.Error(rw, fmt.Sprintf("invalid redirect_uri: %s", err.Error()), http.StatusBadRequest)
+ return
+ }
+
+ ru, err := url.Parse(redirectURI)
+ if err != nil {
+ t.Errorf("invalid redirect_uri %q: %s", redirectURI, err.Error())
+ http.Error(rw, fmt.Sprintf("invalid redirect_uri: %s", err.Error()), http.StatusBadRequest)
+ return
+ }
+
+ q := ru.Query()
+ q.Set("state", state)
+ q.Set("code", f.newCode(state))
+ ru.RawQuery = q.Encode()
+
+ http.Redirect(rw, r, ru.String(), http.StatusTemporaryRedirect)
+ }))
+
+ mux.Handle(tokenPath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ values, err := f.authenticateOIDCClientRequest(t, r)
+ f.logger.Info(r.Context(), "http idp call token",
+ slog.Error(err),
+ slog.F("values", values.Encode()),
+ )
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("invalid token request: %s", err.Error()), http.StatusBadRequest)
+ return
+ }
+ getEmail := func(claims jwt.MapClaims) string {
+ email, ok := claims["email"]
+ if !ok {
+ return "unknown"
+ }
+ emailStr, ok := email.(string)
+ if !ok {
+ return "wrong-type"
+ }
+ return emailStr
+ }
+
+ var claims jwt.MapClaims
+ switch values.Get("grant_type") {
+ case "authorization_code":
+ code := values.Get("code")
+ if !assert.NotEmpty(t, code, "code is empty") {
+ http.Error(rw, "invalid code", http.StatusBadRequest)
+ return
+ }
+ stateStr, ok := f.codeToStateMap.Load(code)
+ if !assert.True(t, ok, "invalid code") {
+ http.Error(rw, "invalid code", http.StatusBadRequest)
+ return
+ }
+ // Always invalidate the code after it is used.
+ f.codeToStateMap.Delete(code)
+
+ idTokenClaims, ok := f.stateToIDTokenClaims.Load(stateStr)
+ if !ok {
+ t.Errorf("missing id token claims")
+ http.Error(rw, "missing id token claims", http.StatusBadRequest)
+ return
+ }
+ claims = idTokenClaims
+ case "refresh_token":
+ refreshToken := values.Get("refresh_token")
+ if !assert.NotEmpty(t, refreshToken, "refresh_token is empty") {
+ http.Error(rw, "invalid refresh_token", http.StatusBadRequest)
+ return
+ }
+
+ _, ok := f.refreshTokens.Load(refreshToken)
+ if !assert.True(t, ok, "invalid refresh_token") {
+ http.Error(rw, "invalid refresh_token", http.StatusBadRequest)
+ return
+ }
+
+ idTokenClaims, ok := f.refreshIDTokenClaims.Load(refreshToken)
+ if !ok {
+ t.Errorf("missing id token claims in refresh")
+ http.Error(rw, "missing id token claims in refresh", http.StatusBadRequest)
+ return
+ }
+
+ claims = idTokenClaims
+ err := f.hookOnRefresh(getEmail(claims))
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("refresh hook blocked refresh: %s", err.Error()), http.StatusBadRequest)
+ return
+ }
+
+ f.refreshTokensUsed.Store(refreshToken, true)
+ // Always invalidate the refresh token after it is used.
+ f.refreshTokens.Delete(refreshToken)
+ default:
+ t.Errorf("unexpected grant_type %q", values.Get("grant_type"))
+ http.Error(rw, "invalid grant_type", http.StatusBadRequest)
+ return
+ }
+
+ exp := time.Now().Add(time.Minute * 5)
+ claims["exp"] = exp.UnixMilli()
+ email := getEmail(claims)
+ refreshToken := f.newRefreshTokens(email)
+ token := map[string]interface{}{
+ "access_token": f.newToken(email),
+ "refresh_token": refreshToken,
+ "token_type": "Bearer",
+ "expires_in": int64((time.Minute * 5).Seconds()),
+ "id_token": f.encodeClaims(t, claims),
+ }
+ // Store the claims for the next refresh
+ f.refreshIDTokenClaims.Store(refreshToken, claims)
+
+ rw.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(rw).Encode(token)
+ }))
+
+ mux.Handle(userInfoPath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ token, err := f.authenticateBearerTokenRequest(t, r)
+ f.logger.Info(r.Context(), "http call idp user info",
+ slog.Error(err),
+ slog.F("url", r.URL.String()),
+ )
+ if err != nil {
+ http.Error(rw, fmt.Sprintf("invalid user info request: %s", err.Error()), http.StatusBadRequest)
+ return
+ }
+
+ email, ok := f.accessTokens.Load(token)
+ if !ok {
+ t.Errorf("access token user for user_info has no email to indicate which user")
+ http.Error(rw, "invalid access token, missing user info", http.StatusBadRequest)
+ return
+ }
+ _ = json.NewEncoder(rw).Encode(f.hookUserInfo(email))
+ }))
+
+ mux.Handle(keysPath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ f.logger.Info(r.Context(), "http call idp /keys")
+ set := jose.JSONWebKeySet{
+ Keys: []jose.JSONWebKey{
+ {
+ Key: f.key.Public(),
+ KeyID: "test-key",
+ Algorithm: "RSA",
+ },
+ },
+ }
+ _ = json.NewEncoder(rw).Encode(set)
+ }))
+
+ mux.NotFound(func(rw http.ResponseWriter, r *http.Request) {
+ f.logger.Error(r.Context(), "http call not found", slog.F("path", r.URL.Path))
+ t.Errorf("unexpected request to IDP at path %q. Not supported", r.URL.Path)
+ })
+
+ return mux
+}
+
+// HTTPClient does nothing if IsServing is used.
+//
+// If IsServing is not used, then it will return a client that will make requests
+// to the IDP all in memory. If a request is not to the IDP, then the passed in
+// client will be used. If no client is passed in, then any regular network
+// requests will fail.
+func (f *FakeIDP) HTTPClient(rest *http.Client) *http.Client {
+ if f.serve {
+ if rest == nil || rest.Transport == nil {
+ return &http.Client{}
+ }
+ return rest
+ }
+
+ var jar http.CookieJar
+ if rest != nil {
+ jar = rest.Jar
+ }
+ return &http.Client{
+ Jar: jar,
+ Transport: fakeRoundTripper{
+ roundTrip: func(req *http.Request) (*http.Response, error) {
+ u, _ := url.Parse(f.issuer)
+ if req.URL.Host != u.Host {
+ if f.fakeCoderd != nil {
+ return f.fakeCoderd(req)
+ }
+ if rest == nil || rest.Transport == nil {
+ return nil, xerrors.Errorf("unexpected network request to %q", req.URL.Host)
+ }
+ return rest.Transport.RoundTrip(req)
+ }
+ resp := httptest.NewRecorder()
+ f.handler.ServeHTTP(resp, req)
+ return resp.Result(), nil
+ },
+ },
+ }
+}
+
+// RefreshUsed returns if the refresh token has been used. All refresh tokens
+// can only be used once, then they are deleted.
+func (f *FakeIDP) RefreshUsed(refreshToken string) bool {
+ used, _ := f.refreshTokensUsed.Load(refreshToken)
+ return used
+}
+
+// UpdateRefreshClaims allows the caller to change what claims are returned
+// for a given refresh token. By default, all refreshes use the same claims as
+// the original IDToken issuance.
+func (f *FakeIDP) UpdateRefreshClaims(refreshToken string, claims jwt.MapClaims) {
+ f.refreshIDTokenClaims.Store(refreshToken, claims)
+}
+
+// SetRedirect is required for the IDP to know where to redirect and call
+// Coderd.
+func (f *FakeIDP) SetRedirect(t testing.TB, u string) {
+ t.Helper()
+
+ f.cfg.RedirectURL = u
+}
+
+// SetCoderdCallback is optional and only works if not using the IsServing.
+// It will setup a fake "Coderd" for the IDP to call when the IDP redirects
+// back after authenticating.
+func (f *FakeIDP) SetCoderdCallback(callback func(req *http.Request) (*http.Response, error)) {
+ if f.serve {
+ panic("cannot set callback handler when using 'WithServing'. Must implement an actual 'Coderd'")
+ }
+ f.fakeCoderd = callback
+}
+
+func (f *FakeIDP) SetCoderdCallbackHandler(handler http.HandlerFunc) {
+ f.SetCoderdCallback(func(req *http.Request) (*http.Response, error) {
+ resp := httptest.NewRecorder()
+ handler.ServeHTTP(resp, req)
+ return resp.Result(), nil
+ })
+}
+
+// OIDCConfig returns the OIDC config to use for Coderd.
+func (f *FakeIDP) OIDCConfig(t testing.TB, scopes []string, opts ...func(cfg *coderd.OIDCConfig)) *coderd.OIDCConfig {
+ t.Helper()
+ if len(scopes) == 0 {
+ scopes = []string{"openid", "email", "profile"}
+ }
+
+ oauthCfg := &oauth2.Config{
+ ClientID: f.clientID,
+ ClientSecret: f.clientSecret,
+ Endpoint: oauth2.Endpoint{
+ AuthURL: f.provider.AuthURL,
+ TokenURL: f.provider.TokenURL,
+ AuthStyle: oauth2.AuthStyleInParams,
+ },
+ // If the user is using a real network request, they will need to do
+ // 'fake.SetRedirect()'
+ RedirectURL: "https://redirect.com",
+ Scopes: scopes,
+ }
+
+ ctx := oidc.ClientContext(context.Background(), f.HTTPClient(nil))
+ p, err := oidc.NewProvider(ctx, f.provider.Issuer)
+ require.NoError(t, err, "failed to create OIDC provider")
+ cfg := &coderd.OIDCConfig{
+ OAuth2Config: oauthCfg,
+ Provider: p,
+ Verifier: oidc.NewVerifier(f.provider.Issuer, &oidc.StaticKeySet{
+ PublicKeys: []crypto.PublicKey{f.key.Public()},
+ }, &oidc.Config{
+ ClientID: oauthCfg.ClientID,
+ SupportedSigningAlgs: []string{
+ "RS256",
+ },
+ // Todo: add support for Now()
+ }),
+ UsernameField: "preferred_username",
+ EmailField: "email",
+ AuthURLParams: map[string]string{"access_type": "offline"},
+ }
+
+ for _, opt := range opts {
+ if opt == nil {
+ continue
+ }
+ opt(cfg)
+ }
+
+ f.cfg = oauthCfg
+
+ return cfg
+}
+
+type fakeRoundTripper struct {
+ roundTrip func(req *http.Request) (*http.Response, error)
+}
+
+func (f fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ return f.roundTrip(req)
+}
+
+const testRSAPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDLets8+7M+iAQAqN/5BVyCIjhTQ4cmXulL+gm3v0oGMWzLupUS
+v8KPA+Tp7dgC/DZPfMLaNH1obBBhJ9DhS6RdS3AS3kzeFrdu8zFHLWF53DUBhS92
+5dCAEuJpDnNizdEhxTfoHrhuCmz8l2nt1pe5eUK2XWgd08Uc93h5ij098wIDAQAB
+AoGAHLaZeWGLSaen6O/rqxg2laZ+jEFbMO7zvOTruiIkL/uJfrY1kw+8RLIn+1q0
+wLcWcuEIHgKKL9IP/aXAtAoYh1FBvRPLkovF1NZB0Je/+CSGka6wvc3TGdvppZJe
+rKNcUvuOYLxkmLy4g9zuY5qrxFyhtIn2qZzXEtLaVOHzPQECQQDvN0mSajpU7dTB
+w4jwx7IRXGSSx65c+AsHSc1Rj++9qtPC6WsFgAfFN2CEmqhMbEUVGPv/aPjdyWk9
+pyLE9xR/AkEA2cGwyIunijE5v2rlZAD7C4vRgdcMyCf3uuPcgzFtsR6ZhyQSgLZ8
+YRPuvwm4cdPJMmO3YwBfxT6XGuSc2k8MjQJBAI0+b8prvpV2+DCQa8L/pjxp+VhR
+Xrq2GozrHrgR7NRokTB88hwFRJFF6U9iogy9wOx8HA7qxEbwLZuhm/4AhbECQC2a
+d8h4Ht09E+f3nhTEc87mODkl7WJZpHL6V2sORfeq/eIkds+H6CJ4hy5w/bSw8tjf
+sz9Di8sGIaUbLZI2rd0CQQCzlVwEtRtoNCyMJTTrkgUuNufLP19RZ5FpyXxBO5/u
+QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8
+-----END RSA PRIVATE KEY-----`
diff --git a/coderd/coderdtest/oidctest/idp_test.go b/coderd/coderdtest/oidctest/idp_test.go
new file mode 100644
index 0000000000000..519635b067916
--- /dev/null
+++ b/coderd/coderdtest/oidctest/idp_test.go
@@ -0,0 +1,73 @@
+package oidctest_test
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/golang-jwt/jwt/v4"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/oauth2"
+
+ "github.com/coder/coder/v2/coderd/coderdtest/oidctest"
+)
+
+// TestFakeIDPBasicFlow tests the basic flow of the fake IDP.
+// It is done all in memory with no actual network requests.
+// nolint:bodyclose
+func TestFakeIDPBasicFlow(t *testing.T) {
+ t.Parallel()
+
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithLogging(t, nil),
+ )
+
+ var handler http.Handler
+ srv := httptest.NewServer(http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ handler.ServeHTTP(w, r)
+ })))
+ defer srv.Close()
+
+ cfg := fake.OIDCConfig(t, nil)
+ cli := fake.HTTPClient(nil)
+ ctx := oidc.ClientContext(context.Background(), cli)
+
+ const expectedState = "random-state"
+ var token *oauth2.Token
+ // This is the Coder callback using an actual network request.
+ fake.SetCoderdCallbackHandler(func(w http.ResponseWriter, r *http.Request) {
+ // Emulate OIDC flow
+ code := r.URL.Query().Get("code")
+ state := r.URL.Query().Get("state")
+ assert.Equal(t, expectedState, state, "state mismatch")
+
+ oauthToken, err := cfg.Exchange(ctx, code)
+ if assert.NoError(t, err, "failed to exchange code") {
+ assert.NotEmpty(t, oauthToken.AccessToken, "access token is empty")
+ assert.NotEmpty(t, oauthToken.RefreshToken, "refresh token is empty")
+ }
+ token = oauthToken
+ })
+
+ resp, err := fake.OIDCCallback(t, expectedState, jwt.MapClaims{})
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ // Test the user info
+ _, err = cfg.Provider.UserInfo(ctx, oauth2.StaticTokenSource(token))
+ require.NoError(t, err)
+
+ // Now test it can refresh
+ refreshed, err := cfg.TokenSource(ctx, &oauth2.Token{
+ AccessToken: token.AccessToken,
+ RefreshToken: token.RefreshToken,
+ Expiry: time.Now().Add(time.Minute * -1),
+ }).Token()
+ require.NoError(t, err, "failed to refresh token")
+ require.NotEmpty(t, refreshed.AccessToken, "access token is empty on refresh")
+}
diff --git a/coderd/coderdtest/swagger_test.go b/coderd/coderdtest/swagger_test.go
index 07fb19ad64b85..7b50a27964631 100644
--- a/coderd/coderdtest/swagger_test.go
+++ b/coderd/coderdtest/swagger_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
)
func TestEndpointsDocumented(t *testing.T) {
diff --git a/coderd/csp.go b/coderd/csp.go
index ba87adfc635e9..84e22daf9a127 100644
--- a/coderd/csp.go
+++ b/coderd/csp.go
@@ -4,8 +4,8 @@ import (
"encoding/json"
"net/http"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
"cdr.dev/slog"
)
diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go
index 263611d5b168b..d6a5bf4b69ef0 100644
--- a/coderd/database/db2sdk/db2sdk.go
+++ b/coderd/database/db2sdk/db2sdk.go
@@ -3,14 +3,16 @@ package db2sdk
import (
"encoding/json"
+ "strings"
"github.com/google/uuid"
+ "golang.org/x/exp/slices"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/parameter"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/parameter"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
func WorkspaceBuildParameters(params []database.WorkspaceBuildParameter) []codersdk.WorkspaceBuildParameter {
@@ -29,20 +31,10 @@ func WorkspaceBuildParameter(p database.WorkspaceBuildParameter) codersdk.Worksp
}
func TemplateVersionParameter(param database.TemplateVersionParameter) (codersdk.TemplateVersionParameter, error) {
- var protoOptions []*proto.RichParameterOption
- err := json.Unmarshal(param.Options, &protoOptions)
+ options, err := templateVersionParameterOptions(param.Options)
if err != nil {
return codersdk.TemplateVersionParameter{}, err
}
- options := make([]codersdk.TemplateVersionParameterOption, 0)
- for _, option := range protoOptions {
- options = append(options, codersdk.TemplateVersionParameterOption{
- Name: option.Name,
- Description: option.Description,
- Value: option.Value,
- Icon: option.Icon,
- })
- }
descriptionPlaintext, err := parameter.Plaintext(param.Description)
if err != nil {
@@ -132,3 +124,82 @@ func Role(role rbac.Role) codersdk.Role {
Name: role.Name,
}
}
+
+func TemplateInsightsParameters(parameterRows []database.GetTemplateParameterInsightsRow) ([]codersdk.TemplateParameterUsage, error) {
+ // Use a stable sort, similarly to how we would sort in the query, note that
+ // we don't sort in the query because order varies depending on the table
+ // collation.
+ //
+ // ORDER BY utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value
+ slices.SortFunc(parameterRows, func(a, b database.GetTemplateParameterInsightsRow) int {
+ if a.Name != b.Name {
+ return strings.Compare(a.Name, b.Name)
+ }
+ if a.Type != b.Type {
+ return strings.Compare(a.Type, b.Type)
+ }
+ if a.DisplayName != b.DisplayName {
+ return strings.Compare(a.DisplayName, b.DisplayName)
+ }
+ if a.Description != b.Description {
+ return strings.Compare(a.Description, b.Description)
+ }
+ if string(a.Options) != string(b.Options) {
+ return strings.Compare(string(a.Options), string(b.Options))
+ }
+ return strings.Compare(a.Value, b.Value)
+ })
+
+ parametersUsage := []codersdk.TemplateParameterUsage{}
+ indexByNum := make(map[int64]int)
+ for _, param := range parameterRows {
+ if _, ok := indexByNum[param.Num]; !ok {
+ var opts []codersdk.TemplateVersionParameterOption
+ err := json.Unmarshal(param.Options, &opts)
+ if err != nil {
+ return nil, err
+ }
+
+ plaintextDescription, err := parameter.Plaintext(param.Description)
+ if err != nil {
+ return nil, err
+ }
+
+ parametersUsage = append(parametersUsage, codersdk.TemplateParameterUsage{
+ TemplateIDs: param.TemplateIDs,
+ Name: param.Name,
+ Type: param.Type,
+ DisplayName: param.DisplayName,
+ Description: plaintextDescription,
+ Options: opts,
+ })
+ indexByNum[param.Num] = len(parametersUsage) - 1
+ }
+
+ i := indexByNum[param.Num]
+ parametersUsage[i].Values = append(parametersUsage[i].Values, codersdk.TemplateParameterValue{
+ Value: param.Value,
+ Count: param.Count,
+ })
+ }
+
+ return parametersUsage, nil
+}
+
+func templateVersionParameterOptions(rawOptions json.RawMessage) ([]codersdk.TemplateVersionParameterOption, error) {
+ var protoOptions []*proto.RichParameterOption
+ err := json.Unmarshal(rawOptions, &protoOptions)
+ if err != nil {
+ return nil, err
+ }
+ var options []codersdk.TemplateVersionParameterOption
+ for _, option := range protoOptions {
+ options = append(options, codersdk.TemplateVersionParameterOption{
+ Name: option.Name,
+ Description: option.Description,
+ Value: option.Value,
+ Icon: option.Icon,
+ })
+ }
+ return options, nil
+}
diff --git a/coderd/database/db2sdk/db2sdk_test.go b/coderd/database/db2sdk/db2sdk_test.go
index 39020e64e9828..6cbbff7aa5b9c 100644
--- a/coderd/database/db2sdk/db2sdk_test.go
+++ b/coderd/database/db2sdk/db2sdk_test.go
@@ -9,10 +9,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
func TestProvisionerJobStatus(t *testing.T) {
diff --git a/coderd/database/db_test.go b/coderd/database/db_test.go
index c922da979f506..66ce7eb82115c 100644
--- a/coderd/database/db_test.go
+++ b/coderd/database/db_test.go
@@ -11,9 +11,9 @@ import (
"github.com/lib/pq"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/migrations"
- "github.com/coder/coder/coderd/database/postgres"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/migrations"
+ "github.com/coder/coder/v2/coderd/database/postgres"
)
func TestSerializedRetry(t *testing.T) {
diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go
index ab865e2cc0f70..9115e9b5ac184 100644
--- a/coderd/database/dbauthz/dbauthz.go
+++ b/coderd/database/dbauthz/dbauthz.go
@@ -15,9 +15,9 @@ import (
"github.com/open-policy-agent/opa/topdown"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/util/slice"
)
var _ database.Store = (*querier)(nil)
@@ -784,6 +784,14 @@ func (q *querier) GetActiveUserCount(ctx context.Context) (int64, error) {
return q.db.GetActiveUserCount(ctx)
}
+func (q *querier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]database.WorkspaceBuild, error) {
+ // This is a system-only function.
+ if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
+ return []database.WorkspaceBuild{}, err
+ }
+ return q.db.GetActiveWorkspaceBuildsByTemplateID(ctx, templateID)
+}
+
func (q *querier) GetAllTailnetAgents(ctx context.Context) ([]database.TailnetAgent, error) {
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTailnetCoordinator); err != nil {
return []database.TailnetAgent{}, err
@@ -908,11 +916,11 @@ func (q *querier) GetGroupByOrgAndName(ctx context.Context, arg database.GetGrou
return fetch(q.log, q.auth, q.db.GetGroupByOrgAndName)(ctx, arg)
}
-func (q *querier) GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([]database.User, error) {
- if _, err := q.GetGroupByID(ctx, groupID); err != nil { // AuthZ check
+func (q *querier) GetGroupMembers(ctx context.Context, id uuid.UUID) ([]database.User, error) {
+ if _, err := q.GetGroupByID(ctx, id); err != nil { // AuthZ check
return nil, err
}
- return q.db.GetGroupMembers(ctx, groupID)
+ return q.db.GetGroupMembers(ctx, id)
}
func (q *querier) GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]database.Group, error) {
@@ -1165,6 +1173,25 @@ func (q *querier) GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UU
return q.db.GetTailnetClientsForAgent(ctx, agentID)
}
+func (q *querier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) {
+ for _, templateID := range arg.TemplateIDs {
+ template, err := q.db.GetTemplateByID(ctx, templateID)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
+ return nil, err
+ }
+ }
+ if len(arg.TemplateIDs) == 0 {
+ if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
+ return nil, err
+ }
+ }
+ return q.db.GetTemplateAppInsights(ctx, arg)
+}
+
// Only used by metrics cache.
func (q *querier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) {
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
@@ -1466,13 +1493,12 @@ func (q *querier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]databas
return q.db.GetUsersByIDs(ctx, ids)
}
-// GetWorkspaceAgentByAuthToken is used in http middleware to get the workspace agent.
-// This should only be used by a system user in that middleware.
-func (q *querier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (database.WorkspaceAgent, error) {
+func (q *querier) GetWorkspaceAgentAndOwnerByAuthToken(ctx context.Context, authToken uuid.UUID) (database.GetWorkspaceAgentAndOwnerByAuthTokenRow, error) {
+ // This is a system function
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
- return database.WorkspaceAgent{}, err
+ return database.GetWorkspaceAgentAndOwnerByAuthTokenRow{}, err
}
- return q.db.GetWorkspaceAgentByAuthToken(ctx, authToken)
+ return q.db.GetWorkspaceAgentAndOwnerByAuthToken(ctx, authToken)
}
func (q *querier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) {
@@ -1853,6 +1879,13 @@ func (q *querier) InsertLicense(ctx context.Context, arg database.InsertLicenseP
return q.db.InsertLicense(ctx, arg)
}
+func (q *querier) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) {
+ if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
+ return nil, err
+ }
+ return q.db.InsertMissingGroups(ctx, arg)
+}
+
func (q *querier) InsertOrganization(ctx context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
return insert(q.log, q.auth, rbac.ResourceOrganization, q.db.InsertOrganization)(ctx, arg)
}
@@ -2016,6 +2049,14 @@ func (q *querier) InsertWorkspaceAgentStat(ctx context.Context, arg database.Ins
return q.db.InsertWorkspaceAgentStat(ctx, arg)
}
+func (q *querier) InsertWorkspaceAgentStats(ctx context.Context, arg database.InsertWorkspaceAgentStatsParams) error {
+ if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
+ return err
+ }
+
+ return q.db.InsertWorkspaceAgentStats(ctx, arg)
+}
+
func (q *querier) InsertWorkspaceApp(ctx context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) {
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
return database.WorkspaceApp{}, err
@@ -2023,6 +2064,13 @@ func (q *querier) InsertWorkspaceApp(ctx context.Context, arg database.InsertWor
return q.db.InsertWorkspaceApp(ctx, arg)
}
+func (q *querier) InsertWorkspaceAppStats(ctx context.Context, arg database.InsertWorkspaceAppStatsParams) error {
+ if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
+ return err
+ }
+ return q.db.InsertWorkspaceAppStats(ctx, arg)
+}
+
func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error {
w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
if err != nil {
@@ -2337,6 +2385,14 @@ func (q *querier) UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Conte
return q.db.UpdateTemplateVersionGitAuthProvidersByJobID(ctx, arg)
}
+func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) error {
+ fetch := func(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) (database.Template, error) {
+ return q.db.GetTemplateByID(ctx, arg.TemplateID)
+ }
+
+ return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg)
+}
+
// UpdateUserDeletedByID
// Deprecated: Delete this function in favor of 'SoftDeleteUserByID'. Deletes are
// irreversible.
@@ -2580,18 +2636,18 @@ func (q *querier) UpdateWorkspaceDeletedByID(ctx context.Context, arg database.U
return deleteQ(q.log, q.auth, fetch, q.db.UpdateWorkspaceDeletedByID)(ctx, arg)
}
-func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error {
- fetch := func(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) (database.Workspace, error) {
+func (q *querier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) {
+ fetch := func(ctx context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) {
return q.db.GetWorkspaceByID(ctx, arg.ID)
}
- return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg)
+ return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateWorkspaceDormantDeletingAt)(ctx, arg)
}
-func (q *querier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error {
- fetch := func(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) (database.Workspace, error) {
+func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error {
+ fetch := func(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) (database.Workspace, error) {
return q.db.GetWorkspaceByID(ctx, arg.ID)
}
- return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedDeletingAt)(ctx, arg)
+ return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg)
}
func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
@@ -2615,12 +2671,12 @@ func (q *querier) UpdateWorkspaceTTL(ctx context.Context, arg database.UpdateWor
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceTTL)(ctx, arg)
}
-func (q *querier) UpdateWorkspacesDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) error {
- fetch := func(ctx context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) (database.Template, error) {
+func (q *querier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error {
+ fetch := func(ctx context.Context, arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) (database.Template, error) {
return q.db.GetTemplateByID(ctx, arg.TemplateID)
}
- return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspacesDeletingAtByTemplateID)(ctx, arg)
+ return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspacesDormantDeletingAtByTemplateID)(ctx, arg)
}
func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error {
diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go
index f3313c768053f..3b41e67a0c0df 100644
--- a/coderd/database/dbauthz/dbauthz_test.go
+++ b/coderd/database/dbauthz/dbauthz_test.go
@@ -13,13 +13,13 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/util/slice"
)
func TestAsNoActor(t *testing.T) {
@@ -1064,8 +1064,10 @@ func (s *MethodTestSuite) TestWorkspace() {
res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: build.JobID})
agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID})
check.Args(database.UpdateWorkspaceAgentStartupByIDParams{
- ID: agt.ID,
- Subsystem: database.WorkspaceAgentSubsystemNone,
+ ID: agt.ID,
+ Subsystems: []database.WorkspaceAgentSubsystem{
+ database.WorkspaceAgentSubsystemEnvbox,
+ },
}).Asserts(ws, rbac.ActionUpdate).Returns()
}))
s.Run("GetWorkspaceAgentLogsAfter", s.Subtest(func(db database.Store, check *expects) {
@@ -1317,10 +1319,6 @@ func (s *MethodTestSuite) TestSystemFunctions() {
dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{})
check.Args().Asserts(rbac.ResourceSystem, rbac.ActionRead)
}))
- s.Run("GetWorkspaceAgentByAuthToken", s.Subtest(func(db database.Store, check *expects) {
- agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{})
- check.Args(agt.AuthToken).Asserts(rbac.ResourceSystem, rbac.ActionRead).Returns(agt)
- }))
s.Run("GetActiveUserCount", s.Subtest(func(db database.Store, check *expects) {
check.Args().Asserts(rbac.ResourceSystem, rbac.ActionRead).Returns(int64(0))
}))
diff --git a/coderd/database/dbauthz/setup_test.go b/coderd/database/dbauthz/setup_test.go
index d333c87362cd0..9efcf5ef9418e 100644
--- a/coderd/database/dbauthz/setup_test.go
+++ b/coderd/database/dbauthz/setup_test.go
@@ -16,14 +16,14 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbmock"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/rbac/regosql"
- "github.com/coder/coder/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbmock"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac/regosql"
+ "github.com/coder/coder/v2/coderd/util/slice"
)
var skipMethods = map[string]string{
diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go
index 0d5c19fa7656d..d26be831db122 100644
--- a/coderd/database/dbfake/dbfake.go
+++ b/coderd/database/dbfake/dbfake.go
@@ -19,13 +19,13 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/rbac/regosql"
- "github.com/coder/coder/coderd/util/slice"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac/regosql"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/codersdk"
)
var validProxyByHostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
@@ -114,33 +114,35 @@ type data struct {
userLinks []database.UserLink
// New tables
- workspaceAgentStats []database.WorkspaceAgentStat
- auditLogs []database.AuditLog
- files []database.File
- gitAuthLinks []database.GitAuthLink
- gitSSHKey []database.GitSSHKey
- groupMembers []database.GroupMember
- groups []database.Group
- licenses []database.License
- parameterSchemas []database.ParameterSchema
- provisionerDaemons []database.ProvisionerDaemon
- provisionerJobLogs []database.ProvisionerJobLog
- provisionerJobs []database.ProvisionerJob
- replicas []database.Replica
- templateVersions []database.TemplateVersionTable
- templateVersionParameters []database.TemplateVersionParameter
- templateVersionVariables []database.TemplateVersionVariable
- templates []database.TemplateTable
- workspaceAgents []database.WorkspaceAgent
- workspaceAgentMetadata []database.WorkspaceAgentMetadatum
- workspaceAgentLogs []database.WorkspaceAgentLog
- workspaceApps []database.WorkspaceApp
- workspaceBuilds []database.WorkspaceBuildTable
- workspaceBuildParameters []database.WorkspaceBuildParameter
- workspaceResourceMetadata []database.WorkspaceResourceMetadatum
- workspaceResources []database.WorkspaceResource
- workspaces []database.Workspace
- workspaceProxies []database.WorkspaceProxy
+ workspaceAgentStats []database.WorkspaceAgentStat
+ auditLogs []database.AuditLog
+ files []database.File
+ gitAuthLinks []database.GitAuthLink
+ gitSSHKey []database.GitSSHKey
+ groupMembers []database.GroupMember
+ groups []database.Group
+ licenses []database.License
+ parameterSchemas []database.ParameterSchema
+ provisionerDaemons []database.ProvisionerDaemon
+ provisionerJobLogs []database.ProvisionerJobLog
+ provisionerJobs []database.ProvisionerJob
+ replicas []database.Replica
+ templateVersions []database.TemplateVersionTable
+ templateVersionParameters []database.TemplateVersionParameter
+ templateVersionVariables []database.TemplateVersionVariable
+ templates []database.TemplateTable
+ workspaceAgents []database.WorkspaceAgent
+ workspaceAgentMetadata []database.WorkspaceAgentMetadatum
+ workspaceAgentLogs []database.WorkspaceAgentLog
+ workspaceApps []database.WorkspaceApp
+ workspaceAppStatsLastInsertID int64
+ workspaceAppStats []database.WorkspaceAppStat
+ workspaceBuilds []database.WorkspaceBuildTable
+ workspaceBuildParameters []database.WorkspaceBuildParameter
+ workspaceResourceMetadata []database.WorkspaceResourceMetadatum
+ workspaceResources []database.WorkspaceResource
+ workspaces []database.Workspace
+ workspaceProxies []database.WorkspaceProxy
// Locks is a map of lock names. Any keys within the map are currently
// locked.
locks map[int64]struct{}
@@ -339,7 +341,7 @@ func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspac
AutostartSchedule: w.AutostartSchedule,
Ttl: w.Ttl,
LastUsedAt: w.LastUsedAt,
- LockedAt: w.LockedAt,
+ DormantAt: w.DormantAt,
DeletingAt: w.DeletingAt,
Count: count,
}
@@ -547,6 +549,19 @@ func (q *FakeQuerier) getWorkspaceAgentsByResourceIDsNoLock(_ context.Context, r
return workspaceAgents, nil
}
+func (q *FakeQuerier) getWorkspaceAppByAgentIDAndSlugNoLock(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) {
+ for _, app := range q.workspaceApps {
+ if app.AgentID != arg.AgentID {
+ continue
+ }
+ if app.Slug != arg.Slug {
+ continue
+ }
+ return app, nil
+ }
+ return database.WorkspaceApp{}, sql.ErrNoRows
+}
+
func (q *FakeQuerier) getProvisionerJobByIDNoLock(_ context.Context, id uuid.UUID) (database.ProvisionerJob, error) {
for _, provisionerJob := range q.provisionerJobs {
if provisionerJob.ID != id {
@@ -605,12 +620,50 @@ func uniqueSortedUUIDs(uuids []uuid.UUID) []uuid.UUID {
for id := range set {
unique = append(unique, id)
}
- slices.SortFunc(unique, func(a, b uuid.UUID) bool {
- return a.String() < b.String()
+ slices.SortFunc(unique, func(a, b uuid.UUID) int {
+ return slice.Ascending(a.String(), b.String())
})
return unique
}
+func (q *FakeQuerier) getOrganizationMemberNoLock(orgID uuid.UUID) []database.OrganizationMember {
+ var members []database.OrganizationMember
+ for _, member := range q.organizationMembers {
+ if member.OrganizationID == orgID {
+ members = append(members, member)
+ }
+ }
+
+ return members
+}
+
+// getEveryoneGroupMembersNoLock fetches all the users in an organization.
+func (q *FakeQuerier) getEveryoneGroupMembersNoLock(orgID uuid.UUID) []database.User {
+ var (
+ everyone []database.User
+ orgMembers = q.getOrganizationMemberNoLock(orgID)
+ )
+ for _, member := range orgMembers {
+ user, err := q.getUserByIDNoLock(member.UserID)
+ if err != nil {
+ return nil
+ }
+ everyone = append(everyone, user)
+ }
+ return everyone
+}
+
+// isEveryoneGroup returns true if the provided ID matches
+// an organization ID.
+func (q *FakeQuerier) isEveryoneGroup(id uuid.UUID) bool {
+ for _, org := range q.organizations {
+ if org.ID == id {
+ return true
+ }
+ }
+ return false
+}
+
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
return xerrors.New("AcquireLock must only be called within a transaction")
}
@@ -918,6 +971,34 @@ func (q *FakeQuerier) GetActiveUserCount(_ context.Context) (int64, error) {
return active, nil
}
+func (q *FakeQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]database.WorkspaceBuild, error) {
+ workspaceIDs := func() []uuid.UUID {
+ q.mutex.RLock()
+ defer q.mutex.RUnlock()
+
+ ids := []uuid.UUID{}
+ for _, workspace := range q.workspaces {
+ if workspace.TemplateID == templateID {
+ ids = append(ids, workspace.ID)
+ }
+ }
+ return ids
+ }()
+
+ builds, err := q.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, workspaceIDs)
+ if err != nil {
+ return nil, err
+ }
+
+ filteredBuilds := []database.WorkspaceBuild{}
+ for _, build := range builds {
+ if build.Transition == database.WorkspaceTransitionStart {
+ filteredBuilds = append(filteredBuilds, build)
+ }
+ }
+ return filteredBuilds, nil
+}
+
func (*FakeQuerier) GetAllTailnetAgents(_ context.Context) ([]database.TailnetAgent, error) {
return nil, ErrUnimplemented
}
@@ -1348,13 +1429,17 @@ func (q *FakeQuerier) GetGroupByOrgAndName(_ context.Context, arg database.GetGr
return database.Group{}, sql.ErrNoRows
}
-func (q *FakeQuerier) GetGroupMembers(_ context.Context, groupID uuid.UUID) ([]database.User, error) {
+func (q *FakeQuerier) GetGroupMembers(_ context.Context, id uuid.UUID) ([]database.User, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
+ if q.isEveryoneGroup(id) {
+ return q.getEveryoneGroupMembersNoLock(id), nil
+ }
+
var members []database.GroupMember
for _, member := range q.groupMembers {
- if member.GroupID == groupID {
+ if member.GroupID == id {
members = append(members, member)
}
}
@@ -1373,14 +1458,13 @@ func (q *FakeQuerier) GetGroupMembers(_ context.Context, groupID uuid.UUID) ([]d
return users, nil
}
-func (q *FakeQuerier) GetGroupsByOrganizationID(_ context.Context, organizationID uuid.UUID) ([]database.Group, error) {
+func (q *FakeQuerier) GetGroupsByOrganizationID(_ context.Context, id uuid.UUID) ([]database.Group, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
- var groups []database.Group
+ groups := make([]database.Group, 0, len(q.groups))
for _, group := range q.groups {
- // Omit the allUsers group.
- if group.OrganizationID == organizationID && group.ID != organizationID {
+ if group.OrganizationID == id {
groups = append(groups, group)
}
}
@@ -1810,9 +1894,17 @@ func (q *FakeQuerier) GetQuotaAllowanceForUser(_ context.Context, userID uuid.UU
for _, group := range q.groups {
if group.ID == member.GroupID {
sum += int64(group.QuotaAllowance)
+ continue
}
}
}
+ // Grab the quota for the Everyone group.
+ for _, group := range q.groups {
+ if group.ID == group.OrganizationID {
+ sum += int64(group.QuotaAllowance)
+ break
+ }
+ }
return sum, nil
}
@@ -1887,6 +1979,131 @@ func (*FakeQuerier) GetTailnetClientsForAgent(context.Context, uuid.UUID) ([]dat
return nil, ErrUnimplemented
}
+func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) {
+ err := validateDatabaseType(arg)
+ if err != nil {
+ return nil, err
+ }
+
+ q.mutex.RLock()
+ defer q.mutex.RUnlock()
+
+ type appKey struct {
+ AccessMethod string
+ SlugOrPort string
+ Slug string
+ DisplayName string
+ Icon string
+ }
+ type uniqueKey struct {
+ TemplateID uuid.UUID
+ UserID uuid.UUID
+ AgentID uuid.UUID
+ AppKey appKey
+ }
+
+ appUsageIntervalsByUserAgentApp := make(map[uniqueKey]map[time.Time]int64)
+ for _, s := range q.workspaceAppStats {
+ // (was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
+ // OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
+ // OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
+ if !(((s.SessionStartedAt.After(arg.StartTime) || s.SessionStartedAt.Equal(arg.StartTime)) && s.SessionStartedAt.Before(arg.EndTime)) ||
+ (s.SessionEndedAt.After(arg.StartTime) && s.SessionEndedAt.Before(arg.EndTime)) ||
+ (s.SessionStartedAt.Before(arg.StartTime) && (s.SessionEndedAt.After(arg.EndTime) || s.SessionEndedAt.Equal(arg.EndTime)))) {
+ continue
+ }
+
+ w, err := q.getWorkspaceByIDNoLock(ctx, s.WorkspaceID)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, w.TemplateID) {
+ continue
+ }
+
+ app, _ := q.getWorkspaceAppByAgentIDAndSlugNoLock(ctx, database.GetWorkspaceAppByAgentIDAndSlugParams{
+ AgentID: s.AgentID,
+ Slug: s.SlugOrPort,
+ })
+
+ key := uniqueKey{
+ TemplateID: w.TemplateID,
+ UserID: s.UserID,
+ AgentID: s.AgentID,
+ AppKey: appKey{
+ AccessMethod: s.AccessMethod,
+ SlugOrPort: s.SlugOrPort,
+ Slug: app.Slug,
+ DisplayName: app.DisplayName,
+ Icon: app.Icon,
+ },
+ }
+ if appUsageIntervalsByUserAgentApp[key] == nil {
+ appUsageIntervalsByUserAgentApp[key] = make(map[time.Time]int64)
+ }
+
+ t := s.SessionStartedAt.Truncate(5 * time.Minute)
+ if t.Before(arg.StartTime) {
+ t = arg.StartTime
+ }
+ for t.Before(s.SessionEndedAt) && t.Before(arg.EndTime) {
+ appUsageIntervalsByUserAgentApp[key][t] = 60 // 1 minute.
+ t = t.Add(1 * time.Minute)
+ }
+ }
+
+ appUsageTemplateIDs := make(map[appKey]map[uuid.UUID]struct{})
+ appUsageUserIDs := make(map[appKey]map[uuid.UUID]struct{})
+ appUsage := make(map[appKey]int64)
+ for uniqueKey, usage := range appUsageIntervalsByUserAgentApp {
+ for _, seconds := range usage {
+ if appUsageTemplateIDs[uniqueKey.AppKey] == nil {
+ appUsageTemplateIDs[uniqueKey.AppKey] = make(map[uuid.UUID]struct{})
+ }
+ appUsageTemplateIDs[uniqueKey.AppKey][uniqueKey.TemplateID] = struct{}{}
+ if appUsageUserIDs[uniqueKey.AppKey] == nil {
+ appUsageUserIDs[uniqueKey.AppKey] = make(map[uuid.UUID]struct{})
+ }
+ appUsageUserIDs[uniqueKey.AppKey][uniqueKey.UserID] = struct{}{}
+ appUsage[uniqueKey.AppKey] += seconds
+ }
+ }
+
+ var rows []database.GetTemplateAppInsightsRow
+ for appKey, usage := range appUsage {
+ templateIDs := make([]uuid.UUID, 0, len(appUsageTemplateIDs[appKey]))
+ for templateID := range appUsageTemplateIDs[appKey] {
+ templateIDs = append(templateIDs, templateID)
+ }
+ slices.SortFunc(templateIDs, func(a, b uuid.UUID) int {
+ return slice.Ascending(a.String(), b.String())
+ })
+ activeUserIDs := make([]uuid.UUID, 0, len(appUsageUserIDs[appKey]))
+ for userID := range appUsageUserIDs[appKey] {
+ activeUserIDs = append(activeUserIDs, userID)
+ }
+ slices.SortFunc(activeUserIDs, func(a, b uuid.UUID) int {
+ return slice.Ascending(a.String(), b.String())
+ })
+
+ rows = append(rows, database.GetTemplateAppInsightsRow{
+ TemplateIDs: templateIDs,
+ ActiveUserIDs: activeUserIDs,
+ AccessMethod: appKey.AccessMethod,
+ SlugOrPort: appKey.SlugOrPort,
+ DisplayName: sql.NullString{String: appKey.DisplayName, Valid: appKey.DisplayName != ""},
+ Icon: sql.NullString{String: appKey.Icon, Valid: appKey.Icon != ""},
+ IsApp: appKey.Slug != "",
+ UsageSeconds: usage,
+ })
+ }
+
+ // NOTE(mafredri): Add sorting if we decide on how to handle PostgreSQL collations.
+ // ORDER BY access_method, slug_or_port, display_name, icon, is_app
+ return rows, nil
+}
+
func (q *FakeQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) {
if err := validateDatabaseType(arg); err != nil {
return database.GetTemplateAverageBuildTimeRow{}, err
@@ -2014,12 +2231,15 @@ func (q *FakeQuerier) GetTemplateDAUs(_ context.Context, arg database.GetTemplat
return rs, nil
}
-func (q *FakeQuerier) GetTemplateDailyInsights(_ context.Context, arg database.GetTemplateDailyInsightsParams) ([]database.GetTemplateDailyInsightsRow, error) {
+func (q *FakeQuerier) GetTemplateDailyInsights(ctx context.Context, arg database.GetTemplateDailyInsightsParams) ([]database.GetTemplateDailyInsightsRow, error) {
err := validateDatabaseType(arg)
if err != nil {
return nil, err
}
+ q.mutex.RLock()
+ defer q.mutex.RUnlock()
+
type dailyStat struct {
startTime, endTime time.Time
userSet map[uuid.UUID]struct{}
@@ -2050,7 +2270,31 @@ func (q *FakeQuerier) GetTemplateDailyInsights(_ context.Context, arg database.G
}
ds.userSet[s.UserID] = struct{}{}
ds.templateIDSet[s.TemplateID] = struct{}{}
- break
+ }
+ }
+
+ for _, s := range q.workspaceAppStats {
+ w, err := q.getWorkspaceByIDNoLock(ctx, s.WorkspaceID)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, w.TemplateID) {
+ continue
+ }
+
+ for _, ds := range dailyStats {
+ // (was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
+ // OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
+ // OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
+ if !(((s.SessionStartedAt.After(ds.startTime) || s.SessionStartedAt.Equal(ds.startTime)) && s.SessionStartedAt.Before(ds.endTime)) ||
+ (s.SessionEndedAt.After(ds.startTime) && s.SessionEndedAt.Before(ds.endTime)) ||
+ (s.SessionStartedAt.Before(ds.startTime) && (s.SessionEndedAt.After(ds.endTime) || s.SessionEndedAt.Equal(ds.endTime)))) {
+ continue
+ }
+
+ ds.userSet[s.UserID] = struct{}{}
+ ds.templateIDSet[w.TemplateID] = struct{}{}
}
}
@@ -2060,8 +2304,8 @@ func (q *FakeQuerier) GetTemplateDailyInsights(_ context.Context, arg database.G
for templateID := range ds.templateIDSet {
templateIDs = append(templateIDs, templateID)
}
- slices.SortFunc(templateIDs, func(a, b uuid.UUID) bool {
- return a.String() < b.String()
+ slices.SortFunc(templateIDs, func(a, b uuid.UUID) int {
+ return slice.Ascending(a.String(), b.String())
})
result = append(result, database.GetTemplateDailyInsightsRow{
StartTime: ds.startTime,
@@ -2096,22 +2340,22 @@ func (q *FakeQuerier) GetTemplateInsights(_ context.Context, arg database.GetTem
if appUsageIntervalsByUser[s.UserID] == nil {
appUsageIntervalsByUser[s.UserID] = make(map[time.Time]*database.GetTemplateInsightsRow)
}
- t := s.CreatedAt.Truncate(5 * time.Minute)
+ t := s.CreatedAt.Truncate(time.Minute)
if _, ok := appUsageIntervalsByUser[s.UserID][t]; !ok {
appUsageIntervalsByUser[s.UserID][t] = &database.GetTemplateInsightsRow{}
}
if s.SessionCountJetBrains > 0 {
- appUsageIntervalsByUser[s.UserID][t].UsageJetbrainsSeconds = 300
+ appUsageIntervalsByUser[s.UserID][t].UsageJetbrainsSeconds = 60
}
if s.SessionCountVSCode > 0 {
- appUsageIntervalsByUser[s.UserID][t].UsageVscodeSeconds = 300
+ appUsageIntervalsByUser[s.UserID][t].UsageVscodeSeconds = 60
}
if s.SessionCountReconnectingPTY > 0 {
- appUsageIntervalsByUser[s.UserID][t].UsageReconnectingPtySeconds = 300
+ appUsageIntervalsByUser[s.UserID][t].UsageReconnectingPtySeconds = 60
}
if s.SessionCountSSH > 0 {
- appUsageIntervalsByUser[s.UserID][t].UsageSshSeconds = 300
+ appUsageIntervalsByUser[s.UserID][t].UsageSshSeconds = 60
}
}
@@ -2119,12 +2363,17 @@ func (q *FakeQuerier) GetTemplateInsights(_ context.Context, arg database.GetTem
for templateID := range templateIDSet {
templateIDs = append(templateIDs, templateID)
}
- slices.SortFunc(templateIDs, func(a, b uuid.UUID) bool {
- return a.String() < b.String()
+ slices.SortFunc(templateIDs, func(a, b uuid.UUID) int {
+ return slice.Ascending(a.String(), b.String())
})
+ activeUserIDs := make([]uuid.UUID, 0, len(appUsageIntervalsByUser))
+ for userID := range appUsageIntervalsByUser {
+ activeUserIDs = append(activeUserIDs, userID)
+ }
+
result := database.GetTemplateInsightsRow{
- TemplateIDs: templateIDs,
- ActiveUsers: int64(len(appUsageIntervalsByUser)),
+ TemplateIDs: templateIDs,
+ ActiveUserIDs: activeUserIDs,
}
for _, intervals := range appUsageIntervalsByUser {
for _, interval := range intervals {
@@ -2180,12 +2429,14 @@ func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg data
if tvp.TemplateVersionID != tv.ID {
continue
}
- key := fmt.Sprintf("%s:%s:%s:%s", tvp.Name, tvp.DisplayName, tvp.Description, tvp.Options)
+ // GROUP BY tvp.name, tvp.type, tvp.display_name, tvp.description, tvp.options
+ key := fmt.Sprintf("%s:%s:%s:%s:%s", tvp.Name, tvp.Type, tvp.DisplayName, tvp.Description, tvp.Options)
if _, ok := uniqueTemplateParams[key]; !ok {
num++
uniqueTemplateParams[key] = &database.GetTemplateParameterInsightsRow{
Num: num,
Name: tvp.Name,
+ Type: tvp.Type,
DisplayName: tvp.DisplayName,
Description: tvp.Description,
Options: tvp.Options,
@@ -2220,6 +2471,7 @@ func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg data
TemplateIDs: uniqueSortedUUIDs(utp.TemplateIDs),
Name: utp.Name,
DisplayName: utp.DisplayName,
+ Type: utp.Type,
Description: utp.Description,
Options: utp.Options,
Value: value,
@@ -2228,6 +2480,8 @@ func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg data
}
}
+ // NOTE(mafredri): Add sorting if we decide on how to handle PostgreSQL collations.
+ // ORDER BY utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value
return rows, nil
}
@@ -2341,13 +2595,16 @@ func (q *FakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, arg dat
}
// Database orders by created_at
- slices.SortFunc(version, func(a, b database.TemplateVersion) bool {
+ slices.SortFunc(version, func(a, b database.TemplateVersion) int {
if a.CreatedAt.Equal(b.CreatedAt) {
// Technically the postgres database also orders by uuid. So match
// that behavior
- return a.ID.String() < b.ID.String()
+ return slice.Ascending(a.ID.String(), b.ID.String())
+ }
+ if a.CreatedAt.Before(b.CreatedAt) {
+ return -1
}
- return a.CreatedAt.Before(b.CreatedAt)
+ return 1
})
if arg.AfterID != uuid.Nil {
@@ -2406,11 +2663,11 @@ func (q *FakeQuerier) GetTemplates(_ context.Context) ([]database.Template, erro
defer q.mutex.RUnlock()
templates := slices.Clone(q.templates)
- slices.SortFunc(templates, func(i, j database.TemplateTable) bool {
- if i.Name != j.Name {
- return i.Name < j.Name
+ slices.SortFunc(templates, func(a, b database.TemplateTable) int {
+ if a.Name != b.Name {
+ return slice.Ascending(a.Name, b.Name)
}
- return i.ID.String() < j.ID.String()
+ return slice.Ascending(a.ID.String(), b.ID.String())
})
return q.templatesWithUserNoLock(templates), nil
@@ -2523,8 +2780,8 @@ func (q *FakeQuerier) GetUserLatencyInsights(_ context.Context, arg database.Get
for templateID := range templateIDSet {
templateIDs = append(templateIDs, templateID)
}
- slices.SortFunc(templateIDs, func(a, b uuid.UUID) bool {
- return a.String() < b.String()
+ slices.SortFunc(templateIDs, func(a, b uuid.UUID) int {
+ return slice.Ascending(a.String(), b.String())
})
user, err := q.getUserByIDNoLock(userID)
if err != nil {
@@ -2540,8 +2797,8 @@ func (q *FakeQuerier) GetUserLatencyInsights(_ context.Context, arg database.Get
}
rows = append(rows, row)
}
- slices.SortFunc(rows, func(a, b database.GetUserLatencyInsightsRow) bool {
- return a.UserID.String() < b.UserID.String()
+ slices.SortFunc(rows, func(a, b database.GetUserLatencyInsightsRow) int {
+ return slice.Ascending(a.UserID.String(), b.UserID.String())
})
return rows, nil
@@ -2588,8 +2845,8 @@ func (q *FakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
copy(users, q.users)
// Database orders by username
- slices.SortFunc(users, func(a, b database.User) bool {
- return strings.ToLower(a.Username) < strings.ToLower(b.Username)
+ slices.SortFunc(users, func(a, b database.User) int {
+ return slice.Ascending(strings.ToLower(a.Username), strings.ToLower(b.Username))
})
// Filter out deleted since they should never be returned..
@@ -2707,18 +2964,72 @@ func (q *FakeQuerier) GetUsersByIDs(_ context.Context, ids []uuid.UUID) ([]datab
return users, nil
}
-func (q *FakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken uuid.UUID) (database.WorkspaceAgent, error) {
+func (q *FakeQuerier) GetWorkspaceAgentAndOwnerByAuthToken(_ context.Context, authToken uuid.UUID) (database.GetWorkspaceAgentAndOwnerByAuthTokenRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
- // The schema sorts this by created at, so we iterate the array backwards.
- for i := len(q.workspaceAgents) - 1; i >= 0; i-- {
- agent := q.workspaceAgents[i]
- if agent.AuthToken == authToken {
- return agent, nil
+ // map of build number -> row
+ rows := make(map[int32]database.GetWorkspaceAgentAndOwnerByAuthTokenRow)
+
+ // We want to return the latest build number
+ var latestBuildNumber int32
+
+ for _, agt := range q.workspaceAgents {
+ if agt.AuthToken != authToken {
+ continue
+ }
+ // get the related workspace and user
+ for _, res := range q.workspaceResources {
+ if agt.ResourceID != res.ID {
+ continue
+ }
+ for _, build := range q.workspaceBuilds {
+ if build.JobID != res.JobID {
+ continue
+ }
+ for _, ws := range q.workspaces {
+ if build.WorkspaceID != ws.ID {
+ continue
+ }
+ var row database.GetWorkspaceAgentAndOwnerByAuthTokenRow
+ row.WorkspaceID = ws.ID
+ usr, err := q.getUserByIDNoLock(ws.OwnerID)
+ if err != nil {
+ return database.GetWorkspaceAgentAndOwnerByAuthTokenRow{}, sql.ErrNoRows
+ }
+ row.OwnerID = usr.ID
+ row.OwnerRoles = append(usr.RBACRoles, "member")
+ // We also need to get org roles for the user
+ row.OwnerName = usr.Username
+ row.WorkspaceAgent = agt
+ for _, mem := range q.organizationMembers {
+ if mem.UserID == usr.ID {
+ row.OwnerRoles = append(row.OwnerRoles, fmt.Sprintf("organization-member:%s", mem.OrganizationID.String()))
+ }
+ }
+ // And group memberships
+ for _, groupMem := range q.groupMembers {
+ if groupMem.UserID == usr.ID {
+ row.OwnerGroups = append(row.OwnerGroups, groupMem.GroupID.String())
+ }
+ }
+
+ // Keep track of the latest build number
+ rows[build.BuildNumber] = row
+ if build.BuildNumber > latestBuildNumber {
+ latestBuildNumber = build.BuildNumber
+ }
+ }
+ }
}
}
- return database.WorkspaceAgent{}, sql.ErrNoRows
+
+ if len(rows) == 0 {
+ return database.GetWorkspaceAgentAndOwnerByAuthTokenRow{}, sql.ErrNoRows
+ }
+
+ // Return the row related to the latest build
+ return rows[latestBuildNumber], nil
}
func (q *FakeQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) {
@@ -2797,21 +3108,25 @@ func (q *FakeQuerier) GetWorkspaceAgentStats(_ context.Context, createdAfter tim
agentStatsCreatedAfter := make([]database.WorkspaceAgentStat, 0)
for _, agentStat := range q.workspaceAgentStats {
- if agentStat.CreatedAt.After(createdAfter) {
+ if agentStat.CreatedAt.After(createdAfter) || agentStat.CreatedAt.Equal(createdAfter) {
agentStatsCreatedAfter = append(agentStatsCreatedAfter, agentStat)
}
}
latestAgentStats := map[uuid.UUID]database.WorkspaceAgentStat{}
for _, agentStat := range q.workspaceAgentStats {
- if agentStat.CreatedAt.After(createdAfter) {
+ if agentStat.CreatedAt.After(createdAfter) || agentStat.CreatedAt.Equal(createdAfter) {
latestAgentStats[agentStat.AgentID] = agentStat
}
}
statByAgent := map[uuid.UUID]database.GetWorkspaceAgentStatsRow{}
- for _, agentStat := range latestAgentStats {
- stat := statByAgent[agentStat.AgentID]
+ for agentID, agentStat := range latestAgentStats {
+ stat := statByAgent[agentID]
+ stat.AgentID = agentStat.AgentID
+ stat.TemplateID = agentStat.TemplateID
+ stat.UserID = agentStat.UserID
+ stat.WorkspaceID = agentStat.WorkspaceID
stat.SessionCountVSCode += agentStat.SessionCountVSCode
stat.SessionCountJetBrains += agentStat.SessionCountJetBrains
stat.SessionCountReconnectingPTY += agentStat.SessionCountReconnectingPTY
@@ -2987,7 +3302,7 @@ func (q *FakeQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.C
return agents, nil
}
-func (q *FakeQuerier) GetWorkspaceAppByAgentIDAndSlug(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) {
+func (q *FakeQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) {
if err := validateDatabaseType(arg); err != nil {
return database.WorkspaceApp{}, err
}
@@ -2995,16 +3310,7 @@ func (q *FakeQuerier) GetWorkspaceAppByAgentIDAndSlug(_ context.Context, arg dat
q.mutex.RLock()
defer q.mutex.RUnlock()
- for _, app := range q.workspaceApps {
- if app.AgentID != arg.AgentID {
- continue
- }
- if app.Slug != arg.Slug {
- continue
- }
- return app, nil
- }
- return database.WorkspaceApp{}, sql.ErrNoRows
+ return q.getWorkspaceAppByAgentIDAndSlugNoLock(ctx, arg)
}
func (q *FakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID) ([]database.WorkspaceApp, error) {
@@ -3126,9 +3432,8 @@ func (q *FakeQuerier) GetWorkspaceBuildsByWorkspaceID(_ context.Context,
}
// Order by build_number
- slices.SortFunc(history, func(a, b database.WorkspaceBuild) bool {
- // use greater than since we want descending order
- return a.BuildNumber > b.BuildNumber
+ slices.SortFunc(history, func(a, b database.WorkspaceBuild) int {
+ return slice.Descending(a.BuildNumber, b.BuildNumber)
})
if params.AfterID != uuid.Nil {
@@ -3432,14 +3737,14 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no
if build.Transition == database.WorkspaceTransitionStart &&
!build.Deadline.IsZero() &&
build.Deadline.Before(now) &&
- !workspace.LockedAt.Valid {
+ !workspace.DormantAt.Valid {
workspaces = append(workspaces, workspace)
continue
}
if build.Transition == database.WorkspaceTransitionStop &&
workspace.AutostartSchedule.Valid &&
- !workspace.LockedAt.Valid {
+ !workspace.DormantAt.Valid {
workspaces = append(workspaces, workspace)
continue
}
@@ -3457,11 +3762,11 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no
if err != nil {
return nil, xerrors.Errorf("get template by ID: %w", err)
}
- if !workspace.LockedAt.Valid && template.InactivityTTL > 0 {
+ if !workspace.DormantAt.Valid && template.TimeTilDormant > 0 {
workspaces = append(workspaces, workspace)
continue
}
- if workspace.LockedAt.Valid && template.LockedTTL > 0 {
+ if workspace.DormantAt.Valid && template.TimeTilDormantAutoDelete > 0 {
workspaces = append(workspaces, workspace)
continue
}
@@ -3510,7 +3815,7 @@ func (q *FakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP
func (q *FakeQuerier) InsertAllUsersGroup(ctx context.Context, orgID uuid.UUID) (database.Group, error) {
return q.InsertGroup(ctx, database.InsertGroupParams{
ID: orgID,
- Name: database.AllUsersGroup,
+ Name: database.EveryoneGroup,
DisplayName: "",
OrganizationID: orgID,
})
@@ -3527,8 +3832,14 @@ func (q *FakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAudit
alog := database.AuditLog(arg)
q.auditLogs = append(q.auditLogs, alog)
- slices.SortFunc(q.auditLogs, func(a, b database.AuditLog) bool {
- return a.Time.Before(b.Time)
+ slices.SortFunc(q.auditLogs, func(a, b database.AuditLog) int {
+ if a.Time.Before(b.Time) {
+ return -1
+ } else if a.Time.Equal(b.Time) {
+ return 0
+ } else {
+ return 1
+ }
})
return alog, nil
@@ -3635,6 +3946,7 @@ func (q *FakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupPar
OrganizationID: arg.OrganizationID,
AvatarURL: arg.AvatarURL,
QuotaAllowance: arg.QuotaAllowance,
+ Source: database.GroupSourceUser,
}
q.groups = append(q.groups, group)
@@ -3687,6 +3999,45 @@ func (q *FakeQuerier) InsertLicense(
return l, nil
}
+func (q *FakeQuerier) InsertMissingGroups(_ context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) {
+ err := validateDatabaseType(arg)
+ if err != nil {
+ return nil, err
+ }
+
+ groupNameMap := make(map[string]struct{})
+ for _, g := range arg.GroupNames {
+ groupNameMap[g] = struct{}{}
+ }
+
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
+
+ for _, g := range q.groups {
+ if g.OrganizationID != arg.OrganizationID {
+ continue
+ }
+ delete(groupNameMap, g.Name)
+ }
+
+ newGroups := make([]database.Group, 0, len(groupNameMap))
+ for k := range groupNameMap {
+ g := database.Group{
+ ID: uuid.New(),
+ Name: k,
+ OrganizationID: arg.OrganizationID,
+ AvatarURL: "",
+ QuotaAllowance: 0,
+ DisplayName: "",
+ Source: arg.Source,
+ }
+ q.groups = append(q.groups, g)
+ newGroups = append(newGroups, g)
+ }
+
+ return newGroups, nil
+}
+
func (q *FakeQuerier) InsertOrganization(_ context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
if err := validateDatabaseType(arg); err != nil {
return database.Organization{}, err
@@ -4177,6 +4528,49 @@ func (q *FakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.Ins
return stat, nil
}
+func (q *FakeQuerier) InsertWorkspaceAgentStats(_ context.Context, arg database.InsertWorkspaceAgentStatsParams) error {
+ err := validateDatabaseType(arg)
+ if err != nil {
+ return err
+ }
+
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
+
+ var connectionsByProto []map[string]int64
+ if err := json.Unmarshal(arg.ConnectionsByProto, &connectionsByProto); err != nil {
+ return err
+ }
+ for i := 0; i < len(arg.ID); i++ {
+ cbp, err := json.Marshal(connectionsByProto[i])
+ if err != nil {
+ return xerrors.Errorf("failed to marshal connections_by_proto: %w", err)
+ }
+ stat := database.WorkspaceAgentStat{
+ ID: arg.ID[i],
+ CreatedAt: arg.CreatedAt[i],
+ WorkspaceID: arg.WorkspaceID[i],
+ AgentID: arg.AgentID[i],
+ UserID: arg.UserID[i],
+ ConnectionsByProto: cbp,
+ ConnectionCount: arg.ConnectionCount[i],
+ RxPackets: arg.RxPackets[i],
+ RxBytes: arg.RxBytes[i],
+ TxPackets: arg.TxPackets[i],
+ TxBytes: arg.TxBytes[i],
+ TemplateID: arg.TemplateID[i],
+ SessionCountVSCode: arg.SessionCountVSCode[i],
+ SessionCountJetBrains: arg.SessionCountJetBrains[i],
+ SessionCountReconnectingPTY: arg.SessionCountReconnectingPTY[i],
+ SessionCountSSH: arg.SessionCountSSH[i],
+ ConnectionMedianLatencyMS: arg.ConnectionMedianLatencyMS[i],
+ }
+ q.workspaceAgentStats = append(q.workspaceAgentStats, stat)
+ }
+
+ return nil
+}
+
func (q *FakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) {
if err := validateDatabaseType(arg); err != nil {
return database.WorkspaceApp{}, err
@@ -4211,6 +4605,44 @@ func (q *FakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW
return workspaceApp, nil
}
+func (q *FakeQuerier) InsertWorkspaceAppStats(_ context.Context, arg database.InsertWorkspaceAppStatsParams) error {
+ err := validateDatabaseType(arg)
+ if err != nil {
+ return err
+ }
+
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
+
+InsertWorkspaceAppStatsLoop:
+ for i := 0; i < len(arg.UserID); i++ {
+ stat := database.WorkspaceAppStat{
+ ID: q.workspaceAppStatsLastInsertID + 1,
+ UserID: arg.UserID[i],
+ WorkspaceID: arg.WorkspaceID[i],
+ AgentID: arg.AgentID[i],
+ AccessMethod: arg.AccessMethod[i],
+ SlugOrPort: arg.SlugOrPort[i],
+ SessionID: arg.SessionID[i],
+ SessionStartedAt: arg.SessionStartedAt[i],
+ SessionEndedAt: arg.SessionEndedAt[i],
+ Requests: arg.Requests[i],
+ }
+ for j, s := range q.workspaceAppStats {
+ // Check unique constraint for upsert.
+ if s.UserID == stat.UserID && s.AgentID == stat.AgentID && s.SessionID == stat.SessionID {
+ q.workspaceAppStats[j].SessionEndedAt = stat.SessionEndedAt
+ q.workspaceAppStats[j].Requests = stat.Requests
+ continue InsertWorkspaceAppStatsLoop
+ }
+ }
+ q.workspaceAppStats = append(q.workspaceAppStats, stat)
+ q.workspaceAppStatsLastInsertID++
+ }
+
+ return nil
+}
+
func (q *FakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
@@ -4698,8 +5130,8 @@ func (q *FakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database
tpl.RestartRequirementDaysOfWeek = arg.RestartRequirementDaysOfWeek
tpl.RestartRequirementWeeks = arg.RestartRequirementWeeks
tpl.FailureTTL = arg.FailureTTL
- tpl.InactivityTTL = arg.InactivityTTL
- tpl.LockedTTL = arg.LockedTTL
+ tpl.TimeTilDormant = arg.TimeTilDormant
+ tpl.TimeTilDormantAutoDelete = arg.TimeTilDormantAutoDelete
q.templates[idx] = tpl
return nil
}
@@ -4769,6 +5201,26 @@ func (q *FakeQuerier) UpdateTemplateVersionGitAuthProvidersByJobID(_ context.Con
return sql.ErrNoRows
}
+func (q *FakeQuerier) UpdateTemplateWorkspacesLastUsedAt(_ context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) error {
+ err := validateDatabaseType(arg)
+ if err != nil {
+ return err
+ }
+
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
+
+ for i, ws := range q.workspaces {
+ if ws.TemplateID != arg.TemplateID {
+ continue
+ }
+ ws.LastUsedAt = arg.LastUsedAt
+ q.workspaces[i] = ws
+ }
+
+ return nil
+}
+
func (q *FakeQuerier) UpdateUserDeletedByID(_ context.Context, params database.UpdateUserDeletedByIDParams) error {
if err := validateDatabaseType(params); err != nil {
return err
@@ -5114,6 +5566,23 @@ func (q *FakeQuerier) UpdateWorkspaceAgentStartupByID(_ context.Context, arg dat
return err
}
+ if len(arg.Subsystems) > 0 {
+ seen := map[database.WorkspaceAgentSubsystem]struct{}{
+ arg.Subsystems[0]: {},
+ }
+ for i := 1; i < len(arg.Subsystems); i++ {
+ s := arg.Subsystems[i]
+ if _, ok := seen[s]; ok {
+ return xerrors.Errorf("duplicate subsystem %q", s)
+ }
+ seen[s] = struct{}{}
+
+ if arg.Subsystems[i-1] > arg.Subsystems[i] {
+ return xerrors.Errorf("subsystems not sorted: %q > %q", arg.Subsystems[i-1], arg.Subsystems[i])
+ }
+ }
+ }
+
q.mutex.Lock()
defer q.mutex.Unlock()
@@ -5124,7 +5593,7 @@ func (q *FakeQuerier) UpdateWorkspaceAgentStartupByID(_ context.Context, arg dat
agent.Version = arg.Version
agent.ExpandedDirectory = arg.ExpandedDirectory
- agent.Subsystem = arg.Subsystem
+ agent.Subsystems = arg.Subsystems
q.workspaceAgents[index] = agent
return nil
}
@@ -5230,29 +5699,9 @@ func (q *FakeQuerier) UpdateWorkspaceDeletedByID(_ context.Context, arg database
return sql.ErrNoRows
}
-func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error {
+func (q *FakeQuerier) UpdateWorkspaceDormantDeletingAt(_ context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) {
if err := validateDatabaseType(arg); err != nil {
- return err
- }
-
- q.mutex.Lock()
- defer q.mutex.Unlock()
-
- for index, workspace := range q.workspaces {
- if workspace.ID != arg.ID {
- continue
- }
- workspace.LastUsedAt = arg.LastUsedAt
- q.workspaces[index] = workspace
- return nil
- }
-
- return sql.ErrNoRows
-}
-
-func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error {
- if err := validateDatabaseType(arg); err != nil {
- return err
+ return database.Workspace{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
@@ -5260,12 +5709,12 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat
if workspace.ID != arg.ID {
continue
}
- workspace.LockedAt = arg.LockedAt
- if workspace.LockedAt.Time.IsZero() {
+ workspace.DormantAt = arg.DormantAt
+ if workspace.DormantAt.Time.IsZero() {
workspace.LastUsedAt = database.Now()
workspace.DeletingAt = sql.NullTime{}
}
- if !workspace.LockedAt.Time.IsZero() {
+ if !workspace.DormantAt.Time.IsZero() {
var template database.TemplateTable
for _, t := range q.templates {
if t.ID == workspace.TemplateID {
@@ -5274,18 +5723,38 @@ func (q *FakeQuerier) UpdateWorkspaceLockedDeletingAt(_ context.Context, arg dat
}
}
if template.ID == uuid.Nil {
- return xerrors.Errorf("unable to find workspace template")
+ return database.Workspace{}, xerrors.Errorf("unable to find workspace template")
}
- if template.LockedTTL > 0 {
+ if template.TimeTilDormantAutoDelete > 0 {
workspace.DeletingAt = sql.NullTime{
Valid: true,
- Time: workspace.LockedAt.Time.Add(time.Duration(template.LockedTTL)),
+ Time: workspace.DormantAt.Time.Add(time.Duration(template.TimeTilDormantAutoDelete)),
}
}
}
q.workspaces[index] = workspace
+ return workspace, nil
+ }
+ return database.Workspace{}, sql.ErrNoRows
+}
+
+func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error {
+ if err := validateDatabaseType(arg); err != nil {
+ return err
+ }
+
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
+
+ for index, workspace := range q.workspaces {
+ if workspace.ID != arg.ID {
+ continue
+ }
+ workspace.LastUsedAt = arg.LastUsedAt
+ q.workspaces[index] = workspace
return nil
}
+
return sql.ErrNoRows
}
@@ -5349,7 +5818,7 @@ func (q *FakeQuerier) UpdateWorkspaceTTL(_ context.Context, arg database.UpdateW
return sql.ErrNoRows
}
-func (q *FakeQuerier) UpdateWorkspacesDeletingAtByTemplateID(_ context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) error {
+func (q *FakeQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(_ context.Context, arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@@ -5359,14 +5828,26 @@ func (q *FakeQuerier) UpdateWorkspacesDeletingAtByTemplateID(_ context.Context,
}
for i, ws := range q.workspaces {
- if ws.LockedAt.Time.IsZero() {
+ if ws.TemplateID != arg.TemplateID {
+ continue
+ }
+
+ if ws.DormantAt.Time.IsZero() {
continue
}
+
+ if !arg.DormantAt.IsZero() {
+ ws.DormantAt = sql.NullTime{
+ Valid: true,
+ Time: arg.DormantAt,
+ }
+ }
+
deletingAt := sql.NullTime{
- Valid: arg.LockedTtlMs > 0,
+ Valid: arg.TimeTilDormantAutodeleteMs > 0,
}
- if arg.LockedTtlMs > 0 {
- deletingAt.Time = ws.LockedAt.Time.Add(time.Duration(arg.LockedTtlMs) * time.Millisecond)
+ if arg.TimeTilDormantAutodeleteMs > 0 {
+ deletingAt.Time = ws.DormantAt.Time.Add(time.Duration(arg.TimeTilDormantAutodeleteMs) * time.Millisecond)
}
ws.DeletingAt = deletingAt
q.workspaces[i] = ws
@@ -5482,11 +5963,11 @@ func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.G
templates = append(templates, template)
}
if len(templates) > 0 {
- slices.SortFunc(templates, func(i, j database.Template) bool {
- if i.Name != j.Name {
- return i.Name < j.Name
+ slices.SortFunc(templates, func(a, b database.Template) int {
+ if a.Name != b.Name {
+ return slice.Ascending(a.Name, b.Name)
}
- return i.ID.String() < j.ID.String()
+ return slice.Ascending(a.ID.String(), b.ID.String())
})
return templates, nil
}
@@ -5572,7 +6053,6 @@ func (q *FakeQuerier) GetTemplateUserRoles(_ context.Context, id uuid.UUID) ([]d
return users, nil
}
-//nolint:gocyclo
func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesRow, error) {
if err := validateDatabaseType(arg); err != nil {
return nil, err
@@ -5617,6 +6097,18 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
continue
}
+ if !arg.LastUsedBefore.IsZero() {
+ if workspace.LastUsedAt.After(arg.LastUsedBefore) {
+ continue
+ }
+ }
+
+ if !arg.LastUsedAfter.IsZero() {
+ if workspace.LastUsedAt.Before(arg.LastUsedAfter) {
+ continue
+ }
+ }
+
if arg.Status != "" {
build, err := q.getLatestWorkspaceBuildByWorkspaceIDNoLock(ctx, workspace.ID)
if err != nil {
@@ -5730,6 +6222,16 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
}
}
+ // We omit locked workspaces by default.
+ if arg.DormantAt.IsZero() && workspace.DormantAt.Valid {
+ continue
+ }
+
+ // Filter out workspaces that are locked after the timestamp.
+ if !arg.DormantAt.IsZero() && workspace.DormantAt.Time.Before(arg.DormantAt) {
+ continue
+ }
+
if len(arg.TemplateIDs) > 0 {
match := false
for _, id := range arg.TemplateIDs {
diff --git a/coderd/database/dbfake/dbfake_test.go b/coderd/database/dbfake/dbfake_test.go
index 445ba6be8ec49..84d3ad39e1200 100644
--- a/coderd/database/dbfake/dbfake_test.go
+++ b/coderd/database/dbfake/dbfake_test.go
@@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
)
// test that transactions don't deadlock, and that we don't see intermediate state.
diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go
index 3e38f5c4561c9..2c3088b9be3b0 100644
--- a/coderd/database/dbgen/dbgen.go
+++ b/coderd/database/dbgen/dbgen.go
@@ -16,10 +16,10 @@ import (
"github.com/sqlc-dev/pqtype"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/cryptorand"
)
// All methods take in a 'seed' object. Any provided fields in the seed will be
@@ -317,8 +317,9 @@ func ProvisionerJob(t testing.TB, db database.Store, orig database.ProvisionerJo
// Make sure when we acquire the job, we only get this one.
orig.Tags[id.String()] = "true"
}
+ jobID := takeFirst(orig.ID, uuid.New())
job, err := db.InsertProvisionerJob(genCtx, database.InsertProvisionerJobParams{
- ID: takeFirst(orig.ID, uuid.New()),
+ ID: jobID,
CreatedAt: takeFirst(orig.CreatedAt, database.Now()),
UpdatedAt: takeFirst(orig.UpdatedAt, database.Now()),
OrganizationID: takeFirst(orig.OrganizationID, uuid.New()),
@@ -343,7 +344,7 @@ func ProvisionerJob(t testing.TB, db database.Store, orig database.ProvisionerJo
if !orig.CompletedAt.Time.IsZero() || orig.Error.String != "" {
err := db.UpdateProvisionerJobWithCompleteByID(genCtx, database.UpdateProvisionerJobWithCompleteByIDParams{
- ID: job.ID,
+ ID: jobID,
UpdatedAt: job.UpdatedAt,
CompletedAt: orig.CompletedAt,
Error: orig.Error,
@@ -353,14 +354,14 @@ func ProvisionerJob(t testing.TB, db database.Store, orig database.ProvisionerJo
}
if !orig.CanceledAt.Time.IsZero() {
err := db.UpdateProvisionerJobWithCancelByID(genCtx, database.UpdateProvisionerJobWithCancelByIDParams{
- ID: job.ID,
+ ID: jobID,
CanceledAt: orig.CanceledAt,
CompletedAt: orig.CompletedAt,
})
require.NoError(t, err)
}
- job, err = db.GetProvisionerJobByID(genCtx, job.ID)
+ job, err = db.GetProvisionerJobByID(genCtx, jobID)
require.NoError(t, err)
return job
diff --git a/coderd/database/dbgen/dbgen_test.go b/coderd/database/dbgen/dbgen_test.go
index 5509455f7a586..de403d23f49c0 100644
--- a/coderd/database/dbgen/dbgen_test.go
+++ b/coderd/database/dbgen/dbgen_test.go
@@ -7,9 +7,9 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
)
func TestGenerator(t *testing.T) {
diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go
index f78d9b44a46c0..8526eb4da1078 100644
--- a/coderd/database/dbmetrics/dbmetrics.go
+++ b/coderd/database/dbmetrics/dbmetrics.go
@@ -12,8 +12,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/exp/slices"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/rbac"
)
var (
@@ -237,6 +237,13 @@ func (m metricsStore) GetActiveUserCount(ctx context.Context) (int64, error) {
return count, err
}
+func (m metricsStore) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]database.WorkspaceBuild, error) {
+ start := time.Now()
+ r0, r1 := m.s.GetActiveWorkspaceBuildsByTemplateID(ctx, templateID)
+ m.queryLatencies.WithLabelValues("GetActiveWorkspaceBuildsByTemplateID").Observe(time.Since(start).Seconds())
+ return r0, r1
+}
+
func (m metricsStore) GetAllTailnetAgents(ctx context.Context) ([]database.TailnetAgent, error) {
start := time.Now()
r0, r1 := m.s.GetAllTailnetAgents(ctx)
@@ -592,6 +599,13 @@ func (m metricsStore) GetTailnetClientsForAgent(ctx context.Context, agentID uui
return m.s.GetTailnetClientsForAgent(ctx, agentID)
}
+func (m metricsStore) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) {
+ start := time.Now()
+ r0, r1 := m.s.GetTemplateAppInsights(ctx, arg)
+ m.queryLatencies.WithLabelValues("GetTemplateAppInsights").Observe(time.Since(start).Seconds())
+ return r0, r1
+}
+
func (m metricsStore) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) {
start := time.Now()
buildTime, err := m.s.GetTemplateAverageBuildTime(ctx, arg)
@@ -774,11 +788,11 @@ func (m metricsStore) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]dat
return users, err
}
-func (m metricsStore) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (database.WorkspaceAgent, error) {
+func (m metricsStore) GetWorkspaceAgentAndOwnerByAuthToken(ctx context.Context, authToken uuid.UUID) (database.GetWorkspaceAgentAndOwnerByAuthTokenRow, error) {
start := time.Now()
- agent, err := m.s.GetWorkspaceAgentByAuthToken(ctx, authToken)
- m.queryLatencies.WithLabelValues("GetWorkspaceAgentByAuthToken").Observe(time.Since(start).Seconds())
- return agent, err
+ r0, r1 := m.s.GetWorkspaceAgentAndOwnerByAuthToken(ctx, authToken)
+ m.queryLatencies.WithLabelValues("GetWorkspaceAgentAndOwnerByAuthToken").Observe(time.Since(start).Seconds())
+ return r0, r1
}
func (m metricsStore) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (database.WorkspaceAgent, error) {
@@ -1110,6 +1124,13 @@ func (m metricsStore) InsertLicense(ctx context.Context, arg database.InsertLice
return license, err
}
+func (m metricsStore) InsertMissingGroups(ctx context.Context, arg database.InsertMissingGroupsParams) ([]database.Group, error) {
+ start := time.Now()
+ r0, r1 := m.s.InsertMissingGroups(ctx, arg)
+ m.queryLatencies.WithLabelValues("InsertMissingGroups").Observe(time.Since(start).Seconds())
+ return r0, r1
+}
+
func (m metricsStore) InsertOrganization(ctx context.Context, arg database.InsertOrganizationParams) (database.Organization, error) {
start := time.Now()
organization, err := m.s.InsertOrganization(ctx, arg)
@@ -1236,6 +1257,13 @@ func (m metricsStore) InsertWorkspaceAgentStat(ctx context.Context, arg database
return stat, err
}
+func (m metricsStore) InsertWorkspaceAgentStats(ctx context.Context, arg database.InsertWorkspaceAgentStatsParams) error {
+ start := time.Now()
+ r0 := m.s.InsertWorkspaceAgentStats(ctx, arg)
+ m.queryLatencies.WithLabelValues("InsertWorkspaceAgentStats").Observe(time.Since(start).Seconds())
+ return r0
+}
+
func (m metricsStore) InsertWorkspaceApp(ctx context.Context, arg database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) {
start := time.Now()
app, err := m.s.InsertWorkspaceApp(ctx, arg)
@@ -1243,6 +1271,13 @@ func (m metricsStore) InsertWorkspaceApp(ctx context.Context, arg database.Inser
return app, err
}
+func (m metricsStore) InsertWorkspaceAppStats(ctx context.Context, arg database.InsertWorkspaceAppStatsParams) error {
+ start := time.Now()
+ r0 := m.s.InsertWorkspaceAppStats(ctx, arg)
+ m.queryLatencies.WithLabelValues("InsertWorkspaceAppStats").Observe(time.Since(start).Seconds())
+ return r0
+}
+
func (m metricsStore) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error {
start := time.Now()
err := m.s.InsertWorkspaceBuild(ctx, arg)
@@ -1418,6 +1453,13 @@ func (m metricsStore) UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.C
return err
}
+func (m metricsStore) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg database.UpdateTemplateWorkspacesLastUsedAtParams) error {
+ start := time.Now()
+ r0 := m.s.UpdateTemplateWorkspacesLastUsedAt(ctx, arg)
+ m.queryLatencies.WithLabelValues("UpdateTemplateWorkspacesLastUsedAt").Observe(time.Since(start).Seconds())
+ return r0
+}
+
func (m metricsStore) UpdateUserDeletedByID(ctx context.Context, arg database.UpdateUserDeletedByIDParams) error {
start := time.Now()
err := m.s.UpdateUserDeletedByID(ctx, arg)
@@ -1565,6 +1607,13 @@ func (m metricsStore) UpdateWorkspaceDeletedByID(ctx context.Context, arg databa
return err
}
+func (m metricsStore) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) {
+ start := time.Now()
+ ws, r0 := m.s.UpdateWorkspaceDormantDeletingAt(ctx, arg)
+ m.queryLatencies.WithLabelValues("UpdateWorkspaceDormantDeletingAt").Observe(time.Since(start).Seconds())
+ return ws, r0
+}
+
func (m metricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.UpdateWorkspaceLastUsedAtParams) error {
start := time.Now()
err := m.s.UpdateWorkspaceLastUsedAt(ctx, arg)
@@ -1572,13 +1621,6 @@ func (m metricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg databas
return err
}
-func (m metricsStore) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg database.UpdateWorkspaceLockedDeletingAtParams) error {
- start := time.Now()
- r0 := m.s.UpdateWorkspaceLockedDeletingAt(ctx, arg)
- m.queryLatencies.WithLabelValues("UpdateWorkspaceLockedDeletingAt").Observe(time.Since(start).Seconds())
- return r0
-}
-
func (m metricsStore) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
start := time.Now()
proxy, err := m.s.UpdateWorkspaceProxy(ctx, arg)
@@ -1600,10 +1642,10 @@ func (m metricsStore) UpdateWorkspaceTTL(ctx context.Context, arg database.Updat
return r0
}
-func (m metricsStore) UpdateWorkspacesDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDeletingAtByTemplateIDParams) error {
+func (m metricsStore) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error {
start := time.Now()
- r0 := m.s.UpdateWorkspacesDeletingAtByTemplateID(ctx, arg)
- m.queryLatencies.WithLabelValues("UpdateWorkspacesDeletingAtByTemplateID").Observe(time.Since(start).Seconds())
+ r0 := m.s.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx, arg)
+ m.queryLatencies.WithLabelValues("UpdateWorkspacesDormantDeletingAtByTemplateID").Observe(time.Since(start).Seconds())
return r0
}
diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go
index adf573cb99e82..b0ae7955a458d 100644
--- a/coderd/database/dbmock/dbmock.go
+++ b/coderd/database/dbmock/dbmock.go
@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/coder/coder/coderd/database (interfaces: Store)
+// Source: github.com/coder/coder/v2/coderd/database (interfaces: Store)
// Package dbmock is a generated GoMock package.
package dbmock
@@ -10,8 +10,8 @@ import (
reflect "reflect"
time "time"
- database "github.com/coder/coder/coderd/database"
- rbac "github.com/coder/coder/coderd/rbac"
+ database "github.com/coder/coder/v2/coderd/database"
+ rbac "github.com/coder/coder/v2/coderd/rbac"
gomock "github.com/golang/mock/gomock"
uuid "github.com/google/uuid"
)
@@ -371,6 +371,21 @@ func (mr *MockStoreMockRecorder) GetActiveUserCount(arg0 interface{}) *gomock.Ca
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveUserCount", reflect.TypeOf((*MockStore)(nil).GetActiveUserCount), arg0)
}
+// GetActiveWorkspaceBuildsByTemplateID mocks base method.
+func (m *MockStore) GetActiveWorkspaceBuildsByTemplateID(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceBuild, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetActiveWorkspaceBuildsByTemplateID", arg0, arg1)
+ ret0, _ := ret[0].([]database.WorkspaceBuild)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetActiveWorkspaceBuildsByTemplateID indicates an expected call of GetActiveWorkspaceBuildsByTemplateID.
+func (mr *MockStoreMockRecorder) GetActiveWorkspaceBuildsByTemplateID(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveWorkspaceBuildsByTemplateID", reflect.TypeOf((*MockStore)(nil).GetActiveWorkspaceBuildsByTemplateID), arg0, arg1)
+}
+
// GetAllTailnetAgents mocks base method.
func (m *MockStore) GetAllTailnetAgents(arg0 context.Context) ([]database.TailnetAgent, error) {
m.ctrl.T.Helper()
@@ -1181,6 +1196,21 @@ func (mr *MockStoreMockRecorder) GetTailnetClientsForAgent(arg0, arg1 interface{
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetClientsForAgent", reflect.TypeOf((*MockStore)(nil).GetTailnetClientsForAgent), arg0, arg1)
}
+// GetTemplateAppInsights mocks base method.
+func (m *MockStore) GetTemplateAppInsights(arg0 context.Context, arg1 database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetTemplateAppInsights", arg0, arg1)
+ ret0, _ := ret[0].([]database.GetTemplateAppInsightsRow)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetTemplateAppInsights indicates an expected call of GetTemplateAppInsights.
+func (mr *MockStoreMockRecorder) GetTemplateAppInsights(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateAppInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateAppInsights), arg0, arg1)
+}
+
// GetTemplateAverageBuildTime mocks base method.
func (m *MockStore) GetTemplateAverageBuildTime(arg0 context.Context, arg1 database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) {
m.ctrl.T.Helper()
@@ -1601,19 +1631,19 @@ func (mr *MockStoreMockRecorder) GetUsersByIDs(arg0, arg1 interface{}) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByIDs", reflect.TypeOf((*MockStore)(nil).GetUsersByIDs), arg0, arg1)
}
-// GetWorkspaceAgentByAuthToken mocks base method.
-func (m *MockStore) GetWorkspaceAgentByAuthToken(arg0 context.Context, arg1 uuid.UUID) (database.WorkspaceAgent, error) {
+// GetWorkspaceAgentAndOwnerByAuthToken mocks base method.
+func (m *MockStore) GetWorkspaceAgentAndOwnerByAuthToken(arg0 context.Context, arg1 uuid.UUID) (database.GetWorkspaceAgentAndOwnerByAuthTokenRow, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetWorkspaceAgentByAuthToken", arg0, arg1)
- ret0, _ := ret[0].(database.WorkspaceAgent)
+ ret := m.ctrl.Call(m, "GetWorkspaceAgentAndOwnerByAuthToken", arg0, arg1)
+ ret0, _ := ret[0].(database.GetWorkspaceAgentAndOwnerByAuthTokenRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// GetWorkspaceAgentByAuthToken indicates an expected call of GetWorkspaceAgentByAuthToken.
-func (mr *MockStoreMockRecorder) GetWorkspaceAgentByAuthToken(arg0, arg1 interface{}) *gomock.Call {
+// GetWorkspaceAgentAndOwnerByAuthToken indicates an expected call of GetWorkspaceAgentAndOwnerByAuthToken.
+func (mr *MockStoreMockRecorder) GetWorkspaceAgentAndOwnerByAuthToken(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentByAuthToken", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentByAuthToken), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentAndOwnerByAuthToken", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentAndOwnerByAuthToken), arg0, arg1)
}
// GetWorkspaceAgentByID mocks base method.
@@ -2332,6 +2362,21 @@ func (mr *MockStoreMockRecorder) InsertLicense(arg0, arg1 interface{}) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertLicense", reflect.TypeOf((*MockStore)(nil).InsertLicense), arg0, arg1)
}
+// InsertMissingGroups mocks base method.
+func (m *MockStore) InsertMissingGroups(arg0 context.Context, arg1 database.InsertMissingGroupsParams) ([]database.Group, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "InsertMissingGroups", arg0, arg1)
+ ret0, _ := ret[0].([]database.Group)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// InsertMissingGroups indicates an expected call of InsertMissingGroups.
+func (mr *MockStoreMockRecorder) InsertMissingGroups(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertMissingGroups", reflect.TypeOf((*MockStore)(nil).InsertMissingGroups), arg0, arg1)
+}
+
// InsertOrganization mocks base method.
func (m *MockStore) InsertOrganization(arg0 context.Context, arg1 database.InsertOrganizationParams) (database.Organization, error) {
m.ctrl.T.Helper()
@@ -2598,6 +2643,20 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStat(arg0, arg1 interface{}
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentStat", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentStat), arg0, arg1)
}
+// InsertWorkspaceAgentStats mocks base method.
+func (m *MockStore) InsertWorkspaceAgentStats(arg0 context.Context, arg1 database.InsertWorkspaceAgentStatsParams) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "InsertWorkspaceAgentStats", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// InsertWorkspaceAgentStats indicates an expected call of InsertWorkspaceAgentStats.
+func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStats(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentStats", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentStats), arg0, arg1)
+}
+
// InsertWorkspaceApp mocks base method.
func (m *MockStore) InsertWorkspaceApp(arg0 context.Context, arg1 database.InsertWorkspaceAppParams) (database.WorkspaceApp, error) {
m.ctrl.T.Helper()
@@ -2613,6 +2672,20 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceApp(arg0, arg1 interface{}) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceApp", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceApp), arg0, arg1)
}
+// InsertWorkspaceAppStats mocks base method.
+func (m *MockStore) InsertWorkspaceAppStats(arg0 context.Context, arg1 database.InsertWorkspaceAppStatsParams) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "InsertWorkspaceAppStats", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// InsertWorkspaceAppStats indicates an expected call of InsertWorkspaceAppStats.
+func (mr *MockStoreMockRecorder) InsertWorkspaceAppStats(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAppStats", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAppStats), arg0, arg1)
+}
+
// InsertWorkspaceBuild mocks base method.
func (m *MockStore) InsertWorkspaceBuild(arg0 context.Context, arg1 database.InsertWorkspaceBuildParams) error {
m.ctrl.T.Helper()
@@ -2989,6 +3062,20 @@ func (mr *MockStoreMockRecorder) UpdateTemplateVersionGitAuthProvidersByJobID(ar
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateVersionGitAuthProvidersByJobID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateVersionGitAuthProvidersByJobID), arg0, arg1)
}
+// UpdateTemplateWorkspacesLastUsedAt mocks base method.
+func (m *MockStore) UpdateTemplateWorkspacesLastUsedAt(arg0 context.Context, arg1 database.UpdateTemplateWorkspacesLastUsedAtParams) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateTemplateWorkspacesLastUsedAt", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpdateTemplateWorkspacesLastUsedAt indicates an expected call of UpdateTemplateWorkspacesLastUsedAt.
+func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(arg0, arg1 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateWorkspacesLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateTemplateWorkspacesLastUsedAt), arg0, arg1)
+}
+
// UpdateUserDeletedByID mocks base method.
func (m *MockStore) UpdateUserDeletedByID(arg0 context.Context, arg1 database.UpdateUserDeletedByIDParams) error {
m.ctrl.T.Helper()
@@ -3292,32 +3379,33 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceDeletedByID(arg0, arg1 interface
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceDeletedByID), arg0, arg1)
}
-// UpdateWorkspaceLastUsedAt mocks base method.
-func (m *MockStore) UpdateWorkspaceLastUsedAt(arg0 context.Context, arg1 database.UpdateWorkspaceLastUsedAtParams) error {
+// UpdateWorkspaceDormantDeletingAt mocks base method.
+func (m *MockStore) UpdateWorkspaceDormantDeletingAt(arg0 context.Context, arg1 database.UpdateWorkspaceDormantDeletingAtParams) (database.Workspace, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateWorkspaceLastUsedAt", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
+ ret := m.ctrl.Call(m, "UpdateWorkspaceDormantDeletingAt", arg0, arg1)
+ ret0, _ := ret[0].(database.Workspace)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
}
-// UpdateWorkspaceLastUsedAt indicates an expected call of UpdateWorkspaceLastUsedAt.
-func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 interface{}) *gomock.Call {
+// UpdateWorkspaceDormantDeletingAt indicates an expected call of UpdateWorkspaceDormantDeletingAt.
+func (mr *MockStoreMockRecorder) UpdateWorkspaceDormantDeletingAt(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLastUsedAt), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceDormantDeletingAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceDormantDeletingAt), arg0, arg1)
}
-// UpdateWorkspaceLockedDeletingAt mocks base method.
-func (m *MockStore) UpdateWorkspaceLockedDeletingAt(arg0 context.Context, arg1 database.UpdateWorkspaceLockedDeletingAtParams) error {
+// UpdateWorkspaceLastUsedAt mocks base method.
+func (m *MockStore) UpdateWorkspaceLastUsedAt(arg0 context.Context, arg1 database.UpdateWorkspaceLastUsedAtParams) error {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateWorkspaceLockedDeletingAt", arg0, arg1)
+ ret := m.ctrl.Call(m, "UpdateWorkspaceLastUsedAt", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
-// UpdateWorkspaceLockedDeletingAt indicates an expected call of UpdateWorkspaceLockedDeletingAt.
-func (mr *MockStoreMockRecorder) UpdateWorkspaceLockedDeletingAt(arg0, arg1 interface{}) *gomock.Call {
+// UpdateWorkspaceLastUsedAt indicates an expected call of UpdateWorkspaceLastUsedAt.
+func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLockedDeletingAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLockedDeletingAt), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLastUsedAt), arg0, arg1)
}
// UpdateWorkspaceProxy mocks base method.
@@ -3363,18 +3451,18 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceTTL(arg0, arg1 interface{}) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceTTL", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceTTL), arg0, arg1)
}
-// UpdateWorkspacesDeletingAtByTemplateID mocks base method.
-func (m *MockStore) UpdateWorkspacesDeletingAtByTemplateID(arg0 context.Context, arg1 database.UpdateWorkspacesDeletingAtByTemplateIDParams) error {
+// UpdateWorkspacesDormantDeletingAtByTemplateID mocks base method.
+func (m *MockStore) UpdateWorkspacesDormantDeletingAtByTemplateID(arg0 context.Context, arg1 database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateWorkspacesDeletingAtByTemplateID", arg0, arg1)
+ ret := m.ctrl.Call(m, "UpdateWorkspacesDormantDeletingAtByTemplateID", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
-// UpdateWorkspacesDeletingAtByTemplateID indicates an expected call of UpdateWorkspacesDeletingAtByTemplateID.
-func (mr *MockStoreMockRecorder) UpdateWorkspacesDeletingAtByTemplateID(arg0, arg1 interface{}) *gomock.Call {
+// UpdateWorkspacesDormantDeletingAtByTemplateID indicates an expected call of UpdateWorkspacesDormantDeletingAtByTemplateID.
+func (mr *MockStoreMockRecorder) UpdateWorkspacesDormantDeletingAtByTemplateID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesDeletingAtByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesDeletingAtByTemplateID), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesDormantDeletingAtByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesDormantDeletingAtByTemplateID), arg0, arg1)
}
// UpsertAppSecurityKey mocks base method.
diff --git a/coderd/database/dbmock/doc.go b/coderd/database/dbmock/doc.go
index 2199de635b86b..9d06ed8a0dbf1 100644
--- a/coderd/database/dbmock/doc.go
+++ b/coderd/database/dbmock/doc.go
@@ -1,4 +1,4 @@
// package dbmock contains a mocked implementation of the database.Store interface for use in tests
package dbmock
-//go:generate mockgen -destination ./dbmock.go -package dbmock github.com/coder/coder/coderd/database Store
+//go:generate mockgen -destination ./dbmock.go -package dbmock github.com/coder/coder/v2/coderd/database Store
diff --git a/coderd/database/dbpurge/dbpurge.go b/coderd/database/dbpurge/dbpurge.go
index de7c2db67ef4b..b1062eee312ed 100644
--- a/coderd/database/dbpurge/dbpurge.go
+++ b/coderd/database/dbpurge/dbpurge.go
@@ -9,8 +9,8 @@ import (
"golang.org/x/sync/errgroup"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
)
const (
diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go
index bc51f15b451da..f83d1b81a1d2a 100644
--- a/coderd/database/dbpurge/dbpurge_test.go
+++ b/coderd/database/dbpurge/dbpurge_test.go
@@ -9,8 +9,8 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbpurge"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbpurge"
)
func TestMain(m *testing.M) {
diff --git a/coderd/database/dbtestutil/db.go b/coderd/database/dbtestutil/db.go
index ad8cecf143240..00eae9dd11218 100644
--- a/coderd/database/dbtestutil/db.go
+++ b/coderd/database/dbtestutil/db.go
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/postgres"
- "github.com/coder/coder/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/postgres"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
)
// WillUsePostgres returns true if a call to NewDB() will return a real, postgres-backed Store and Pubsub.
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index f121fccf8cebb..a2767c9cfd5e1 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -31,6 +31,11 @@ CREATE TYPE build_reason AS ENUM (
'autodelete'
);
+CREATE TYPE group_source AS ENUM (
+ 'user',
+ 'oidc'
+);
+
CREATE TYPE log_level AS ENUM (
'trace',
'debug',
@@ -143,7 +148,8 @@ CREATE TYPE workspace_agent_log_source AS ENUM (
CREATE TYPE workspace_agent_subsystem AS ENUM (
'envbuilder',
'envbox',
- 'none'
+ 'none',
+ 'exectrace'
);
CREATE TYPE workspace_app_health AS ENUM (
@@ -299,11 +305,14 @@ CREATE TABLE groups (
organization_id uuid NOT NULL,
avatar_url text DEFAULT ''::text NOT NULL,
quota_allowance integer DEFAULT 0 NOT NULL,
- display_name text DEFAULT ''::text NOT NULL
+ display_name text DEFAULT ''::text NOT NULL,
+ source group_source DEFAULT 'user'::group_source NOT NULL
);
COMMENT ON COLUMN groups.display_name IS 'Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string.';
+COMMENT ON COLUMN groups.source IS 'Source indicates how the group was created. It can be created by a user manually, or through some system process like OIDC group sync.';
+
CREATE TABLE licenses (
id integer NOT NULL,
uploaded_at timestamp with time zone NOT NULL,
@@ -626,8 +635,8 @@ CREATE TABLE templates (
allow_user_autostart boolean DEFAULT true NOT NULL,
allow_user_autostop boolean DEFAULT true NOT NULL,
failure_ttl bigint DEFAULT 0 NOT NULL,
- inactivity_ttl bigint DEFAULT 0 NOT NULL,
- locked_ttl bigint DEFAULT 0 NOT NULL,
+ time_til_dormant bigint DEFAULT 0 NOT NULL,
+ time_til_dormant_autodelete bigint DEFAULT 0 NOT NULL,
restart_requirement_days_of_week smallint DEFAULT 0 NOT NULL,
restart_requirement_weeks bigint DEFAULT 0 NOT NULL
);
@@ -667,8 +676,8 @@ CREATE VIEW template_with_users AS
templates.allow_user_autostart,
templates.allow_user_autostop,
templates.failure_ttl,
- templates.inactivity_ttl,
- templates.locked_ttl,
+ templates.time_til_dormant,
+ templates.time_til_dormant_autodelete,
templates.restart_requirement_days_of_week,
templates.restart_requirement_weeks,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
@@ -767,11 +776,12 @@ CREATE TABLE workspace_agents (
shutdown_script_timeout_seconds integer DEFAULT 0 NOT NULL,
logs_length integer DEFAULT 0 NOT NULL,
logs_overflowed boolean DEFAULT false NOT NULL,
- subsystem workspace_agent_subsystem DEFAULT 'none'::workspace_agent_subsystem NOT NULL,
startup_script_behavior startup_script_behavior DEFAULT 'non-blocking'::startup_script_behavior NOT NULL,
started_at timestamp with time zone,
ready_at timestamp with time zone,
- CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576))
+ subsystems workspace_agent_subsystem[] DEFAULT '{}'::workspace_agent_subsystem[],
+ CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576)),
+ CONSTRAINT subsystems_not_none CHECK ((NOT ('none'::workspace_agent_subsystem = ANY (subsystems))))
);
COMMENT ON COLUMN workspace_agents.version IS 'Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.';
@@ -802,6 +812,50 @@ COMMENT ON COLUMN workspace_agents.started_at IS 'The time the agent entered the
COMMENT ON COLUMN workspace_agents.ready_at IS 'The time the agent entered the ready or start_error lifecycle state';
+CREATE TABLE workspace_app_stats (
+ id bigint NOT NULL,
+ user_id uuid NOT NULL,
+ workspace_id uuid NOT NULL,
+ agent_id uuid NOT NULL,
+ access_method text NOT NULL,
+ slug_or_port text NOT NULL,
+ session_id uuid NOT NULL,
+ session_started_at timestamp with time zone NOT NULL,
+ session_ended_at timestamp with time zone NOT NULL,
+ requests integer NOT NULL
+);
+
+COMMENT ON TABLE workspace_app_stats IS 'A record of workspace app usage statistics';
+
+COMMENT ON COLUMN workspace_app_stats.id IS 'The ID of the record';
+
+COMMENT ON COLUMN workspace_app_stats.user_id IS 'The user who used the workspace app';
+
+COMMENT ON COLUMN workspace_app_stats.workspace_id IS 'The workspace that the workspace app was used in';
+
+COMMENT ON COLUMN workspace_app_stats.agent_id IS 'The workspace agent that was used';
+
+COMMENT ON COLUMN workspace_app_stats.access_method IS 'The method used to access the workspace app';
+
+COMMENT ON COLUMN workspace_app_stats.slug_or_port IS 'The slug or port used to to identify the app';
+
+COMMENT ON COLUMN workspace_app_stats.session_id IS 'The unique identifier for the session';
+
+COMMENT ON COLUMN workspace_app_stats.session_started_at IS 'The time the session started';
+
+COMMENT ON COLUMN workspace_app_stats.session_ended_at IS 'The time the session ended';
+
+COMMENT ON COLUMN workspace_app_stats.requests IS 'The number of requests made during the session, a number larger than 1 indicates that multiple sessions were rolled up into one';
+
+CREATE SEQUENCE workspace_app_stats_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE workspace_app_stats_id_seq OWNED BY workspace_app_stats.id;
+
CREATE TABLE workspace_apps (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -949,7 +1003,7 @@ CREATE TABLE workspaces (
autostart_schedule text,
ttl bigint,
last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL,
- locked_at timestamp with time zone,
+ dormant_at timestamp with time zone,
deleting_at timestamp with time zone
);
@@ -959,6 +1013,8 @@ ALTER TABLE ONLY provisioner_job_logs ALTER COLUMN id SET DEFAULT nextval('provi
ALTER TABLE ONLY workspace_agent_logs ALTER COLUMN id SET DEFAULT nextval('workspace_agent_startup_logs_id_seq'::regclass);
+ALTER TABLE ONLY workspace_app_stats ALTER COLUMN id SET DEFAULT nextval('workspace_app_stats_id_seq'::regclass);
+
ALTER TABLE ONLY workspace_proxies ALTER COLUMN region_id SET DEFAULT nextval('workspace_proxies_region_id_seq'::regclass);
ALTER TABLE ONLY workspace_resource_metadata ALTER COLUMN id SET DEFAULT nextval('workspace_resource_metadata_id_seq'::regclass);
@@ -1071,6 +1127,12 @@ ALTER TABLE ONLY workspace_agent_logs
ALTER TABLE ONLY workspace_agents
ADD CONSTRAINT workspace_agents_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY workspace_app_stats
+ ADD CONSTRAINT workspace_app_stats_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY workspace_app_stats
+ ADD CONSTRAINT workspace_app_stats_user_id_agent_id_session_id_key UNIQUE (user_id, agent_id, session_id);
+
ALTER TABLE ONLY workspace_apps
ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
@@ -1157,6 +1219,8 @@ CREATE INDEX workspace_agents_auth_token_idx ON workspace_agents USING btree (au
CREATE INDEX workspace_agents_resource_id_idx ON workspace_agents USING btree (resource_id);
+CREATE INDEX workspace_app_stats_workspace_id_idx ON workspace_app_stats USING btree (workspace_id);
+
CREATE UNIQUE INDEX workspace_proxies_lower_name_idx ON workspace_proxies USING btree (lower(name)) WHERE (deleted = false);
CREATE INDEX workspace_resources_job_id_idx ON workspace_resources USING btree (job_id);
@@ -1242,6 +1306,15 @@ ALTER TABLE ONLY workspace_agent_logs
ALTER TABLE ONLY workspace_agents
ADD CONSTRAINT workspace_agents_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE;
+ALTER TABLE ONLY workspace_app_stats
+ ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id);
+
+ALTER TABLE ONLY workspace_app_stats
+ ADD CONSTRAINT workspace_app_stats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
+
+ALTER TABLE ONLY workspace_app_stats
+ ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id);
+
ALTER TABLE ONLY workspace_apps
ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
diff --git a/coderd/database/gen/dump/main.go b/coderd/database/gen/dump/main.go
index 2c70c4294c35d..daa26923f9411 100644
--- a/coderd/database/gen/dump/main.go
+++ b/coderd/database/gen/dump/main.go
@@ -11,8 +11,8 @@ import (
"strconv"
"strings"
- "github.com/coder/coder/coderd/database/migrations"
- "github.com/coder/coder/coderd/database/postgres"
+ "github.com/coder/coder/v2/coderd/database/migrations"
+ "github.com/coder/coder/v2/coderd/database/postgres"
)
const minimumPostgreSQLVersion = 13
diff --git a/coderd/database/migrations/000148_group_source.down.sql b/coderd/database/migrations/000148_group_source.down.sql
new file mode 100644
index 0000000000000..504c227d186bb
--- /dev/null
+++ b/coderd/database/migrations/000148_group_source.down.sql
@@ -0,0 +1,8 @@
+BEGIN;
+
+ALTER TABLE groups
+ DROP COLUMN source;
+
+DROP TYPE group_source;
+
+COMMIT;
diff --git a/coderd/database/migrations/000148_group_source.up.sql b/coderd/database/migrations/000148_group_source.up.sql
new file mode 100644
index 0000000000000..d06e89ca2b1d6
--- /dev/null
+++ b/coderd/database/migrations/000148_group_source.up.sql
@@ -0,0 +1,15 @@
+BEGIN;
+
+CREATE TYPE group_source AS ENUM (
+ -- User created groups
+ 'user',
+ -- Groups created by the system through oidc sync
+ 'oidc'
+);
+
+ALTER TABLE groups
+ ADD COLUMN source group_source NOT NULL DEFAULT 'user';
+
+COMMENT ON COLUMN groups.source IS 'Source indicates how the group was created. It can be created by a user manually, or through some system process like OIDC group sync.';
+
+COMMIT;
diff --git a/coderd/database/migrations/000149_agent_multiple_subsystems.down.sql b/coderd/database/migrations/000149_agent_multiple_subsystems.down.sql
new file mode 100644
index 0000000000000..05bea6c620502
--- /dev/null
+++ b/coderd/database/migrations/000149_agent_multiple_subsystems.down.sql
@@ -0,0 +1,17 @@
+BEGIN;
+
+-- Bring back the subsystem column.
+ALTER TABLE workspace_agents ADD COLUMN subsystem workspace_agent_subsystem NOT NULL DEFAULT 'none';
+
+-- Update all existing workspace_agents to have subsystem = subsystems[0] unless
+-- subsystems is empty.
+UPDATE workspace_agents SET subsystem = subsystems[1] WHERE cardinality(subsystems) > 0;
+
+-- Drop the subsystems column from workspace_agents.
+ALTER TABLE workspace_agents DROP COLUMN subsystems;
+
+-- We cannot drop the "exectrace" value from the workspace_agent_subsystem type
+-- because you cannot drop values from an enum type.
+UPDATE workspace_agents SET subsystem = 'none' WHERE subsystem = 'exectrace';
+
+COMMIT;
diff --git a/coderd/database/migrations/000149_agent_multiple_subsystems.up.sql b/coderd/database/migrations/000149_agent_multiple_subsystems.up.sql
new file mode 100644
index 0000000000000..9ebb71d5bdf5e
--- /dev/null
+++ b/coderd/database/migrations/000149_agent_multiple_subsystems.up.sql
@@ -0,0 +1,21 @@
+BEGIN;
+
+-- Add "exectrace" to workspace_agent_subsystem type.
+ALTER TYPE workspace_agent_subsystem ADD VALUE 'exectrace';
+
+-- Create column subsystems in workspace_agents table, with default value being
+-- an empty array.
+ALTER TABLE workspace_agents ADD COLUMN subsystems workspace_agent_subsystem[] DEFAULT '{}';
+
+-- Add a constraint that the subsystems cannot contain the deprecated value
+-- 'none'.
+ALTER TABLE workspace_agents ADD CONSTRAINT subsystems_not_none CHECK (NOT ('none' = ANY (subsystems)));
+
+-- Update all existing workspace_agents to have subsystems = [subsystem] unless
+-- the subsystem is 'none'.
+UPDATE workspace_agents SET subsystems = ARRAY[subsystem] WHERE subsystem != 'none';
+
+-- Drop the subsystem column from workspace_agents.
+ALTER TABLE workspace_agents DROP COLUMN subsystem;
+
+COMMIT;
diff --git a/coderd/database/migrations/000150_workspace_app_stats.down.sql b/coderd/database/migrations/000150_workspace_app_stats.down.sql
new file mode 100644
index 0000000000000..983a13c180edc
--- /dev/null
+++ b/coderd/database/migrations/000150_workspace_app_stats.down.sql
@@ -0,0 +1 @@
+DROP TABLE workspace_app_stats;
diff --git a/coderd/database/migrations/000150_workspace_app_stats.up.sql b/coderd/database/migrations/000150_workspace_app_stats.up.sql
new file mode 100644
index 0000000000000..ace09e52760f6
--- /dev/null
+++ b/coderd/database/migrations/000150_workspace_app_stats.up.sql
@@ -0,0 +1,32 @@
+CREATE TABLE workspace_app_stats (
+ id BIGSERIAL PRIMARY KEY,
+ user_id uuid NOT NULL REFERENCES users (id),
+ workspace_id uuid NOT NULL REFERENCES workspaces (id),
+ agent_id uuid NOT NULL REFERENCES workspace_agents (id),
+ access_method text NOT NULL,
+ slug_or_port text NOT NULL,
+ session_id uuid NOT NULL,
+ session_started_at timestamptz NOT NULL,
+ session_ended_at timestamptz NOT NULL,
+ requests integer NOT NULL,
+
+ -- Set a unique constraint to allow upserting the session_ended_at
+ -- and requests fields without risk of collisions.
+ UNIQUE(user_id, agent_id, session_id)
+);
+
+COMMENT ON TABLE workspace_app_stats IS 'A record of workspace app usage statistics';
+
+COMMENT ON COLUMN workspace_app_stats.id IS 'The ID of the record';
+COMMENT ON COLUMN workspace_app_stats.user_id IS 'The user who used the workspace app';
+COMMENT ON COLUMN workspace_app_stats.workspace_id IS 'The workspace that the workspace app was used in';
+COMMENT ON COLUMN workspace_app_stats.agent_id IS 'The workspace agent that was used';
+COMMENT ON COLUMN workspace_app_stats.access_method IS 'The method used to access the workspace app';
+COMMENT ON COLUMN workspace_app_stats.slug_or_port IS 'The slug or port used to to identify the app';
+COMMENT ON COLUMN workspace_app_stats.session_id IS 'The unique identifier for the session';
+COMMENT ON COLUMN workspace_app_stats.session_started_at IS 'The time the session started';
+COMMENT ON COLUMN workspace_app_stats.session_ended_at IS 'The time the session ended';
+COMMENT ON COLUMN workspace_app_stats.requests IS 'The number of requests made during the session, a number larger than 1 indicates that multiple sessions were rolled up into one';
+
+-- Create index on workspace_id for joining/filtering by templates.
+CREATE INDEX workspace_app_stats_workspace_id_idx ON workspace_app_stats (workspace_id);
diff --git a/coderd/database/migrations/000151_rename_locked.down.sql b/coderd/database/migrations/000151_rename_locked.down.sql
new file mode 100644
index 0000000000000..4dfb254268fa2
--- /dev/null
+++ b/coderd/database/migrations/000151_rename_locked.down.sql
@@ -0,0 +1,26 @@
+BEGIN;
+
+ALTER TABLE templates RENAME COLUMN time_til_dormant TO inactivity_ttl;
+ALTER TABLE templates RENAME COLUMN time_til_dormant_autodelete TO locked_ttl;
+ALTER TABLE workspaces RENAME COLUMN dormant_at TO locked_at;
+
+-- Update the template_with_users view;
+DROP VIEW template_with_users;
+-- If you need to update this view, put 'DROP VIEW template_with_users;' before this.
+CREATE VIEW
+ template_with_users
+AS
+ SELECT
+ templates.*,
+ coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
+ coalesce(visible_users.username, '') AS created_by_username
+ FROM
+ templates
+ LEFT JOIN
+ visible_users
+ ON
+ templates.created_by = visible_users.id;
+
+COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
+
+COMMIT;
diff --git a/coderd/database/migrations/000151_rename_locked.up.sql b/coderd/database/migrations/000151_rename_locked.up.sql
new file mode 100644
index 0000000000000..ae72c7efa98cb
--- /dev/null
+++ b/coderd/database/migrations/000151_rename_locked.up.sql
@@ -0,0 +1,25 @@
+BEGIN;
+ALTER TABLE templates RENAME COLUMN inactivity_ttl TO time_til_dormant;
+ALTER TABLE templates RENAME COLUMN locked_ttl TO time_til_dormant_autodelete;
+ALTER TABLE workspaces RENAME COLUMN locked_at TO dormant_at;
+
+-- Update the template_with_users view;a
+DROP VIEW template_with_users;
+-- If you need to update this view, put 'DROP VIEW template_with_users;' before this.
+CREATE VIEW
+ template_with_users
+AS
+ SELECT
+ templates.*,
+ coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
+ coalesce(visible_users.username, '') AS created_by_username
+ FROM
+ templates
+ LEFT JOIN
+ visible_users
+ ON
+ templates.created_by = visible_users.id;
+
+COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
+
+COMMIT;
diff --git a/coderd/database/migrations/migrate_test.go b/coderd/database/migrations/migrate_test.go
index 5726b65609ac2..a138e58bac05f 100644
--- a/coderd/database/migrations/migrate_test.go
+++ b/coderd/database/migrations/migrate_test.go
@@ -22,9 +22,9 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
- "github.com/coder/coder/coderd/database/migrations"
- "github.com/coder/coder/coderd/database/postgres"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database/migrations"
+ "github.com/coder/coder/v2/coderd/database/postgres"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
diff --git a/coderd/database/migrations/testdata/fixtures/000150_workspace_app_usage_stats.up.sql b/coderd/database/migrations/testdata/fixtures/000150_workspace_app_usage_stats.up.sql
new file mode 100644
index 0000000000000..9a9a8f0fa72dc
--- /dev/null
+++ b/coderd/database/migrations/testdata/fixtures/000150_workspace_app_usage_stats.up.sql
@@ -0,0 +1,133 @@
+INSERT INTO public.workspace_app_stats (
+ id,
+ user_id,
+ workspace_id,
+ agent_id,
+ access_method,
+ slug_or_port,
+ session_id,
+ session_started_at,
+ session_ended_at,
+ requests
+)
+VALUES
+ (
+ 1498,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'path',
+ 'code-server',
+ '562cbfb8-3d9a-4018-9c04-e8159d5aa43e',
+ '2023-08-14 20:15:00+00',
+ '2023-08-14 20:16:00+00',
+ 1
+ ),
+ (
+ 59,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'terminal',
+ '',
+ '281919d0-5d99-48fb-8a93-2c3019010387',
+ '2023-08-14 14:15:40.085827+00',
+ '2023-08-14 14:17:41.295989+00',
+ 1
+ ),
+ (
+ 58,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'path',
+ 'code-server',
+ '5b7c9d43-19e6-4401-997b-c26de2c86c55',
+ '2023-08-14 14:15:34.620496+00',
+ '2023-08-14 23:58:37.158964+00',
+ 1
+ ),
+ (
+ 57,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'path',
+ 'code-server',
+ 'fe546a68-0921-4a2b-bced-5dc5c5635576',
+ '2023-08-14 14:15:34.129002+00',
+ '2023-08-14 23:58:37.158901+00',
+ 1
+ ),
+ (
+ 56,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'path',
+ 'code-server',
+ '96e4e857-598c-4881-bc40-e13008b48bb0',
+ '2023-08-14 14:15:00+00',
+ '2023-08-14 14:16:00+00',
+ 36
+ ),
+ (
+ 7,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'terminal',
+ '',
+ '95d22d41-0fde-447b-9743-0b8583edb60a',
+ '2023-08-14 13:00:28.732837+00',
+ '2023-08-14 13:09:23.990797+00',
+ 1
+ ),
+ (
+ 4,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'path',
+ 'code-server',
+ '442688ce-f9e7-46df-ba3d-623ef9a1d30d',
+ '2023-08-14 13:00:12.843977+00',
+ '2023-08-14 13:09:26.276696+00',
+ 1
+ ),
+ (
+ 3,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'path',
+ 'code-server',
+ 'f963c4f0-55b7-4813-8b61-ea58536754db',
+ '2023-08-14 13:00:12.323196+00',
+ '2023-08-14 13:09:26.277073+00',
+ 1
+ ),
+ (
+ 2,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'terminal',
+ '',
+ '5a034459-73e4-4642-91b8-80b0f718f29e',
+ '2023-08-14 13:00:00+00',
+ '2023-08-14 13:01:00+00',
+ 4
+ ),
+ (
+ 1,
+ '30095c71-380b-457a-8995-97b8ee6e5307',
+ '3a9a1feb-e89d-457c-9d53-ac751b198ebe',
+ '7a1ce5f8-8d00-431c-ad1b-97a846512804',
+ 'path',
+ 'code-server',
+ 'd7a0d8e1-069e-421d-b876-b5d0ddbcaf6d',
+ '2023-08-14 13:00:00+00',
+ '2023-08-14 13:01:00+00',
+ 36
+ );
diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go
index 3daaec35da595..1cccdd949ecc8 100644
--- a/coderd/database/modelmethods.go
+++ b/coderd/database/modelmethods.go
@@ -7,7 +7,7 @@ import (
"golang.org/x/exp/maps"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac"
)
type WorkspaceStatus string
@@ -84,7 +84,7 @@ func (g Group) Auditable(users []User) AuditableGroup {
}
}
-const AllUsersGroup = "Everyone"
+const EveryoneGroup = "Everyone"
func (s APIKeyScope) ToRBAC() rbac.ScopeName {
switch s {
@@ -146,8 +146,8 @@ func (w Workspace) RBACObject() rbac.Object {
func (w Workspace) ExecutionRBAC() rbac.Object {
// If a workspace is locked it cannot be accessed.
- if w.LockedAt.Valid {
- return w.LockedRBAC()
+ if w.DormantAt.Valid {
+ return w.DormantRBAC()
}
return rbac.ResourceWorkspaceExecution.
@@ -158,8 +158,8 @@ func (w Workspace) ExecutionRBAC() rbac.Object {
func (w Workspace) ApplicationConnectRBAC() rbac.Object {
// If a workspace is locked it cannot be accessed.
- if w.LockedAt.Valid {
- return w.LockedRBAC()
+ if w.DormantAt.Valid {
+ return w.DormantRBAC()
}
return rbac.ResourceWorkspaceApplicationConnect.
@@ -173,9 +173,9 @@ func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Objec
// However we need to allow stopping a workspace by a caller once a workspace
// is locked (e.g. for autobuild). Additionally, if a user wants to delete
// a locked workspace, they shouldn't have to have it unlocked first.
- if w.LockedAt.Valid && transition != WorkspaceTransitionStop &&
+ if w.DormantAt.Valid && transition != WorkspaceTransitionStop &&
transition != WorkspaceTransitionDelete {
- return w.LockedRBAC()
+ return w.DormantRBAC()
}
return rbac.ResourceWorkspaceBuild.
@@ -184,8 +184,8 @@ func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Objec
WithOwner(w.OwnerID.String())
}
-func (w Workspace) LockedRBAC() rbac.Object {
- return rbac.ResourceWorkspaceLocked.
+func (w Workspace) DormantRBAC() rbac.Object {
+ return rbac.ResourceWorkspaceDormant.
WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
@@ -355,10 +355,14 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
AutostartSchedule: r.AutostartSchedule,
Ttl: r.Ttl,
LastUsedAt: r.LastUsedAt,
- LockedAt: r.LockedAt,
+ DormantAt: r.DormantAt,
DeletingAt: r.DeletingAt,
}
}
return workspaces
}
+
+func (g Group) IsEveryone() bool {
+ return g.ID == g.OrganizationID
+}
diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go
index ffa346d04998c..5ccf3282e677c 100644
--- a/coderd/database/modelqueries.go
+++ b/coderd/database/modelqueries.go
@@ -9,8 +9,8 @@ import (
"github.com/lib/pq"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/rbac/regosql"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac/regosql"
)
const (
@@ -81,8 +81,8 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
&i.AllowUserAutostart,
&i.AllowUserAutostop,
&i.FailureTTL,
- &i.InactivityTTL,
- &i.LockedTTL,
+ &i.TimeTilDormant,
+ &i.TimeTilDormantAutoDelete,
&i.RestartRequirementDaysOfWeek,
&i.RestartRequirementWeeks,
&i.CreatedByAvatarURL,
@@ -217,11 +217,14 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
arg.Name,
arg.HasAgent,
arg.AgentInactiveDisconnectTimeoutSeconds,
+ arg.DormantAt,
+ arg.LastUsedBefore,
+ arg.LastUsedAfter,
arg.Offset,
arg.Limit,
)
if err != nil {
- return nil, xerrors.Errorf("get authorized workspaces: %w", err)
+ return nil, err
}
defer rows.Close()
var items []GetWorkspacesRow
@@ -239,7 +242,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
- &i.LockedAt,
+ &i.DormantAt,
&i.DeletingAt,
&i.TemplateName,
&i.TemplateVersionID,
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 4e34989b09ae9..85f90020d9fc1 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.19.1
+// sqlc v1.20.0
package database
@@ -281,6 +281,64 @@ func AllBuildReasonValues() []BuildReason {
}
}
+type GroupSource string
+
+const (
+ GroupSourceUser GroupSource = "user"
+ GroupSourceOidc GroupSource = "oidc"
+)
+
+func (e *GroupSource) Scan(src interface{}) error {
+ switch s := src.(type) {
+ case []byte:
+ *e = GroupSource(s)
+ case string:
+ *e = GroupSource(s)
+ default:
+ return fmt.Errorf("unsupported scan type for GroupSource: %T", src)
+ }
+ return nil
+}
+
+type NullGroupSource struct {
+ GroupSource GroupSource `json:"group_source"`
+ Valid bool `json:"valid"` // Valid is true if GroupSource is not NULL
+}
+
+// Scan implements the Scanner interface.
+func (ns *NullGroupSource) Scan(value interface{}) error {
+ if value == nil {
+ ns.GroupSource, ns.Valid = "", false
+ return nil
+ }
+ ns.Valid = true
+ return ns.GroupSource.Scan(value)
+}
+
+// Value implements the driver Valuer interface.
+func (ns NullGroupSource) Value() (driver.Value, error) {
+ if !ns.Valid {
+ return nil, nil
+ }
+ return string(ns.GroupSource), nil
+}
+
+func (e GroupSource) Valid() bool {
+ switch e {
+ case GroupSourceUser,
+ GroupSourceOidc:
+ return true
+ }
+ return false
+}
+
+func AllGroupSourceValues() []GroupSource {
+ return []GroupSource{
+ GroupSourceUser,
+ GroupSourceOidc,
+ }
+}
+
type LogLevel string
const (
@@ -1249,6 +1307,7 @@ const (
WorkspaceAgentSubsystemEnvbuilder WorkspaceAgentSubsystem = "envbuilder"
WorkspaceAgentSubsystemEnvbox WorkspaceAgentSubsystem = "envbox"
WorkspaceAgentSubsystemNone WorkspaceAgentSubsystem = "none"
+ WorkspaceAgentSubsystemExectrace WorkspaceAgentSubsystem = "exectrace"
)
func (e *WorkspaceAgentSubsystem) Scan(src interface{}) error {
@@ -1290,7 +1349,8 @@ func (e WorkspaceAgentSubsystem) Valid() bool {
switch e {
case WorkspaceAgentSubsystemEnvbuilder,
WorkspaceAgentSubsystemEnvbox,
- WorkspaceAgentSubsystemNone:
+ WorkspaceAgentSubsystemNone,
+ WorkspaceAgentSubsystemExectrace:
return true
}
return false
@@ -1301,6 +1361,7 @@ func AllWorkspaceAgentSubsystemValues() []WorkspaceAgentSubsystem {
WorkspaceAgentSubsystemEnvbuilder,
WorkspaceAgentSubsystemEnvbox,
WorkspaceAgentSubsystemNone,
+ WorkspaceAgentSubsystemExectrace,
}
}
@@ -1498,6 +1559,8 @@ type Group struct {
QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"`
// Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string.
DisplayName string `db:"display_name" json:"display_name"`
+ // Source indicates how the group was created. It can be created by a user manually, or through some system process like OIDC group sync.
+ Source GroupSource `db:"source" json:"source"`
}
type GroupMember struct {
@@ -1666,8 +1729,8 @@ type Template struct {
AllowUserAutostart bool `db:"allow_user_autostart" json:"allow_user_autostart"`
AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"`
FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"`
- InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"`
- LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"`
+ TimeTilDormant int64 `db:"time_til_dormant" json:"time_til_dormant"`
+ TimeTilDormantAutoDelete int64 `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"`
RestartRequirementDaysOfWeek int16 `db:"restart_requirement_days_of_week" json:"restart_requirement_days_of_week"`
RestartRequirementWeeks int64 `db:"restart_requirement_weeks" json:"restart_requirement_weeks"`
CreatedByAvatarURL sql.NullString `db:"created_by_avatar_url" json:"created_by_avatar_url"`
@@ -1698,10 +1761,10 @@ type TemplateTable struct {
// Allow users to specify an autostart schedule for workspaces (enterprise).
AllowUserAutostart bool `db:"allow_user_autostart" json:"allow_user_autostart"`
// Allow users to specify custom autostop values for workspaces (enterprise).
- AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"`
- FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"`
- InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"`
- LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"`
+ AllowUserAutostop bool `db:"allow_user_autostop" json:"allow_user_autostop"`
+ FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"`
+ TimeTilDormant int64 `db:"time_til_dormant" json:"time_til_dormant"`
+ TimeTilDormantAutoDelete int64 `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"`
// A bitmap of days of week to restart the workspace on, starting with Monday as the 0th bit, and Sunday as the 6th bit. The 7th bit is unused.
RestartRequirementDaysOfWeek int16 `db:"restart_requirement_days_of_week" json:"restart_requirement_days_of_week"`
// The number of weeks between restarts. 0 or 1 weeks means "every week", 2 week means "every second week", etc. Weeks are counted from January 2, 2023, which is the first Monday of 2023. This is to ensure workspaces are started consistently for all customers on the same n-week cycles.
@@ -1840,7 +1903,7 @@ type Workspace struct {
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
- LockedAt sql.NullTime `db:"locked_at" json:"locked_at"`
+ DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
}
@@ -1884,14 +1947,14 @@ type WorkspaceAgent struct {
// Total length of startup logs
LogsLength int32 `db:"logs_length" json:"logs_length"`
// Whether the startup logs overflowed in length
- LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
- Subsystem WorkspaceAgentSubsystem `db:"subsystem" json:"subsystem"`
+ LogsOverflowed bool `db:"logs_overflowed" json:"logs_overflowed"`
// When startup script behavior is non-blocking, the workspace will be ready and accessible upon agent connection, when it is blocking, workspace will wait for the startup script to complete before becoming ready and accessible.
StartupScriptBehavior StartupScriptBehavior `db:"startup_script_behavior" json:"startup_script_behavior"`
// The time the agent entered the starting lifecycle state
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
// The time the agent entered the ready or start_error lifecycle state
- ReadyAt sql.NullTime `db:"ready_at" json:"ready_at"`
+ ReadyAt sql.NullTime `db:"ready_at" json:"ready_at"`
+ Subsystems []WorkspaceAgentSubsystem `db:"subsystems" json:"subsystems"`
}
type WorkspaceAgentLog struct {
@@ -1953,6 +2016,30 @@ type WorkspaceApp struct {
External bool `db:"external" json:"external"`
}
+// A record of workspace app usage statistics
+type WorkspaceAppStat struct {
+ // The ID of the record
+ ID int64 `db:"id" json:"id"`
+ // The user who used the workspace app
+ UserID uuid.UUID `db:"user_id" json:"user_id"`
+ // The workspace that the workspace app was used in
+ WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
+ // The workspace agent that was used
+ AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
+ // The method used to access the workspace app
+ AccessMethod string `db:"access_method" json:"access_method"`
+ // The slug or port used to to identify the app
+ SlugOrPort string `db:"slug_or_port" json:"slug_or_port"`
+ // The unique identifier for the session
+ SessionID uuid.UUID `db:"session_id" json:"session_id"`
+ // The time the session started
+ SessionStartedAt time.Time `db:"session_started_at" json:"session_started_at"`
+ // The time the session ended
+ SessionEndedAt time.Time `db:"session_ended_at" json:"session_ended_at"`
+ // The number of requests made during the session, a number larger than 1 indicates that multiple sessions were rolled up into one
+ Requests int32 `db:"requests" json:"requests"`
+}
+
// Joins in the username + avatar url of the initiated by user.
type WorkspaceBuild struct {
ID uuid.UUID `db:"id" json:"id"`
diff --git a/coderd/database/models_test.go b/coderd/database/models_test.go
index 1ebb40ae2ff26..a3c37683ac2c8 100644
--- a/coderd/database/models_test.go
+++ b/coderd/database/models_test.go
@@ -5,10 +5,24 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
)
+// TestAuditDBEnumsCovered ensures that all enums in the database are covered by the codersdk enums
+// for audit log strings.
+func TestAuditDBEnumsCovered(t *testing.T) {
+ t.Parallel()
+
+ dbTypes := database.AllResourceTypeValues()
+ for _, ty := range dbTypes {
+ str := codersdk.ResourceType(ty).FriendlyString()
+ require.NotEqualf(t, "unknown", str, "ResourceType %q not covered by codersdk.ResourceType", ty)
+ }
+}
+
// TestViewSubsetTemplate ensures TemplateTable is a subset of Template
func TestViewSubsetTemplate(t *testing.T) {
t.Parallel()
diff --git a/coderd/database/postgres/postgres.go b/coderd/database/postgres/postgres.go
index 85710d8bb0f7b..8a7d0209ba4e0 100644
--- a/coderd/database/postgres/postgres.go
+++ b/coderd/database/postgres/postgres.go
@@ -12,8 +12,8 @@ import (
"github.com/ory/dockertest/v3/docker"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database/migrations"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/database/migrations"
+ "github.com/coder/coder/v2/cryptorand"
)
// Open creates a new PostgreSQL database instance. With DB_FROM environment variable set, it clones a database
diff --git a/coderd/database/postgres/postgres_test.go b/coderd/database/postgres/postgres_test.go
index 88d9800a57644..4a217d072f4af 100644
--- a/coderd/database/postgres/postgres_test.go
+++ b/coderd/database/postgres/postgres_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
- "github.com/coder/coder/coderd/database/postgres"
+ "github.com/coder/coder/v2/coderd/database/postgres"
)
func TestMain(m *testing.M) {
diff --git a/coderd/database/pubsub/pubsub_internal_test.go b/coderd/database/pubsub/pubsub_internal_test.go
index adfa70286dbe0..47dd324fc09df 100644
--- a/coderd/database/pubsub/pubsub_internal_test.go
+++ b/coderd/database/pubsub/pubsub_internal_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/testutil"
)
func Test_msgQueue_ListenerWithError(t *testing.T) {
diff --git a/coderd/database/pubsub/pubsub_memory_test.go b/coderd/database/pubsub/pubsub_memory_test.go
index 80553c8fa73da..0f392ade742c4 100644
--- a/coderd/database/pubsub/pubsub_memory_test.go
+++ b/coderd/database/pubsub/pubsub_memory_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
)
func TestPubsubMemory(t *testing.T) {
diff --git a/coderd/database/pubsub/pubsub_test.go b/coderd/database/pubsub/pubsub_test.go
index d1f80fa5a1aed..1d414d9edcd2c 100644
--- a/coderd/database/pubsub/pubsub_test.go
+++ b/coderd/database/pubsub/pubsub_test.go
@@ -15,9 +15,9 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database/postgres"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database/postgres"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/testutil"
)
// nolint:tparallel,paralleltest
diff --git a/coderd/database/querier.go b/coderd/database/querier.go
index 5e9da77b66c0a..520266bd1d25c 100644
--- a/coderd/database/querier.go
+++ b/coderd/database/querier.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.19.1
+// sqlc v1.20.0
package database
@@ -48,6 +48,7 @@ type sqlcQuerier interface {
GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error)
GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error)
GetActiveUserCount(ctx context.Context) (int64, error)
+ GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error)
GetAllTailnetAgents(ctx context.Context) ([]TailnetAgent, error)
GetAllTailnetClients(ctx context.Context) ([]TailnetClient, error)
GetAppSecurityKey(ctx context.Context) (string, error)
@@ -71,6 +72,8 @@ type sqlcQuerier interface {
GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error)
GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error)
GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error)
+ // If the group is a user made group, then we need to check the group_members table.
+ // If it is the "Everyone" group, then we need to check the organization_members table.
GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([]User, error)
GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error)
GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error)
@@ -104,6 +107,10 @@ type sqlcQuerier interface {
GetServiceBanner(ctx context.Context) (string, error)
GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error)
GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error)
+ // GetTemplateAppInsights returns the aggregate usage of each app in a given
+ // timeframe. The result can be filtered on template_ids, meaning only user data
+ // from workspaces based on those templates will be included.
+ GetTemplateAppInsights(ctx context.Context, arg GetTemplateAppInsightsParams) ([]GetTemplateAppInsightsRow, error)
GetTemplateAverageBuildTime(ctx context.Context, arg GetTemplateAverageBuildTimeParams) (GetTemplateAverageBuildTimeRow, error)
GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error)
GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error)
@@ -114,7 +121,8 @@ type sqlcQuerier interface {
// interval/template, it will be included in the results with 0 active users.
GetTemplateDailyInsights(ctx context.Context, arg GetTemplateDailyInsightsParams) ([]GetTemplateDailyInsightsRow, error)
// GetTemplateInsights has a granularity of 5 minutes where if a session/app was
- // in use, we will add 5 minutes to the total usage for that session (per user).
+ // in use during a minute, we will add 5 minutes to the total usage for that
+ // session/app (per user).
GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error)
// GetTemplateParameterInsights does for each template in a given timeframe,
// look for the latest workspace build (for every workspace) that has been
@@ -148,7 +156,7 @@ type sqlcQuerier interface {
// to look up references to actions. eg. a user could build a workspace
// for another user, then be deleted... we still want them to appear!
GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error)
- GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error)
+ GetWorkspaceAgentAndOwnerByAuthToken(ctx context.Context, authToken uuid.UUID) (GetWorkspaceAgentAndOwnerByAuthTokenRow, error)
GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error)
GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error)
GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error)
@@ -206,6 +214,11 @@ type sqlcQuerier interface {
InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error)
InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error
InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error)
+ // Inserts any group by name that does not exist. All new groups are given
+ // a random uuid, are inserted into the same organization. They have the default
+ // values for avatar, display name, and quota allowance (all zero values).
+ // If the name conflicts, do nothing.
+ InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error)
InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error)
InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error)
InsertProvisionerDaemon(ctx context.Context, arg InsertProvisionerDaemonParams) (ProvisionerDaemon, error)
@@ -225,7 +238,9 @@ type sqlcQuerier interface {
InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error)
InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error
InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error)
+ InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error
InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error)
+ InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error
InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
@@ -255,6 +270,7 @@ type sqlcQuerier interface {
UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error
UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error
UpdateTemplateVersionGitAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionGitAuthProvidersByJobIDParams) error
+ UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error
UpdateUserDeletedByID(ctx context.Context, arg UpdateUserDeletedByIDParams) error
UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error
UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error)
@@ -276,13 +292,13 @@ type sqlcQuerier interface {
UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) error
UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
+ UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (Workspace, error)
UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error
- UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) error
// This allows editing the properties of a workspace proxy.
UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error)
UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error
UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error
- UpdateWorkspacesDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDeletingAtByTemplateIDParams) error
+ UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error
UpsertAppSecurityKey(ctx context.Context, value string) error
// The default proxy is implied and not actually stored in the database.
// So we need to store it's configuration here for display purposes.
diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go
index 07a81111d64c9..2a6328cb96e22 100644
--- a/coderd/database/querier_test.go
+++ b/coderd/database/querier_test.go
@@ -13,10 +13,10 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/database/migrations"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/migrations"
+ "github.com/coder/coder/v2/testutil"
)
func TestGetDeploymentWorkspaceAgentStats(t *testing.T) {
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index ff22cc3120193..364ae4c546267 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.19.1
+// sqlc v1.20.0
package database
@@ -1069,18 +1069,29 @@ SELECT
users.id, users.email, users.username, users.hashed_password, users.created_at, users.updated_at, users.status, users.rbac_roles, users.login_type, users.avatar_url, users.deleted, users.last_seen_at, users.quiet_hours_schedule
FROM
users
-JOIN
+LEFT JOIN
group_members
ON
- users.id = group_members.user_id
-WHERE
+ group_members.user_id = users.id AND
group_members.group_id = $1
+LEFT JOIN
+ organization_members
+ON
+ organization_members.user_id = users.id AND
+ organization_members.organization_id = $1
+WHERE
+ -- In either case, the group_id will only match an org or a group.
+ (group_members.group_id = $1
+ OR
+ organization_members.organization_id = $1)
AND
users.status = 'active'
AND
users.deleted = 'false'
`
+// If the group is a user made group, then we need to check the group_members table.
+// If it is the "Everyone" group, then we need to check the organization_members table.
func (q *sqlQuerier) GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([]User, error) {
rows, err := q.db.QueryContext(ctx, getGroupMembers, groupID)
if err != nil {
@@ -1180,7 +1191,7 @@ func (q *sqlQuerier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error {
const getGroupByID = `-- name: GetGroupByID :one
SELECT
- id, name, organization_id, avatar_url, quota_allowance, display_name
+ id, name, organization_id, avatar_url, quota_allowance, display_name, source
FROM
groups
WHERE
@@ -1199,13 +1210,14 @@ func (q *sqlQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (Group, err
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
+ &i.Source,
)
return i, err
}
const getGroupByOrgAndName = `-- name: GetGroupByOrgAndName :one
SELECT
- id, name, organization_id, avatar_url, quota_allowance, display_name
+ id, name, organization_id, avatar_url, quota_allowance, display_name, source
FROM
groups
WHERE
@@ -1231,19 +1243,18 @@ func (q *sqlQuerier) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrg
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
+ &i.Source,
)
return i, err
}
const getGroupsByOrganizationID = `-- name: GetGroupsByOrganizationID :many
SELECT
- id, name, organization_id, avatar_url, quota_allowance, display_name
+ id, name, organization_id, avatar_url, quota_allowance, display_name, source
FROM
groups
WHERE
organization_id = $1
-AND
- id != $1
`
func (q *sqlQuerier) GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error) {
@@ -1262,6 +1273,7 @@ func (q *sqlQuerier) GetGroupsByOrganizationID(ctx context.Context, organization
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
+ &i.Source,
); err != nil {
return nil, err
}
@@ -1283,7 +1295,7 @@ INSERT INTO groups (
organization_id
)
VALUES
- ($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name
+ ($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source
`
// We use the organization_id as the id
@@ -1299,6 +1311,7 @@ func (q *sqlQuerier) InsertAllUsersGroup(ctx context.Context, organizationID uui
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
+ &i.Source,
)
return i, err
}
@@ -1313,7 +1326,7 @@ INSERT INTO groups (
quota_allowance
)
VALUES
- ($1, $2, $3, $4, $5, $6) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name
+ ($1, $2, $3, $4, $5, $6) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source
`
type InsertGroupParams struct {
@@ -1342,10 +1355,70 @@ func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Gr
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
+ &i.Source,
)
return i, err
}
+const insertMissingGroups = `-- name: InsertMissingGroups :many
+INSERT INTO groups (
+ id,
+ name,
+ organization_id,
+ source
+)
+SELECT
+ gen_random_uuid(),
+ group_name,
+ $1,
+ $2
+FROM
+ UNNEST($3 :: text[]) AS group_name
+ON CONFLICT DO NOTHING
+RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source
+`
+
+type InsertMissingGroupsParams struct {
+ OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
+ Source GroupSource `db:"source" json:"source"`
+ GroupNames []string `db:"group_names" json:"group_names"`
+}
+
+// Inserts any group by name that does not exist. All new groups are given
+// a random uuid, are inserted into the same organization. They have the default
+// values for avatar, display name, and quota allowance (all zero values).
+// If the name conflicts, do nothing.
+func (q *sqlQuerier) InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) {
+ rows, err := q.db.QueryContext(ctx, insertMissingGroups, arg.OrganizationID, arg.Source, pq.Array(arg.GroupNames))
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []Group
+ for rows.Next() {
+ var i Group
+ if err := rows.Scan(
+ &i.ID,
+ &i.Name,
+ &i.OrganizationID,
+ &i.AvatarURL,
+ &i.QuotaAllowance,
+ &i.DisplayName,
+ &i.Source,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const updateGroupByID = `-- name: UpdateGroupByID :one
UPDATE
groups
@@ -1356,7 +1429,7 @@ SET
quota_allowance = $4
WHERE
id = $5
-RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name
+RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source
`
type UpdateGroupByIDParams struct {
@@ -1383,25 +1456,141 @@ func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDPar
&i.AvatarURL,
&i.QuotaAllowance,
&i.DisplayName,
+ &i.Source,
)
return i, err
}
+const getTemplateAppInsights = `-- name: GetTemplateAppInsights :many
+WITH app_stats_by_user_and_agent AS (
+ SELECT
+ s.start_time,
+ 60 as seconds,
+ w.template_id,
+ was.user_id,
+ was.agent_id,
+ was.access_method,
+ was.slug_or_port,
+ wa.display_name,
+ wa.icon,
+ (wa.slug IS NOT NULL)::boolean AS is_app
+ FROM workspace_app_stats was
+ JOIN workspaces w ON (
+ w.id = was.workspace_id
+ AND CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN w.template_id = ANY($1::uuid[]) ELSE TRUE END
+ )
+ -- We do a left join here because we want to include user IDs that have used
+ -- e.g. ports when counting active users.
+ LEFT JOIN workspace_apps wa ON (
+ wa.agent_id = was.agent_id
+ AND wa.slug = was.slug_or_port
+ )
+ -- This table contains both 1 minute entries and >1 minute entries,
+ -- to calculate this with our uniqueness constraints, we generate series
+ -- for the longer intervals.
+ CROSS JOIN LATERAL generate_series(
+ date_trunc('minute', was.session_started_at),
+ -- Subtract 1 microsecond to avoid creating an extra series.
+ date_trunc('minute', was.session_ended_at - '1 microsecond'::interval),
+ '1 minute'::interval
+ ) s(start_time)
+ WHERE
+ s.start_time >= $2::timestamptz
+ -- Subtract one minute because the series only contains the start time.
+ AND s.start_time < ($3::timestamptz) - '1 minute'::interval
+ GROUP BY s.start_time, w.template_id, was.user_id, was.agent_id, was.access_method, was.slug_or_port, wa.display_name, wa.icon, wa.slug
+)
+
+SELECT
+ array_agg(DISTINCT template_id)::uuid[] AS template_ids,
+ -- Return IDs so we can combine this with GetTemplateInsights.
+ array_agg(DISTINCT user_id)::uuid[] AS active_user_ids,
+ access_method,
+ slug_or_port,
+ display_name,
+ icon,
+ is_app,
+ SUM(seconds) AS usage_seconds
+FROM app_stats_by_user_and_agent
+GROUP BY access_method, slug_or_port, display_name, icon, is_app
+`
+
+type GetTemplateAppInsightsParams struct {
+ TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
+ StartTime time.Time `db:"start_time" json:"start_time"`
+ EndTime time.Time `db:"end_time" json:"end_time"`
+}
+
+type GetTemplateAppInsightsRow struct {
+ TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
+ ActiveUserIDs []uuid.UUID `db:"active_user_ids" json:"active_user_ids"`
+ AccessMethod string `db:"access_method" json:"access_method"`
+ SlugOrPort string `db:"slug_or_port" json:"slug_or_port"`
+ DisplayName sql.NullString `db:"display_name" json:"display_name"`
+ Icon sql.NullString `db:"icon" json:"icon"`
+ IsApp bool `db:"is_app" json:"is_app"`
+ UsageSeconds int64 `db:"usage_seconds" json:"usage_seconds"`
+}
+
+// GetTemplateAppInsights returns the aggregate usage of each app in a given
+// timeframe. The result can be filtered on template_ids, meaning only user data
+// from workspaces based on those templates will be included.
+func (q *sqlQuerier) GetTemplateAppInsights(ctx context.Context, arg GetTemplateAppInsightsParams) ([]GetTemplateAppInsightsRow, error) {
+ rows, err := q.db.QueryContext(ctx, getTemplateAppInsights, pq.Array(arg.TemplateIDs), arg.StartTime, arg.EndTime)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []GetTemplateAppInsightsRow
+ for rows.Next() {
+ var i GetTemplateAppInsightsRow
+ if err := rows.Scan(
+ pq.Array(&i.TemplateIDs),
+ pq.Array(&i.ActiveUserIDs),
+ &i.AccessMethod,
+ &i.SlugOrPort,
+ &i.DisplayName,
+ &i.Icon,
+ &i.IsApp,
+ &i.UsageSeconds,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const getTemplateDailyInsights = `-- name: GetTemplateDailyInsights :many
-WITH d AS (
- -- sqlc workaround, use SELECT generate_series instead of SELECT * FROM generate_series.
- -- Subtract 1 second from end_time to avoid including the next interval in the results.
- SELECT generate_series($1::timestamptz, ($2::timestamptz) - '1 second'::interval, '1 day'::interval) AS d
-), ts AS (
+WITH ts AS (
SELECT
d::timestamptz AS from_,
- CASE WHEN (d + '1 day'::interval)::timestamptz <= $2::timestamptz THEN (d + '1 day'::interval)::timestamptz ELSE $2::timestamptz END AS to_
- FROM d
-), usage_by_day AS (
+ CASE
+ WHEN (d::timestamptz + '1 day'::interval) <= $1::timestamptz
+ THEN (d::timestamptz + '1 day'::interval)
+ ELSE $1::timestamptz
+ END AS to_
+ FROM
+ -- Subtract 1 second from end_time to avoid including the next interval in the results.
+ generate_series($2::timestamptz, ($1::timestamptz) - '1 second'::interval, '1 day'::interval) AS d
+), unflattened_usage_by_day AS (
+ -- We select data from both workspace agent stats and workspace app stats to
+ -- get a complete picture of usage. This matches how usage is calculated by
+ -- the combination of GetTemplateInsights and GetTemplateAppInsights. We use
+ -- a union all to avoid a costly distinct operation.
+ --
+ -- Note that one query must perform a left join so that all intervals are
+ -- present at least once.
SELECT
ts.from_, ts.to_,
- was.user_id,
- array_agg(was.template_id) AS template_ids
+ was.template_id,
+ was.user_id
FROM ts
LEFT JOIN workspace_agent_stats was ON (
was.created_at >= ts.from_
@@ -1409,33 +1598,39 @@ WITH d AS (
AND was.connection_count > 0
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN was.template_id = ANY($3::uuid[]) ELSE TRUE END
)
- GROUP BY ts.from_, ts.to_, was.user_id
-), template_ids AS (
+ GROUP BY ts.from_, ts.to_, was.template_id, was.user_id
+
+ UNION ALL
+
SELECT
- template_usage_by_day.from_,
- array_agg(template_id) AS ids
- FROM (
- SELECT DISTINCT
- from_,
- unnest(template_ids) AS template_id
- FROM usage_by_day
- ) AS template_usage_by_day
- WHERE template_id IS NOT NULL
- GROUP BY template_usage_by_day.from_
+ ts.from_, ts.to_,
+ w.template_id,
+ was.user_id
+ FROM ts
+ JOIN workspace_app_stats was ON (
+ (was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
+ OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
+ OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
+ )
+ JOIN workspaces w ON (
+ w.id = was.workspace_id
+ AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN w.template_id = ANY($3::uuid[]) ELSE TRUE END
+ )
+ GROUP BY ts.from_, ts.to_, w.template_id, was.user_id
)
SELECT
from_ AS start_time,
to_ AS end_time,
- COALESCE((SELECT template_ids.ids FROM template_ids WHERE template_ids.from_ = usage_by_day.from_), '{}')::uuid[] AS template_ids,
+ array_remove(array_agg(DISTINCT template_id), NULL)::uuid[] AS template_ids,
COUNT(DISTINCT user_id) AS active_users
-FROM usage_by_day
+FROM unflattened_usage_by_day
GROUP BY from_, to_
`
type GetTemplateDailyInsightsParams struct {
- StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
+ StartTime time.Time `db:"start_time" json:"start_time"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}
@@ -1451,7 +1646,7 @@ type GetTemplateDailyInsightsRow struct {
// that interval will be less than 24 hours. If there is no data for a selected
// interval/template, it will be included in the results with 0 active users.
func (q *sqlQuerier) GetTemplateDailyInsights(ctx context.Context, arg GetTemplateDailyInsightsParams) ([]GetTemplateDailyInsightsRow, error) {
- rows, err := q.db.QueryContext(ctx, getTemplateDailyInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
+ rows, err := q.db.QueryContext(ctx, getTemplateDailyInsights, arg.EndTime, arg.StartTime, pq.Array(arg.TemplateIDs))
if err != nil {
return nil, err
}
@@ -1479,47 +1674,37 @@ func (q *sqlQuerier) GetTemplateDailyInsights(ctx context.Context, arg GetTempla
}
const getTemplateInsights = `-- name: GetTemplateInsights :one
-WITH d AS (
- -- Subtract 1 second from end_time to avoid including the next interval in the results.
- SELECT generate_series($1::timestamptz, ($2::timestamptz) - '1 second'::interval, '5 minute'::interval) AS d
-), ts AS (
+WITH agent_stats_by_interval_and_user AS (
SELECT
- d::timestamptz AS from_,
- (d + '5 minute'::interval)::timestamptz AS to_,
- EXTRACT(epoch FROM '5 minute'::interval) AS seconds
- FROM d
-), usage_by_user AS (
- SELECT
- ts.from_,
- ts.to_,
+ date_trunc('minute', was.created_at),
was.user_id,
array_agg(was.template_id) AS template_ids,
- CASE WHEN SUM(was.session_count_vscode) > 0 THEN ts.seconds ELSE 0 END AS usage_vscode_seconds,
- CASE WHEN SUM(was.session_count_jetbrains) > 0 THEN ts.seconds ELSE 0 END AS usage_jetbrains_seconds,
- CASE WHEN SUM(was.session_count_reconnecting_pty) > 0 THEN ts.seconds ELSE 0 END AS usage_reconnecting_pty_seconds,
- CASE WHEN SUM(was.session_count_ssh) > 0 THEN ts.seconds ELSE 0 END AS usage_ssh_seconds
- FROM ts
- JOIN workspace_agent_stats was ON (
- was.created_at >= ts.from_
- AND was.created_at < ts.to_
+ CASE WHEN SUM(was.session_count_vscode) > 0 THEN 60 ELSE 0 END AS usage_vscode_seconds,
+ CASE WHEN SUM(was.session_count_jetbrains) > 0 THEN 60 ELSE 0 END AS usage_jetbrains_seconds,
+ CASE WHEN SUM(was.session_count_reconnecting_pty) > 0 THEN 60 ELSE 0 END AS usage_reconnecting_pty_seconds,
+ CASE WHEN SUM(was.session_count_ssh) > 0 THEN 60 ELSE 0 END AS usage_ssh_seconds
+ FROM workspace_agent_stats was
+ WHERE
+ was.created_at >= $1::timestamptz
+ AND was.created_at < $2::timestamptz
AND was.connection_count > 0
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN was.template_id = ANY($3::uuid[]) ELSE TRUE END
- )
- GROUP BY ts.from_, ts.to_, ts.seconds, was.user_id
+ GROUP BY date_trunc('minute', was.created_at), was.user_id
), template_ids AS (
SELECT array_agg(DISTINCT template_id) AS ids
- FROM usage_by_user, unnest(template_ids) template_id
+ FROM agent_stats_by_interval_and_user, unnest(template_ids) template_id
WHERE template_id IS NOT NULL
)
SELECT
COALESCE((SELECT ids FROM template_ids), '{}')::uuid[] AS template_ids,
- COUNT(DISTINCT user_id) AS active_users,
+ -- Return IDs so we can combine this with GetTemplateAppInsights.
+ COALESCE(array_agg(DISTINCT user_id), '{}')::uuid[] AS active_user_ids,
COALESCE(SUM(usage_vscode_seconds), 0)::bigint AS usage_vscode_seconds,
COALESCE(SUM(usage_jetbrains_seconds), 0)::bigint AS usage_jetbrains_seconds,
COALESCE(SUM(usage_reconnecting_pty_seconds), 0)::bigint AS usage_reconnecting_pty_seconds,
COALESCE(SUM(usage_ssh_seconds), 0)::bigint AS usage_ssh_seconds
-FROM usage_by_user
+FROM agent_stats_by_interval_and_user
`
type GetTemplateInsightsParams struct {
@@ -1530,7 +1715,7 @@ type GetTemplateInsightsParams struct {
type GetTemplateInsightsRow struct {
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
- ActiveUsers int64 `db:"active_users" json:"active_users"`
+ ActiveUserIDs []uuid.UUID `db:"active_user_ids" json:"active_user_ids"`
UsageVscodeSeconds int64 `db:"usage_vscode_seconds" json:"usage_vscode_seconds"`
UsageJetbrainsSeconds int64 `db:"usage_jetbrains_seconds" json:"usage_jetbrains_seconds"`
UsageReconnectingPtySeconds int64 `db:"usage_reconnecting_pty_seconds" json:"usage_reconnecting_pty_seconds"`
@@ -1538,13 +1723,14 @@ type GetTemplateInsightsRow struct {
}
// GetTemplateInsights has a granularity of 5 minutes where if a session/app was
-// in use, we will add 5 minutes to the total usage for that session (per user).
+// in use during a minute, we will add 5 minutes to the total usage for that
+// session/app (per user).
func (q *sqlQuerier) GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error) {
row := q.db.QueryRowContext(ctx, getTemplateInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
var i GetTemplateInsightsRow
err := row.Scan(
pq.Array(&i.TemplateIDs),
- &i.ActiveUsers,
+ pq.Array(&i.ActiveUserIDs),
&i.UsageVscodeSeconds,
&i.UsageJetbrainsSeconds,
&i.UsageReconnectingPtySeconds,
@@ -1560,15 +1746,15 @@ WITH latest_workspace_builds AS (
wbmax.template_id,
wb.template_version_id
FROM (
- SELECT
- tv.template_id, wbmax.workspace_id, MAX(wbmax.build_number) as max_build_number
+ SELECT
+ tv.template_id, wbmax.workspace_id, MAX(wbmax.build_number) as max_build_number
FROM workspace_builds wbmax
JOIN template_versions tv ON (tv.id = wbmax.template_version_id)
WHERE
wbmax.created_at >= $1::timestamptz
AND wbmax.created_at < $2::timestamptz
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN tv.template_id = ANY($3::uuid[]) ELSE TRUE END
- GROUP BY tv.template_id, wbmax.workspace_id
+ GROUP BY tv.template_id, wbmax.workspace_id
) wbmax
JOIN workspace_builds wb ON (
wb.workspace_id = wbmax.workspace_id
@@ -1580,18 +1766,20 @@ WITH latest_workspace_builds AS (
array_agg(DISTINCT wb.template_id)::uuid[] AS template_ids,
array_agg(wb.id)::uuid[] AS workspace_build_ids,
tvp.name,
+ tvp.type,
tvp.display_name,
tvp.description,
tvp.options
FROM latest_workspace_builds wb
JOIN template_version_parameters tvp ON (tvp.template_version_id = wb.template_version_id)
- GROUP BY tvp.name, tvp.display_name, tvp.description, tvp.options
+ GROUP BY tvp.name, tvp.type, tvp.display_name, tvp.description, tvp.options
)
SELECT
utp.num,
utp.template_ids,
utp.name,
+ utp.type,
utp.display_name,
utp.description,
utp.options,
@@ -1599,7 +1787,7 @@ SELECT
COUNT(wbp.value) AS count
FROM unique_template_params utp
JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.workspace_build_id] AND utp.name = wbp.name)
-GROUP BY utp.num, utp.name, utp.display_name, utp.description, utp.options, utp.template_ids, wbp.value
+GROUP BY utp.num, utp.template_ids, utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value
`
type GetTemplateParameterInsightsParams struct {
@@ -1612,6 +1800,7 @@ type GetTemplateParameterInsightsRow struct {
Num int64 `db:"num" json:"num"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
Name string `db:"name" json:"name"`
+ Type string `db:"type" json:"type"`
DisplayName string `db:"display_name" json:"display_name"`
Description string `db:"description" json:"description"`
Options json.RawMessage `db:"options" json:"options"`
@@ -1636,6 +1825,7 @@ func (q *sqlQuerier) GetTemplateParameterInsights(ctx context.Context, arg GetTe
&i.Num,
pq.Array(&i.TemplateIDs),
&i.Name,
+ &i.Type,
&i.DisplayName,
&i.Description,
&i.Options,
@@ -3329,11 +3519,13 @@ const getQuotaAllowanceForUser = `-- name: GetQuotaAllowanceForUser :one
SELECT
coalesce(SUM(quota_allowance), 0)::BIGINT
FROM
- group_members gm
-JOIN groups g ON
+ groups g
+LEFT JOIN group_members gm ON
g.id = gm.group_id
WHERE
user_id = $1
+OR
+ g.id = g.organization_id
`
func (q *sqlQuerier) GetQuotaAllowanceForUser(ctx context.Context, userID uuid.UUID) (int64, error) {
@@ -4125,7 +4317,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem
const getTemplateByID = `-- name: GetTemplateByID :one
SELECT
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username
+ id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username
FROM
template_with_users
WHERE
@@ -4158,8 +4350,8 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.AllowUserAutostart,
&i.AllowUserAutostop,
&i.FailureTTL,
- &i.InactivityTTL,
- &i.LockedTTL,
+ &i.TimeTilDormant,
+ &i.TimeTilDormantAutoDelete,
&i.RestartRequirementDaysOfWeek,
&i.RestartRequirementWeeks,
&i.CreatedByAvatarURL,
@@ -4170,7 +4362,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username
+ id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username
FROM
template_with_users AS templates
WHERE
@@ -4211,8 +4403,8 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.AllowUserAutostart,
&i.AllowUserAutostop,
&i.FailureTTL,
- &i.InactivityTTL,
- &i.LockedTTL,
+ &i.TimeTilDormant,
+ &i.TimeTilDormantAutoDelete,
&i.RestartRequirementDaysOfWeek,
&i.RestartRequirementWeeks,
&i.CreatedByAvatarURL,
@@ -4222,7 +4414,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
}
const getTemplates = `-- name: GetTemplates :many
-SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username FROM template_with_users AS templates
+SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username FROM template_with_users AS templates
ORDER BY (name, id) ASC
`
@@ -4256,8 +4448,8 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.AllowUserAutostart,
&i.AllowUserAutostop,
&i.FailureTTL,
- &i.InactivityTTL,
- &i.LockedTTL,
+ &i.TimeTilDormant,
+ &i.TimeTilDormantAutoDelete,
&i.RestartRequirementDaysOfWeek,
&i.RestartRequirementWeeks,
&i.CreatedByAvatarURL,
@@ -4278,7 +4470,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
SELECT
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, inactivity_ttl, locked_ttl, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username
+ id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, restart_requirement_days_of_week, restart_requirement_weeks, created_by_avatar_url, created_by_username
FROM
template_with_users AS templates
WHERE
@@ -4349,8 +4541,8 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.AllowUserAutostart,
&i.AllowUserAutostop,
&i.FailureTTL,
- &i.InactivityTTL,
- &i.LockedTTL,
+ &i.TimeTilDormant,
+ &i.TimeTilDormantAutoDelete,
&i.RestartRequirementDaysOfWeek,
&i.RestartRequirementWeeks,
&i.CreatedByAvatarURL,
@@ -4540,8 +4732,8 @@ SET
restart_requirement_days_of_week = $7,
restart_requirement_weeks = $8,
failure_ttl = $9,
- inactivity_ttl = $10,
- locked_ttl = $11
+ time_til_dormant = $10,
+ time_til_dormant_autodelete = $11
WHERE
id = $1
`
@@ -4556,8 +4748,8 @@ type UpdateTemplateScheduleByIDParams struct {
RestartRequirementDaysOfWeek int16 `db:"restart_requirement_days_of_week" json:"restart_requirement_days_of_week"`
RestartRequirementWeeks int64 `db:"restart_requirement_weeks" json:"restart_requirement_weeks"`
FailureTTL int64 `db:"failure_ttl" json:"failure_ttl"`
- InactivityTTL int64 `db:"inactivity_ttl" json:"inactivity_ttl"`
- LockedTTL int64 `db:"locked_ttl" json:"locked_ttl"`
+ TimeTilDormant int64 `db:"time_til_dormant" json:"time_til_dormant"`
+ TimeTilDormantAutoDelete int64 `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"`
}
func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error {
@@ -4571,8 +4763,8 @@ func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateT
arg.RestartRequirementDaysOfWeek,
arg.RestartRequirementWeeks,
arg.FailureTTL,
- arg.InactivityTTL,
- arg.LockedTTL,
+ arg.TimeTilDormant,
+ arg.TimeTilDormantAutoDelete,
)
return err
}
@@ -6168,61 +6360,120 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context) error {
return err
}
-const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one
+const getWorkspaceAgentAndOwnerByAuthToken = `-- name: GetWorkspaceAgentAndOwnerByAuthToken :one
SELECT
- id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
-FROM
- workspace_agents
+ workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.startup_script, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.startup_script_timeout_seconds, workspace_agents.expanded_directory, workspace_agents.shutdown_script, workspace_agents.shutdown_script_timeout_seconds, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.startup_script_behavior, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems,
+ workspaces.id AS workspace_id,
+ users.id AS owner_id,
+ users.username AS owner_name,
+ users.status AS owner_status,
+ array_cat(
+ array_append(users.rbac_roles, 'member'),
+ array_append(ARRAY[]::text[], 'organization-member:' || organization_members.organization_id::text)
+ )::text[] as owner_roles,
+ array_agg(COALESCE(group_members.group_id::text, ''))::text[] AS owner_groups
+FROM users
+ INNER JOIN
+ workspaces
+ ON
+ workspaces.owner_id = users.id
+ INNER JOIN
+ workspace_builds
+ ON
+ workspace_builds.workspace_id = workspaces.id
+ INNER JOIN
+ workspace_resources
+ ON
+ workspace_resources.job_id = workspace_builds.job_id
+ INNER JOIN
+ workspace_agents
+ ON
+ workspace_agents.resource_id = workspace_resources.id
+ INNER JOIN -- every user is a member of some org
+ organization_members
+ ON
+ organization_members.user_id = users.id
+ LEFT JOIN -- as they may not be a member of any groups
+ group_members
+ ON
+ group_members.user_id = users.id
WHERE
- auth_token = $1
+ -- TODO: we can add more conditions here, such as:
+ -- 1) The user must be active
+ -- 2) The user must not be deleted
+ -- 3) The workspace must be running
+ workspace_agents.auth_token = $1
+GROUP BY
+ workspace_agents.id,
+ workspaces.id,
+ users.id,
+ organization_members.organization_id,
+ workspace_builds.build_number
ORDER BY
- created_at DESC
+ workspace_builds.build_number DESC
+LIMIT 1
`
-func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error) {
- row := q.db.QueryRowContext(ctx, getWorkspaceAgentByAuthToken, authToken)
- var i WorkspaceAgent
+type GetWorkspaceAgentAndOwnerByAuthTokenRow struct {
+ WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
+ WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
+ OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
+ OwnerName string `db:"owner_name" json:"owner_name"`
+ OwnerStatus UserStatus `db:"owner_status" json:"owner_status"`
+ OwnerRoles []string `db:"owner_roles" json:"owner_roles"`
+ OwnerGroups []string `db:"owner_groups" json:"owner_groups"`
+}
+
+func (q *sqlQuerier) GetWorkspaceAgentAndOwnerByAuthToken(ctx context.Context, authToken uuid.UUID) (GetWorkspaceAgentAndOwnerByAuthTokenRow, error) {
+ row := q.db.QueryRowContext(ctx, getWorkspaceAgentAndOwnerByAuthToken, authToken)
+ var i GetWorkspaceAgentAndOwnerByAuthTokenRow
err := row.Scan(
- &i.ID,
- &i.CreatedAt,
- &i.UpdatedAt,
- &i.Name,
- &i.FirstConnectedAt,
- &i.LastConnectedAt,
- &i.DisconnectedAt,
- &i.ResourceID,
- &i.AuthToken,
- &i.AuthInstanceID,
- &i.Architecture,
- &i.EnvironmentVariables,
- &i.OperatingSystem,
- &i.StartupScript,
- &i.InstanceMetadata,
- &i.ResourceMetadata,
- &i.Directory,
- &i.Version,
- &i.LastConnectedReplicaID,
- &i.ConnectionTimeoutSeconds,
- &i.TroubleshootingURL,
- &i.MOTDFile,
- &i.LifecycleState,
- &i.StartupScriptTimeoutSeconds,
- &i.ExpandedDirectory,
- &i.ShutdownScript,
- &i.ShutdownScriptTimeoutSeconds,
- &i.LogsLength,
- &i.LogsOverflowed,
- &i.Subsystem,
- &i.StartupScriptBehavior,
- &i.StartedAt,
- &i.ReadyAt,
+ &i.WorkspaceAgent.ID,
+ &i.WorkspaceAgent.CreatedAt,
+ &i.WorkspaceAgent.UpdatedAt,
+ &i.WorkspaceAgent.Name,
+ &i.WorkspaceAgent.FirstConnectedAt,
+ &i.WorkspaceAgent.LastConnectedAt,
+ &i.WorkspaceAgent.DisconnectedAt,
+ &i.WorkspaceAgent.ResourceID,
+ &i.WorkspaceAgent.AuthToken,
+ &i.WorkspaceAgent.AuthInstanceID,
+ &i.WorkspaceAgent.Architecture,
+ &i.WorkspaceAgent.EnvironmentVariables,
+ &i.WorkspaceAgent.OperatingSystem,
+ &i.WorkspaceAgent.StartupScript,
+ &i.WorkspaceAgent.InstanceMetadata,
+ &i.WorkspaceAgent.ResourceMetadata,
+ &i.WorkspaceAgent.Directory,
+ &i.WorkspaceAgent.Version,
+ &i.WorkspaceAgent.LastConnectedReplicaID,
+ &i.WorkspaceAgent.ConnectionTimeoutSeconds,
+ &i.WorkspaceAgent.TroubleshootingURL,
+ &i.WorkspaceAgent.MOTDFile,
+ &i.WorkspaceAgent.LifecycleState,
+ &i.WorkspaceAgent.StartupScriptTimeoutSeconds,
+ &i.WorkspaceAgent.ExpandedDirectory,
+ &i.WorkspaceAgent.ShutdownScript,
+ &i.WorkspaceAgent.ShutdownScriptTimeoutSeconds,
+ &i.WorkspaceAgent.LogsLength,
+ &i.WorkspaceAgent.LogsOverflowed,
+ &i.WorkspaceAgent.StartupScriptBehavior,
+ &i.WorkspaceAgent.StartedAt,
+ &i.WorkspaceAgent.ReadyAt,
+ pq.Array(&i.WorkspaceAgent.Subsystems),
+ &i.WorkspaceID,
+ &i.OwnerID,
+ &i.OwnerName,
+ &i.OwnerStatus,
+ pq.Array(&i.OwnerRoles),
+ pq.Array(&i.OwnerGroups),
)
return i, err
}
const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one
SELECT
- id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
+ id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems
FROM
workspace_agents
WHERE
@@ -6262,17 +6513,17 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
- &i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
+ pq.Array(&i.Subsystems),
)
return i, err
}
const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one
SELECT
- id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
+ id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems
FROM
workspace_agents
WHERE
@@ -6314,10 +6565,10 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
- &i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
+ pq.Array(&i.Subsystems),
)
return i, err
}
@@ -6437,7 +6688,7 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, workspaceAge
const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
SELECT
- id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
+ id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems
FROM
workspace_agents
WHERE
@@ -6483,10 +6734,10 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
- &i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
+ pq.Array(&i.Subsystems),
); err != nil {
return nil, err
}
@@ -6502,7 +6753,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []
}
const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many
-SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at FROM workspace_agents WHERE created_at > $1
+SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems FROM workspace_agents WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
@@ -6544,10 +6795,10 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
- &i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
+ pq.Array(&i.Subsystems),
); err != nil {
return nil, err
}
@@ -6564,7 +6815,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created
const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many
SELECT
- workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.startup_script, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.startup_script_timeout_seconds, workspace_agents.expanded_directory, workspace_agents.shutdown_script, workspace_agents.shutdown_script_timeout_seconds, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.subsystem, workspace_agents.startup_script_behavior, workspace_agents.started_at, workspace_agents.ready_at
+ workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.startup_script, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.startup_script_timeout_seconds, workspace_agents.expanded_directory, workspace_agents.shutdown_script, workspace_agents.shutdown_script_timeout_seconds, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.startup_script_behavior, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems
FROM
workspace_agents
JOIN
@@ -6622,10 +6873,10 @@ func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Co
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
- &i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
+ pq.Array(&i.Subsystems),
); err != nil {
return nil, err
}
@@ -6666,7 +6917,7 @@ INSERT INTO
shutdown_script_timeout_seconds
)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, subsystem, startup_script_behavior, started_at, ready_at
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, startup_script_timeout_seconds, expanded_directory, shutdown_script, shutdown_script_timeout_seconds, logs_length, logs_overflowed, startup_script_behavior, started_at, ready_at, subsystems
`
type InsertWorkspaceAgentParams struct {
@@ -6748,10 +6999,10 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa
&i.ShutdownScriptTimeoutSeconds,
&i.LogsLength,
&i.LogsOverflowed,
- &i.Subsystem,
&i.StartupScriptBehavior,
&i.StartedAt,
&i.ReadyAt,
+ pq.Array(&i.Subsystems),
)
return i, err
}
@@ -6971,16 +7222,16 @@ UPDATE
SET
version = $2,
expanded_directory = $3,
- subsystem = $4
+ subsystems = $4
WHERE
id = $1
`
type UpdateWorkspaceAgentStartupByIDParams struct {
- ID uuid.UUID `db:"id" json:"id"`
- Version string `db:"version" json:"version"`
- ExpandedDirectory string `db:"expanded_directory" json:"expanded_directory"`
- Subsystem WorkspaceAgentSubsystem `db:"subsystem" json:"subsystem"`
+ ID uuid.UUID `db:"id" json:"id"`
+ Version string `db:"version" json:"version"`
+ ExpandedDirectory string `db:"expanded_directory" json:"expanded_directory"`
+ Subsystems []WorkspaceAgentSubsystem `db:"subsystems" json:"subsystems"`
}
func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error {
@@ -6988,7 +7239,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg Up
arg.ID,
arg.Version,
arg.ExpandedDirectory,
- arg.Subsystem,
+ pq.Array(arg.Subsystems),
)
return err
}
@@ -7418,6 +7669,90 @@ func (q *sqlQuerier) InsertWorkspaceAgentStat(ctx context.Context, arg InsertWor
return i, err
}
+const insertWorkspaceAgentStats = `-- name: InsertWorkspaceAgentStats :exec
+INSERT INTO
+ workspace_agent_stats (
+ id,
+ created_at,
+ user_id,
+ workspace_id,
+ template_id,
+ agent_id,
+ connections_by_proto,
+ connection_count,
+ rx_packets,
+ rx_bytes,
+ tx_packets,
+ tx_bytes,
+ session_count_vscode,
+ session_count_jetbrains,
+ session_count_reconnecting_pty,
+ session_count_ssh,
+ connection_median_latency_ms
+ )
+SELECT
+ unnest($1 :: uuid[]) AS id,
+ unnest($2 :: timestamptz[]) AS created_at,
+ unnest($3 :: uuid[]) AS user_id,
+ unnest($4 :: uuid[]) AS workspace_id,
+ unnest($5 :: uuid[]) AS template_id,
+ unnest($6 :: uuid[]) AS agent_id,
+ jsonb_array_elements($7 :: jsonb) AS connections_by_proto,
+ unnest($8 :: bigint[]) AS connection_count,
+ unnest($9 :: bigint[]) AS rx_packets,
+ unnest($10 :: bigint[]) AS rx_bytes,
+ unnest($11 :: bigint[]) AS tx_packets,
+ unnest($12 :: bigint[]) AS tx_bytes,
+ unnest($13 :: bigint[]) AS session_count_vscode,
+ unnest($14 :: bigint[]) AS session_count_jetbrains,
+ unnest($15 :: bigint[]) AS session_count_reconnecting_pty,
+ unnest($16 :: bigint[]) AS session_count_ssh,
+ unnest($17 :: double precision[]) AS connection_median_latency_ms
+`
+
+type InsertWorkspaceAgentStatsParams struct {
+ ID []uuid.UUID `db:"id" json:"id"`
+ CreatedAt []time.Time `db:"created_at" json:"created_at"`
+ UserID []uuid.UUID `db:"user_id" json:"user_id"`
+ WorkspaceID []uuid.UUID `db:"workspace_id" json:"workspace_id"`
+ TemplateID []uuid.UUID `db:"template_id" json:"template_id"`
+ AgentID []uuid.UUID `db:"agent_id" json:"agent_id"`
+ ConnectionsByProto json.RawMessage `db:"connections_by_proto" json:"connections_by_proto"`
+ ConnectionCount []int64 `db:"connection_count" json:"connection_count"`
+ RxPackets []int64 `db:"rx_packets" json:"rx_packets"`
+ RxBytes []int64 `db:"rx_bytes" json:"rx_bytes"`
+ TxPackets []int64 `db:"tx_packets" json:"tx_packets"`
+ TxBytes []int64 `db:"tx_bytes" json:"tx_bytes"`
+ SessionCountVSCode []int64 `db:"session_count_vscode" json:"session_count_vscode"`
+ SessionCountJetBrains []int64 `db:"session_count_jetbrains" json:"session_count_jetbrains"`
+ SessionCountReconnectingPTY []int64 `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
+ SessionCountSSH []int64 `db:"session_count_ssh" json:"session_count_ssh"`
+ ConnectionMedianLatencyMS []float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"`
+}
+
+func (q *sqlQuerier) InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error {
+ _, err := q.db.ExecContext(ctx, insertWorkspaceAgentStats,
+ pq.Array(arg.ID),
+ pq.Array(arg.CreatedAt),
+ pq.Array(arg.UserID),
+ pq.Array(arg.WorkspaceID),
+ pq.Array(arg.TemplateID),
+ pq.Array(arg.AgentID),
+ arg.ConnectionsByProto,
+ pq.Array(arg.ConnectionCount),
+ pq.Array(arg.RxPackets),
+ pq.Array(arg.RxBytes),
+ pq.Array(arg.TxPackets),
+ pq.Array(arg.TxBytes),
+ pq.Array(arg.SessionCountVSCode),
+ pq.Array(arg.SessionCountJetBrains),
+ pq.Array(arg.SessionCountReconnectingPTY),
+ pq.Array(arg.SessionCountSSH),
+ pq.Array(arg.ConnectionMedianLatencyMS),
+ )
+ return err
+}
+
const getWorkspaceAppByAgentIDAndSlug = `-- name: GetWorkspaceAppByAgentIDAndSlug :one
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external FROM workspace_apps WHERE agent_id = $1 AND slug = $2
`
@@ -7678,6 +8013,72 @@ func (q *sqlQuerier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg Updat
return err
}
+const insertWorkspaceAppStats = `-- name: InsertWorkspaceAppStats :exec
+INSERT INTO
+ workspace_app_stats (
+ user_id,
+ workspace_id,
+ agent_id,
+ access_method,
+ slug_or_port,
+ session_id,
+ session_started_at,
+ session_ended_at,
+ requests
+ )
+SELECT
+ unnest($1::uuid[]) AS user_id,
+ unnest($2::uuid[]) AS workspace_id,
+ unnest($3::uuid[]) AS agent_id,
+ unnest($4::text[]) AS access_method,
+ unnest($5::text[]) AS slug_or_port,
+ unnest($6::uuid[]) AS session_id,
+ unnest($7::timestamptz[]) AS session_started_at,
+ unnest($8::timestamptz[]) AS session_ended_at,
+ unnest($9::int[]) AS requests
+ON CONFLICT
+ (user_id, agent_id, session_id)
+DO
+ UPDATE SET
+ session_ended_at = EXCLUDED.session_ended_at,
+ requests = EXCLUDED.requests
+ WHERE
+ workspace_app_stats.user_id = EXCLUDED.user_id
+ AND workspace_app_stats.agent_id = EXCLUDED.agent_id
+ AND workspace_app_stats.session_id = EXCLUDED.session_id
+ -- Since stats are updated in place as time progresses, we only
+ -- want to update this row if it's fresh.
+ AND workspace_app_stats.session_ended_at <= EXCLUDED.session_ended_at
+ AND workspace_app_stats.requests <= EXCLUDED.requests
+`
+
+type InsertWorkspaceAppStatsParams struct {
+ UserID []uuid.UUID `db:"user_id" json:"user_id"`
+ WorkspaceID []uuid.UUID `db:"workspace_id" json:"workspace_id"`
+ AgentID []uuid.UUID `db:"agent_id" json:"agent_id"`
+ AccessMethod []string `db:"access_method" json:"access_method"`
+ SlugOrPort []string `db:"slug_or_port" json:"slug_or_port"`
+ SessionID []uuid.UUID `db:"session_id" json:"session_id"`
+ SessionStartedAt []time.Time `db:"session_started_at" json:"session_started_at"`
+ SessionEndedAt []time.Time `db:"session_ended_at" json:"session_ended_at"`
+ Requests []int32 `db:"requests" json:"requests"`
+}
+
+func (q *sqlQuerier) InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error {
+ _, err := q.db.ExecContext(ctx, insertWorkspaceAppStats,
+ pq.Array(arg.UserID),
+ pq.Array(arg.WorkspaceID),
+ pq.Array(arg.AgentID),
+ pq.Array(arg.AccessMethod),
+ pq.Array(arg.SlugOrPort),
+ pq.Array(arg.SessionID),
+ pq.Array(arg.SessionStartedAt),
+ pq.Array(arg.SessionEndedAt),
+ pq.Array(arg.Requests),
+ )
+ return err
+}
+
const getWorkspaceBuildParameters = `-- name: GetWorkspaceBuildParameters :many
SELECT
workspace_build_id, name, value
@@ -7731,6 +8132,72 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins
return err
}
+const getActiveWorkspaceBuildsByTemplateID = `-- name: GetActiveWorkspaceBuildsByTemplateID :many
+SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.initiator_by_avatar_url, wb.initiator_by_username
+FROM (
+ SELECT
+ workspace_id, MAX(build_number) as max_build_number
+ FROM
+ workspace_build_with_user AS workspace_builds
+ WHERE
+ workspace_id IN (
+ SELECT
+ id
+ FROM
+ workspaces
+ WHERE
+ template_id = $1
+ )
+ GROUP BY
+ workspace_id
+) m
+JOIN
+ workspace_build_with_user AS wb
+ ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number
+WHERE
+ wb.transition = 'start'::workspace_transition
+`
+
+func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error) {
+ rows, err := q.db.QueryContext(ctx, getActiveWorkspaceBuildsByTemplateID, templateID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []WorkspaceBuild
+ for rows.Next() {
+ var i WorkspaceBuild
+ if err := rows.Scan(
+ &i.ID,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ &i.WorkspaceID,
+ &i.TemplateVersionID,
+ &i.BuildNumber,
+ &i.Transition,
+ &i.InitiatorID,
+ &i.ProvisionerState,
+ &i.JobID,
+ &i.Deadline,
+ &i.Reason,
+ &i.DailyCost,
+ &i.MaxDeadline,
+ &i.InitiatorByAvatarUrl,
+ &i.InitiatorByUsername,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one
SELECT
id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline, initiator_by_avatar_url, initiator_by_username
@@ -8637,7 +9104,7 @@ func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploy
const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one
SELECT
- id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at
+ id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
FROM
workspaces
WHERE
@@ -8680,7 +9147,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
- &i.LockedAt,
+ &i.DormantAt,
&i.DeletingAt,
)
return i, err
@@ -8688,7 +9155,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
SELECT
- id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at
+ id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
FROM
workspaces
WHERE
@@ -8712,7 +9179,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
- &i.LockedAt,
+ &i.DormantAt,
&i.DeletingAt,
)
return i, err
@@ -8720,7 +9187,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one
SELECT
- id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at
+ id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
FROM
workspaces
WHERE
@@ -8751,7 +9218,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
- &i.LockedAt,
+ &i.DormantAt,
&i.DeletingAt,
)
return i, err
@@ -8759,7 +9226,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one
SELECT
- id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at
+ id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
FROM
workspaces
WHERE
@@ -8809,7 +9276,7 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
- &i.LockedAt,
+ &i.DormantAt,
&i.DeletingAt,
)
return i, err
@@ -8817,7 +9284,7 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace
const getWorkspaces = `-- name: GetWorkspaces :many
SELECT
- workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.locked_at, workspaces.deleting_at,
+ workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at,
COALESCE(template_name.template_name, 'unknown') as template_name,
latest_build.template_version_id,
latest_build.template_version_name,
@@ -9001,6 +9468,25 @@ WHERE
) > 0
ELSE true
END
+ -- Filter by dormant workspaces. By default we do not return dormant
+ -- workspaces since they are considered soft-deleted.
+ AND CASE
+ WHEN $10 :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN
+ dormant_at IS NOT NULL AND dormant_at >= $10
+ ELSE
+ dormant_at IS NULL
+ END
+ -- Filter by last_used
+ AND CASE
+ WHEN $11 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
+ workspaces.last_used_at <= $11
+ ELSE true
+ END
+ AND CASE
+ WHEN $12 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
+ workspaces.last_used_at >= $12
+ ELSE true
+ END
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
-- @authorize_filter
ORDER BY
@@ -9012,11 +9498,11 @@ ORDER BY
LOWER(workspaces.name) ASC
LIMIT
CASE
- WHEN $11 :: integer > 0 THEN
- $11
+ WHEN $14 :: integer > 0 THEN
+ $14
END
OFFSET
- $10
+ $13
`
type GetWorkspacesParams struct {
@@ -9029,6 +9515,9 @@ type GetWorkspacesParams struct {
Name string `db:"name" json:"name"`
HasAgent string `db:"has_agent" json:"has_agent"`
AgentInactiveDisconnectTimeoutSeconds int64 `db:"agent_inactive_disconnect_timeout_seconds" json:"agent_inactive_disconnect_timeout_seconds"`
+ DormantAt time.Time `db:"dormant_at" json:"dormant_at"`
+ LastUsedBefore time.Time `db:"last_used_before" json:"last_used_before"`
+ LastUsedAfter time.Time `db:"last_used_after" json:"last_used_after"`
Offset int32 `db:"offset_" json:"offset_"`
Limit int32 `db:"limit_" json:"limit_"`
}
@@ -9045,7 +9534,7 @@ type GetWorkspacesRow struct {
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
- LockedAt sql.NullTime `db:"locked_at" json:"locked_at"`
+ DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
@@ -9064,6 +9553,9 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
arg.Name,
arg.HasAgent,
arg.AgentInactiveDisconnectTimeoutSeconds,
+ arg.DormantAt,
+ arg.LastUsedBefore,
+ arg.LastUsedAfter,
arg.Offset,
arg.Limit,
)
@@ -9086,7 +9578,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
- &i.LockedAt,
+ &i.DormantAt,
&i.DeletingAt,
&i.TemplateName,
&i.TemplateVersionID,
@@ -9108,7 +9600,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many
SELECT
- workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.locked_at, workspaces.deleting_at
+ workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at
FROM
workspaces
LEFT JOIN
@@ -9157,17 +9649,17 @@ WHERE
) OR
-- If the workspace's template has an inactivity_ttl set
- -- it may be eligible for locking.
+ -- it may be eligible for dormancy.
(
- templates.inactivity_ttl > 0 AND
- workspaces.locked_at IS NULL
+ templates.time_til_dormant > 0 AND
+ workspaces.dormant_at IS NULL
) OR
- -- If the workspace's template has a locked_ttl set
- -- and the workspace is already locked
+ -- If the workspace's template has a time_til_dormant_autodelete set
+ -- and the workspace is already dormant.
(
- templates.locked_ttl > 0 AND
- workspaces.locked_at IS NOT NULL
+ templates.time_til_dormant_autodelete > 0 AND
+ workspaces.dormant_at IS NOT NULL
)
) AND workspaces.deleted = 'false'
`
@@ -9193,7 +9685,7 @@ func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
- &i.LockedAt,
+ &i.DormantAt,
&i.DeletingAt,
); err != nil {
return nil, err
@@ -9224,7 +9716,7 @@ INSERT INTO
last_used_at
)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
`
type InsertWorkspaceParams struct {
@@ -9266,12 +9758,30 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
- &i.LockedAt,
+ &i.DormantAt,
&i.DeletingAt,
)
return i, err
}
+const updateTemplateWorkspacesLastUsedAt = `-- name: UpdateTemplateWorkspacesLastUsedAt :exec
+UPDATE workspaces
+SET
+ last_used_at = $1::timestamptz
+WHERE
+ template_id = $2
+`
+
+type UpdateTemplateWorkspacesLastUsedAtParams struct {
+ LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
+ TemplateID uuid.UUID `db:"template_id" json:"template_id"`
+}
+
+func (q *sqlQuerier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error {
+ _, err := q.db.ExecContext(ctx, updateTemplateWorkspacesLastUsedAt, arg.LastUsedAt, arg.TemplateID)
+ return err
+}
+
const updateWorkspace = `-- name: UpdateWorkspace :one
UPDATE
workspaces
@@ -9280,7 +9790,7 @@ SET
WHERE
id = $1
AND deleted = false
-RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at, deleting_at
+RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
`
type UpdateWorkspaceParams struct {
@@ -9303,7 +9813,7 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
- &i.LockedAt,
+ &i.DormantAt,
&i.DeletingAt,
)
return i, err
@@ -9347,51 +9857,68 @@ func (q *sqlQuerier) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateW
return err
}
-const updateWorkspaceLastUsedAt = `-- name: UpdateWorkspaceLastUsedAt :exec
+const updateWorkspaceDormantDeletingAt = `-- name: UpdateWorkspaceDormantDeletingAt :one
UPDATE
workspaces
SET
- last_used_at = $2
+ dormant_at = $2,
+ -- When a workspace is active we want to update the last_used_at to avoid the workspace going
+ -- immediately dormant. If we're transition the workspace to dormant then we leave it alone.
+ last_used_at = CASE WHEN $2::timestamptz IS NULL THEN now() at time zone 'utc' ELSE last_used_at END,
+ -- If dormant_at is null (meaning active) or the template-defined time_til_dormant_autodelete is 0 we should set
+ -- deleting_at to NULL else set it to the dormant_at + time_til_dormant_autodelete duration.
+ deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.time_til_dormant_autodelete = 0 THEN NULL ELSE $2::timestamptz + INTERVAL '1 milliseconds' * templates.time_til_dormant_autodelete / 1000000 END
+FROM
+ templates
WHERE
- id = $1
+ workspaces.template_id = templates.id
+AND
+ workspaces.id = $1
+RETURNING workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at
`
-type UpdateWorkspaceLastUsedAtParams struct {
- ID uuid.UUID `db:"id" json:"id"`
- LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
+type UpdateWorkspaceDormantDeletingAtParams struct {
+ ID uuid.UUID `db:"id" json:"id"`
+ DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
}
-func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error {
- _, err := q.db.ExecContext(ctx, updateWorkspaceLastUsedAt, arg.ID, arg.LastUsedAt)
- return err
+func (q *sqlQuerier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (Workspace, error) {
+ row := q.db.QueryRowContext(ctx, updateWorkspaceDormantDeletingAt, arg.ID, arg.DormantAt)
+ var i Workspace
+ err := row.Scan(
+ &i.ID,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ &i.OwnerID,
+ &i.OrganizationID,
+ &i.TemplateID,
+ &i.Deleted,
+ &i.Name,
+ &i.AutostartSchedule,
+ &i.Ttl,
+ &i.LastUsedAt,
+ &i.DormantAt,
+ &i.DeletingAt,
+ )
+ return i, err
}
-const updateWorkspaceLockedDeletingAt = `-- name: UpdateWorkspaceLockedDeletingAt :exec
+const updateWorkspaceLastUsedAt = `-- name: UpdateWorkspaceLastUsedAt :exec
UPDATE
workspaces
SET
- locked_at = $2,
- -- When a workspace is unlocked we want to update the last_used_at to avoid the workspace getting re-locked.
- -- if we're locking the workspace then we leave it alone.
- last_used_at = CASE WHEN $2::timestamptz IS NULL THEN now() at time zone 'utc' ELSE last_used_at END,
- -- If locked_at is null (meaning unlocked) or the template-defined locked_ttl is 0 we should set
- -- deleting_at to NULL else set it to the locked_at + locked_ttl duration.
- deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.locked_ttl = 0 THEN NULL ELSE $2::timestamptz + INTERVAL '1 milliseconds' * templates.locked_ttl / 1000000 END
-FROM
- templates
+ last_used_at = $2
WHERE
- workspaces.template_id = templates.id
-AND
- workspaces.id = $1
+ id = $1
`
-type UpdateWorkspaceLockedDeletingAtParams struct {
- ID uuid.UUID `db:"id" json:"id"`
- LockedAt sql.NullTime `db:"locked_at" json:"locked_at"`
+type UpdateWorkspaceLastUsedAtParams struct {
+ ID uuid.UUID `db:"id" json:"id"`
+ LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
}
-func (q *sqlQuerier) UpdateWorkspaceLockedDeletingAt(ctx context.Context, arg UpdateWorkspaceLockedDeletingAtParams) error {
- _, err := q.db.ExecContext(ctx, updateWorkspaceLockedDeletingAt, arg.ID, arg.LockedAt)
+func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error {
+ _, err := q.db.ExecContext(ctx, updateWorkspaceLastUsedAt, arg.ID, arg.LastUsedAt)
return err
}
@@ -9414,23 +9941,28 @@ func (q *sqlQuerier) UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspace
return err
}
-const updateWorkspacesDeletingAtByTemplateID = `-- name: UpdateWorkspacesDeletingAtByTemplateID :exec
-UPDATE
- workspaces
+const updateWorkspacesDormantDeletingAtByTemplateID = `-- name: UpdateWorkspacesDormantDeletingAtByTemplateID :exec
+UPDATE workspaces
SET
- deleting_at = CASE WHEN $1::bigint = 0 THEN NULL ELSE locked_at + interval '1 milliseconds' * $1::bigint END
+ deleting_at = CASE
+ WHEN $1::bigint = 0 THEN NULL
+ WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN ($2::timestamptz) + interval '1 milliseconds' * $1::bigint
+ ELSE dormant_at + interval '1 milliseconds' * $1::bigint
+ END,
+ dormant_at = CASE WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN $2::timestamptz ELSE dormant_at END
WHERE
- template_id = $2
+ template_id = $3
AND
- locked_at IS NOT NULL
+ dormant_at IS NOT NULL
`
-type UpdateWorkspacesDeletingAtByTemplateIDParams struct {
- LockedTtlMs int64 `db:"locked_ttl_ms" json:"locked_ttl_ms"`
- TemplateID uuid.UUID `db:"template_id" json:"template_id"`
+type UpdateWorkspacesDormantDeletingAtByTemplateIDParams struct {
+ TimeTilDormantAutodeleteMs int64 `db:"time_til_dormant_autodelete_ms" json:"time_til_dormant_autodelete_ms"`
+ DormantAt time.Time `db:"dormant_at" json:"dormant_at"`
+ TemplateID uuid.UUID `db:"template_id" json:"template_id"`
}
-func (q *sqlQuerier) UpdateWorkspacesDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDeletingAtByTemplateIDParams) error {
- _, err := q.db.ExecContext(ctx, updateWorkspacesDeletingAtByTemplateID, arg.LockedTtlMs, arg.TemplateID)
+func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) error {
+ _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID)
return err
}
diff --git a/coderd/database/queries/groupmembers.sql b/coderd/database/queries/groupmembers.sql
index a7c04d85f20a8..0b3d0a33f4d54 100644
--- a/coderd/database/queries/groupmembers.sql
+++ b/coderd/database/queries/groupmembers.sql
@@ -3,12 +3,23 @@ SELECT
users.*
FROM
users
-JOIN
+-- If the group is a user made group, then we need to check the group_members table.
+LEFT JOIN
group_members
ON
- users.id = group_members.user_id
+ group_members.user_id = users.id AND
+ group_members.group_id = @group_id
+-- If it is the "Everyone" group, then we need to check the organization_members table.
+LEFT JOIN
+ organization_members
+ON
+ organization_members.user_id = users.id AND
+ organization_members.organization_id = @group_id
WHERE
- group_members.group_id = $1
+ -- In either case, the group_id will only match an org or a group.
+ (group_members.group_id = @group_id
+ OR
+ organization_members.organization_id = @group_id)
AND
users.status = 'active'
AND
diff --git a/coderd/database/queries/groups.sql b/coderd/database/queries/groups.sql
index e1ee6635a5fe0..e772d21a5840f 100644
--- a/coderd/database/queries/groups.sql
+++ b/coderd/database/queries/groups.sql
@@ -26,9 +26,7 @@ SELECT
FROM
groups
WHERE
- organization_id = $1
-AND
- id != $1;
+ organization_id = $1;
-- name: InsertGroup :one
INSERT INTO groups (
@@ -42,6 +40,28 @@ INSERT INTO groups (
VALUES
($1, $2, $3, $4, $5, $6) RETURNING *;
+-- name: InsertMissingGroups :many
+-- Inserts any group by name that does not exist. All new groups are given
+-- a random uuid, are inserted into the same organization. They have the default
+-- values for avatar, display name, and quota allowance (all zero values).
+INSERT INTO groups (
+ id,
+ name,
+ organization_id,
+ source
+)
+SELECT
+ gen_random_uuid(),
+ group_name,
+ @organization_id,
+ @source
+FROM
+ UNNEST(@group_names :: text[]) AS group_name
+-- If the name conflicts, do nothing.
+ON CONFLICT DO NOTHING
+RETURNING *;
+
+
-- We use the organization_id as the id
-- for simplicity since all users is
-- every member of the org.
diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql
index 94a80117dcc1d..d76d106edd5d1 100644
--- a/coderd/database/queries/insights.sql
+++ b/coderd/database/queries/insights.sql
@@ -23,68 +23,124 @@ ORDER BY user_id ASC;
-- name: GetTemplateInsights :one
-- GetTemplateInsights has a granularity of 5 minutes where if a session/app was
--- in use, we will add 5 minutes to the total usage for that session (per user).
-WITH d AS (
- -- Subtract 1 second from end_time to avoid including the next interval in the results.
- SELECT generate_series(@start_time::timestamptz, (@end_time::timestamptz) - '1 second'::interval, '5 minute'::interval) AS d
-), ts AS (
+-- in use during a minute, we will add 5 minutes to the total usage for that
+-- session/app (per user).
+WITH agent_stats_by_interval_and_user AS (
SELECT
- d::timestamptz AS from_,
- (d + '5 minute'::interval)::timestamptz AS to_,
- EXTRACT(epoch FROM '5 minute'::interval) AS seconds
- FROM d
-), usage_by_user AS (
- SELECT
- ts.from_,
- ts.to_,
+ date_trunc('minute', was.created_at),
was.user_id,
array_agg(was.template_id) AS template_ids,
- CASE WHEN SUM(was.session_count_vscode) > 0 THEN ts.seconds ELSE 0 END AS usage_vscode_seconds,
- CASE WHEN SUM(was.session_count_jetbrains) > 0 THEN ts.seconds ELSE 0 END AS usage_jetbrains_seconds,
- CASE WHEN SUM(was.session_count_reconnecting_pty) > 0 THEN ts.seconds ELSE 0 END AS usage_reconnecting_pty_seconds,
- CASE WHEN SUM(was.session_count_ssh) > 0 THEN ts.seconds ELSE 0 END AS usage_ssh_seconds
- FROM ts
- JOIN workspace_agent_stats was ON (
- was.created_at >= ts.from_
- AND was.created_at < ts.to_
+ CASE WHEN SUM(was.session_count_vscode) > 0 THEN 60 ELSE 0 END AS usage_vscode_seconds,
+ CASE WHEN SUM(was.session_count_jetbrains) > 0 THEN 60 ELSE 0 END AS usage_jetbrains_seconds,
+ CASE WHEN SUM(was.session_count_reconnecting_pty) > 0 THEN 60 ELSE 0 END AS usage_reconnecting_pty_seconds,
+ CASE WHEN SUM(was.session_count_ssh) > 0 THEN 60 ELSE 0 END AS usage_ssh_seconds
+ FROM workspace_agent_stats was
+ WHERE
+ was.created_at >= @start_time::timestamptz
+ AND was.created_at < @end_time::timestamptz
AND was.connection_count > 0
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN was.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
- )
- GROUP BY ts.from_, ts.to_, ts.seconds, was.user_id
+ GROUP BY date_trunc('minute', was.created_at), was.user_id
), template_ids AS (
SELECT array_agg(DISTINCT template_id) AS ids
- FROM usage_by_user, unnest(template_ids) template_id
+ FROM agent_stats_by_interval_and_user, unnest(template_ids) template_id
WHERE template_id IS NOT NULL
)
SELECT
COALESCE((SELECT ids FROM template_ids), '{}')::uuid[] AS template_ids,
- COUNT(DISTINCT user_id) AS active_users,
+ -- Return IDs so we can combine this with GetTemplateAppInsights.
+ COALESCE(array_agg(DISTINCT user_id), '{}')::uuid[] AS active_user_ids,
COALESCE(SUM(usage_vscode_seconds), 0)::bigint AS usage_vscode_seconds,
COALESCE(SUM(usage_jetbrains_seconds), 0)::bigint AS usage_jetbrains_seconds,
COALESCE(SUM(usage_reconnecting_pty_seconds), 0)::bigint AS usage_reconnecting_pty_seconds,
COALESCE(SUM(usage_ssh_seconds), 0)::bigint AS usage_ssh_seconds
-FROM usage_by_user;
+FROM agent_stats_by_interval_and_user;
+
+-- name: GetTemplateAppInsights :many
+-- GetTemplateAppInsights returns the aggregate usage of each app in a given
+-- timeframe. The result can be filtered on template_ids, meaning only user data
+-- from workspaces based on those templates will be included.
+WITH app_stats_by_user_and_agent AS (
+ SELECT
+ s.start_time,
+ 60 as seconds,
+ w.template_id,
+ was.user_id,
+ was.agent_id,
+ was.access_method,
+ was.slug_or_port,
+ wa.display_name,
+ wa.icon,
+ (wa.slug IS NOT NULL)::boolean AS is_app
+ FROM workspace_app_stats was
+ JOIN workspaces w ON (
+ w.id = was.workspace_id
+ AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN w.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
+ )
+ -- We do a left join here because we want to include user IDs that have used
+ -- e.g. ports when counting active users.
+ LEFT JOIN workspace_apps wa ON (
+ wa.agent_id = was.agent_id
+ AND wa.slug = was.slug_or_port
+ )
+ -- This table contains both 1 minute entries and >1 minute entries,
+ -- to calculate this with our uniqueness constraints, we generate series
+ -- for the longer intervals.
+ CROSS JOIN LATERAL generate_series(
+ date_trunc('minute', was.session_started_at),
+ -- Subtract 1 microsecond to avoid creating an extra series.
+ date_trunc('minute', was.session_ended_at - '1 microsecond'::interval),
+ '1 minute'::interval
+ ) s(start_time)
+ WHERE
+ s.start_time >= @start_time::timestamptz
+ -- Subtract one minute because the series only contains the start time.
+ AND s.start_time < (@end_time::timestamptz) - '1 minute'::interval
+ GROUP BY s.start_time, w.template_id, was.user_id, was.agent_id, was.access_method, was.slug_or_port, wa.display_name, wa.icon, wa.slug
+)
+
+SELECT
+ array_agg(DISTINCT template_id)::uuid[] AS template_ids,
+ -- Return IDs so we can combine this with GetTemplateInsights.
+ array_agg(DISTINCT user_id)::uuid[] AS active_user_ids,
+ access_method,
+ slug_or_port,
+ display_name,
+ icon,
+ is_app,
+ SUM(seconds) AS usage_seconds
+FROM app_stats_by_user_and_agent
+GROUP BY access_method, slug_or_port, display_name, icon, is_app;
-- name: GetTemplateDailyInsights :many
-- GetTemplateDailyInsights returns all daily intervals between start and end
-- time, if end time is a partial day, it will be included in the results and
-- that interval will be less than 24 hours. If there is no data for a selected
-- interval/template, it will be included in the results with 0 active users.
-WITH d AS (
- -- sqlc workaround, use SELECT generate_series instead of SELECT * FROM generate_series.
- -- Subtract 1 second from end_time to avoid including the next interval in the results.
- SELECT generate_series(@start_time::timestamptz, (@end_time::timestamptz) - '1 second'::interval, '1 day'::interval) AS d
-), ts AS (
+WITH ts AS (
SELECT
d::timestamptz AS from_,
- CASE WHEN (d + '1 day'::interval)::timestamptz <= @end_time::timestamptz THEN (d + '1 day'::interval)::timestamptz ELSE @end_time::timestamptz END AS to_
- FROM d
-), usage_by_day AS (
+ CASE
+ WHEN (d::timestamptz + '1 day'::interval) <= @end_time::timestamptz
+ THEN (d::timestamptz + '1 day'::interval)
+ ELSE @end_time::timestamptz
+ END AS to_
+ FROM
+ -- Subtract 1 second from end_time to avoid including the next interval in the results.
+ generate_series(@start_time::timestamptz, (@end_time::timestamptz) - '1 second'::interval, '1 day'::interval) AS d
+), unflattened_usage_by_day AS (
+ -- We select data from both workspace agent stats and workspace app stats to
+ -- get a complete picture of usage. This matches how usage is calculated by
+ -- the combination of GetTemplateInsights and GetTemplateAppInsights. We use
+ -- a union all to avoid a costly distinct operation.
+ --
+ -- Note that one query must perform a left join so that all intervals are
+ -- present at least once.
SELECT
ts.*,
- was.user_id,
- array_agg(was.template_id) AS template_ids
+ was.template_id,
+ was.user_id
FROM ts
LEFT JOIN workspace_agent_stats was ON (
was.created_at >= ts.from_
@@ -92,30 +148,35 @@ WITH d AS (
AND was.connection_count > 0
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN was.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
)
- GROUP BY ts.from_, ts.to_, was.user_id
-), template_ids AS (
+ GROUP BY ts.from_, ts.to_, was.template_id, was.user_id
+
+ UNION ALL
+
SELECT
- template_usage_by_day.from_,
- array_agg(template_id) AS ids
- FROM (
- SELECT DISTINCT
- from_,
- unnest(template_ids) AS template_id
- FROM usage_by_day
- ) AS template_usage_by_day
- WHERE template_id IS NOT NULL
- GROUP BY template_usage_by_day.from_
+ ts.*,
+ w.template_id,
+ was.user_id
+ FROM ts
+ JOIN workspace_app_stats was ON (
+ (was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
+ OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
+ OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
+ )
+ JOIN workspaces w ON (
+ w.id = was.workspace_id
+ AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN w.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
+ )
+ GROUP BY ts.from_, ts.to_, w.template_id, was.user_id
)
SELECT
from_ AS start_time,
to_ AS end_time,
- COALESCE((SELECT template_ids.ids FROM template_ids WHERE template_ids.from_ = usage_by_day.from_), '{}')::uuid[] AS template_ids,
+ array_remove(array_agg(DISTINCT template_id), NULL)::uuid[] AS template_ids,
COUNT(DISTINCT user_id) AS active_users
-FROM usage_by_day
+FROM unflattened_usage_by_day
GROUP BY from_, to_;
-
-- name: GetTemplateParameterInsights :many
-- GetTemplateParameterInsights does for each template in a given timeframe,
-- look for the latest workspace build (for every workspace) that has been
@@ -127,15 +188,15 @@ WITH latest_workspace_builds AS (
wbmax.template_id,
wb.template_version_id
FROM (
- SELECT
- tv.template_id, wbmax.workspace_id, MAX(wbmax.build_number) as max_build_number
+ SELECT
+ tv.template_id, wbmax.workspace_id, MAX(wbmax.build_number) as max_build_number
FROM workspace_builds wbmax
JOIN template_versions tv ON (tv.id = wbmax.template_version_id)
WHERE
wbmax.created_at >= @start_time::timestamptz
AND wbmax.created_at < @end_time::timestamptz
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN tv.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
- GROUP BY tv.template_id, wbmax.workspace_id
+ GROUP BY tv.template_id, wbmax.workspace_id
) wbmax
JOIN workspace_builds wb ON (
wb.workspace_id = wbmax.workspace_id
@@ -147,18 +208,20 @@ WITH latest_workspace_builds AS (
array_agg(DISTINCT wb.template_id)::uuid[] AS template_ids,
array_agg(wb.id)::uuid[] AS workspace_build_ids,
tvp.name,
+ tvp.type,
tvp.display_name,
tvp.description,
tvp.options
FROM latest_workspace_builds wb
JOIN template_version_parameters tvp ON (tvp.template_version_id = wb.template_version_id)
- GROUP BY tvp.name, tvp.display_name, tvp.description, tvp.options
+ GROUP BY tvp.name, tvp.type, tvp.display_name, tvp.description, tvp.options
)
SELECT
utp.num,
utp.template_ids,
utp.name,
+ utp.type,
utp.display_name,
utp.description,
utp.options,
@@ -166,4 +229,4 @@ SELECT
COUNT(wbp.value) AS count
FROM unique_template_params utp
JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.workspace_build_id] AND utp.name = wbp.name)
-GROUP BY utp.num, utp.name, utp.display_name, utp.description, utp.options, utp.template_ids, wbp.value;
+GROUP BY utp.num, utp.template_ids, utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value;
diff --git a/coderd/database/queries/quotas.sql b/coderd/database/queries/quotas.sql
index c640ba02ce982..48b9a673c7f03 100644
--- a/coderd/database/queries/quotas.sql
+++ b/coderd/database/queries/quotas.sql
@@ -2,11 +2,13 @@
SELECT
coalesce(SUM(quota_allowance), 0)::BIGINT
FROM
- group_members gm
-JOIN groups g ON
+ groups g
+LEFT JOIN group_members gm ON
g.id = gm.group_id
WHERE
- user_id = $1;
+ user_id = $1
+OR
+ g.id = g.organization_id;
-- name: GetQuotaConsumedForUser :one
WITH latest_builds AS (
diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql
index 7f4c9ce5de4ab..5387bea009c2d 100644
--- a/coderd/database/queries/templates.sql
+++ b/coderd/database/queries/templates.sql
@@ -121,8 +121,8 @@ SET
restart_requirement_days_of_week = $7,
restart_requirement_weeks = $8,
failure_ttl = $9,
- inactivity_ttl = $10,
- locked_ttl = $11
+ time_til_dormant = $10,
+ time_til_dormant_autodelete = $11
WHERE
id = $1
;
diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql
index 4025ac7e59a1b..9906d367e7bcf 100644
--- a/coderd/database/queries/workspaceagents.sql
+++ b/coderd/database/queries/workspaceagents.sql
@@ -1,13 +1,3 @@
--- name: GetWorkspaceAgentByAuthToken :one
-SELECT
- *
-FROM
- workspace_agents
-WHERE
- auth_token = $1
-ORDER BY
- created_at DESC;
-
-- name: GetWorkspaceAgentByID :one
SELECT
*
@@ -83,7 +73,7 @@ UPDATE
SET
version = $2,
expanded_directory = $3,
- subsystem = $4
+ subsystems = $4
WHERE
id = $1;
@@ -200,3 +190,56 @@ WHERE
WHERE
wb.workspace_id = @workspace_id :: uuid
);
+
+-- name: GetWorkspaceAgentAndOwnerByAuthToken :one
+SELECT
+ sqlc.embed(workspace_agents),
+ workspaces.id AS workspace_id,
+ users.id AS owner_id,
+ users.username AS owner_name,
+ users.status AS owner_status,
+ array_cat(
+ array_append(users.rbac_roles, 'member'),
+ array_append(ARRAY[]::text[], 'organization-member:' || organization_members.organization_id::text)
+ )::text[] as owner_roles,
+ array_agg(COALESCE(group_members.group_id::text, ''))::text[] AS owner_groups
+FROM users
+ INNER JOIN
+ workspaces
+ ON
+ workspaces.owner_id = users.id
+ INNER JOIN
+ workspace_builds
+ ON
+ workspace_builds.workspace_id = workspaces.id
+ INNER JOIN
+ workspace_resources
+ ON
+ workspace_resources.job_id = workspace_builds.job_id
+ INNER JOIN
+ workspace_agents
+ ON
+ workspace_agents.resource_id = workspace_resources.id
+ INNER JOIN -- every user is a member of some org
+ organization_members
+ ON
+ organization_members.user_id = users.id
+ LEFT JOIN -- as they may not be a member of any groups
+ group_members
+ ON
+ group_members.user_id = users.id
+WHERE
+ -- TODO: we can add more conditions here, such as:
+ -- 1) The user must be active
+ -- 2) The user must not be deleted
+ -- 3) The workspace must be running
+ workspace_agents.auth_token = @auth_token
+GROUP BY
+ workspace_agents.id,
+ workspaces.id,
+ users.id,
+ organization_members.organization_id,
+ workspace_builds.build_number
+ORDER BY
+ workspace_builds.build_number DESC
+LIMIT 1;
diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql
index 1a598bd6a6263..daba093a3d9e1 100644
--- a/coderd/database/queries/workspaceagentstats.sql
+++ b/coderd/database/queries/workspaceagentstats.sql
@@ -22,6 +22,46 @@ INSERT INTO
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING *;
+-- name: InsertWorkspaceAgentStats :exec
+INSERT INTO
+ workspace_agent_stats (
+ id,
+ created_at,
+ user_id,
+ workspace_id,
+ template_id,
+ agent_id,
+ connections_by_proto,
+ connection_count,
+ rx_packets,
+ rx_bytes,
+ tx_packets,
+ tx_bytes,
+ session_count_vscode,
+ session_count_jetbrains,
+ session_count_reconnecting_pty,
+ session_count_ssh,
+ connection_median_latency_ms
+ )
+SELECT
+ unnest(@id :: uuid[]) AS id,
+ unnest(@created_at :: timestamptz[]) AS created_at,
+ unnest(@user_id :: uuid[]) AS user_id,
+ unnest(@workspace_id :: uuid[]) AS workspace_id,
+ unnest(@template_id :: uuid[]) AS template_id,
+ unnest(@agent_id :: uuid[]) AS agent_id,
+ jsonb_array_elements(@connections_by_proto :: jsonb) AS connections_by_proto,
+ unnest(@connection_count :: bigint[]) AS connection_count,
+ unnest(@rx_packets :: bigint[]) AS rx_packets,
+ unnest(@rx_bytes :: bigint[]) AS rx_bytes,
+ unnest(@tx_packets :: bigint[]) AS tx_packets,
+ unnest(@tx_bytes :: bigint[]) AS tx_bytes,
+ unnest(@session_count_vscode :: bigint[]) AS session_count_vscode,
+ unnest(@session_count_jetbrains :: bigint[]) AS session_count_jetbrains,
+ unnest(@session_count_reconnecting_pty :: bigint[]) AS session_count_reconnecting_pty,
+ unnest(@session_count_ssh :: bigint[]) AS session_count_ssh,
+ unnest(@connection_median_latency_ms :: double precision[]) AS connection_median_latency_ms;
+
-- name: GetTemplateDAUs :many
SELECT
(created_at at TIME ZONE cast(@tz_offset::integer as text))::date as date,
diff --git a/coderd/database/queries/workspaceappstats.sql b/coderd/database/queries/workspaceappstats.sql
new file mode 100644
index 0000000000000..98da75e6972c7
--- /dev/null
+++ b/coderd/database/queries/workspaceappstats.sql
@@ -0,0 +1,37 @@
+-- name: InsertWorkspaceAppStats :exec
+INSERT INTO
+ workspace_app_stats (
+ user_id,
+ workspace_id,
+ agent_id,
+ access_method,
+ slug_or_port,
+ session_id,
+ session_started_at,
+ session_ended_at,
+ requests
+ )
+SELECT
+ unnest(@user_id::uuid[]) AS user_id,
+ unnest(@workspace_id::uuid[]) AS workspace_id,
+ unnest(@agent_id::uuid[]) AS agent_id,
+ unnest(@access_method::text[]) AS access_method,
+ unnest(@slug_or_port::text[]) AS slug_or_port,
+ unnest(@session_id::uuid[]) AS session_id,
+ unnest(@session_started_at::timestamptz[]) AS session_started_at,
+ unnest(@session_ended_at::timestamptz[]) AS session_ended_at,
+ unnest(@requests::int[]) AS requests
+ON CONFLICT
+ (user_id, agent_id, session_id)
+DO
+ UPDATE SET
+ session_ended_at = EXCLUDED.session_ended_at,
+ requests = EXCLUDED.requests
+ WHERE
+ workspace_app_stats.user_id = EXCLUDED.user_id
+ AND workspace_app_stats.agent_id = EXCLUDED.agent_id
+ AND workspace_app_stats.session_id = EXCLUDED.session_id
+ -- Since stats are updated in place as time progresses, we only
+ -- want to update this row if it's fresh.
+ AND workspace_app_stats.session_ended_at <= EXCLUDED.session_ended_at
+ AND workspace_app_stats.requests <= EXCLUDED.requests;
diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql
index ea2ccdb8d08ce..1020b729c4f27 100644
--- a/coderd/database/queries/workspacebuilds.sql
+++ b/coderd/database/queries/workspacebuilds.sql
@@ -144,3 +144,27 @@ SET
WHERE
id = $1;
+-- name: GetActiveWorkspaceBuildsByTemplateID :many
+SELECT wb.*
+FROM (
+ SELECT
+ workspace_id, MAX(build_number) as max_build_number
+ FROM
+ workspace_build_with_user AS workspace_builds
+ WHERE
+ workspace_id IN (
+ SELECT
+ id
+ FROM
+ workspaces
+ WHERE
+ template_id = $1
+ )
+ GROUP BY
+ workspace_id
+) m
+JOIN
+ workspace_build_with_user AS wb
+ ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number
+WHERE
+ wb.transition = 'start'::workspace_transition;
diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql
index 5e540a0e5c90a..0aa073301eb8f 100644
--- a/coderd/database/queries/workspaces.sql
+++ b/coderd/database/queries/workspaces.sql
@@ -259,6 +259,25 @@ WHERE
) > 0
ELSE true
END
+ -- Filter by dormant workspaces. By default we do not return dormant
+ -- workspaces since they are considered soft-deleted.
+ AND CASE
+ WHEN @dormant_at :: timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN
+ dormant_at IS NOT NULL AND dormant_at >= @dormant_at
+ ELSE
+ dormant_at IS NULL
+ END
+ -- Filter by last_used
+ AND CASE
+ WHEN @last_used_before :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
+ workspaces.last_used_at <= @last_used_before
+ ELSE true
+ END
+ AND CASE
+ WHEN @last_used_after :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
+ workspaces.last_used_at >= @last_used_after
+ ELSE true
+ END
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
-- @authorize_filter
ORDER BY
@@ -460,44 +479,56 @@ WHERE
) OR
-- If the workspace's template has an inactivity_ttl set
- -- it may be eligible for locking.
+ -- it may be eligible for dormancy.
(
- templates.inactivity_ttl > 0 AND
- workspaces.locked_at IS NULL
+ templates.time_til_dormant > 0 AND
+ workspaces.dormant_at IS NULL
) OR
- -- If the workspace's template has a locked_ttl set
- -- and the workspace is already locked
+ -- If the workspace's template has a time_til_dormant_autodelete set
+ -- and the workspace is already dormant.
(
- templates.locked_ttl > 0 AND
- workspaces.locked_at IS NOT NULL
+ templates.time_til_dormant_autodelete > 0 AND
+ workspaces.dormant_at IS NOT NULL
)
) AND workspaces.deleted = 'false';
--- name: UpdateWorkspaceLockedDeletingAt :exec
+-- name: UpdateWorkspaceDormantDeletingAt :one
UPDATE
workspaces
SET
- locked_at = $2,
- -- When a workspace is unlocked we want to update the last_used_at to avoid the workspace getting re-locked.
- -- if we're locking the workspace then we leave it alone.
+ dormant_at = $2,
+ -- When a workspace is active we want to update the last_used_at to avoid the workspace going
+ -- immediately dormant. If we're transition the workspace to dormant then we leave it alone.
last_used_at = CASE WHEN $2::timestamptz IS NULL THEN now() at time zone 'utc' ELSE last_used_at END,
- -- If locked_at is null (meaning unlocked) or the template-defined locked_ttl is 0 we should set
- -- deleting_at to NULL else set it to the locked_at + locked_ttl duration.
- deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.locked_ttl = 0 THEN NULL ELSE $2::timestamptz + INTERVAL '1 milliseconds' * templates.locked_ttl / 1000000 END
+ -- If dormant_at is null (meaning active) or the template-defined time_til_dormant_autodelete is 0 we should set
+ -- deleting_at to NULL else set it to the dormant_at + time_til_dormant_autodelete duration.
+ deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.time_til_dormant_autodelete = 0 THEN NULL ELSE $2::timestamptz + INTERVAL '1 milliseconds' * templates.time_til_dormant_autodelete / 1000000 END
FROM
templates
WHERE
workspaces.template_id = templates.id
AND
- workspaces.id = $1;
+ workspaces.id = $1
+RETURNING workspaces.*;
--- name: UpdateWorkspacesDeletingAtByTemplateID :exec
-UPDATE
- workspaces
+-- name: UpdateWorkspacesDormantDeletingAtByTemplateID :exec
+UPDATE workspaces
SET
- deleting_at = CASE WHEN @locked_ttl_ms::bigint = 0 THEN NULL ELSE locked_at + interval '1 milliseconds' * @locked_ttl_ms::bigint END
+ deleting_at = CASE
+ WHEN @time_til_dormant_autodelete_ms::bigint = 0 THEN NULL
+ WHEN @dormant_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN (@dormant_at::timestamptz) + interval '1 milliseconds' * @time_til_dormant_autodelete_ms::bigint
+ ELSE dormant_at + interval '1 milliseconds' * @time_til_dormant_autodelete_ms::bigint
+ END,
+ dormant_at = CASE WHEN @dormant_at::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN @dormant_at::timestamptz ELSE dormant_at END
WHERE
- template_id = @template_id
+ template_id = @template_id
AND
- locked_at IS NOT NULL;
+ dormant_at IS NOT NULL;
+
+-- name: UpdateTemplateWorkspacesLastUsedAt :exec
+UPDATE workspaces
+SET
+ last_used_at = @last_used_at::timestamptz
+WHERE
+ template_id = @template_id;
diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml
index 526f62d3a5ca5..7718b01e0335e 100644
--- a/coderd/database/sqlc.yaml
+++ b/coderd/database/sqlc.yaml
@@ -67,10 +67,10 @@ overrides:
motd_file: MOTDFile
uuid: UUID
failure_ttl: FailureTTL
- inactivity_ttl: InactivityTTL
+ time_til_dormant_autodelete: TimeTilDormantAutoDelete
eof: EOF
- locked_ttl: LockedTTL
template_ids: TemplateIDs
+ active_user_ids: ActiveUserIDs
sql:
- schema: "./dump.sql"
diff --git a/coderd/database/tx.go b/coderd/database/tx.go
new file mode 100644
index 0000000000000..43da15f3f058c
--- /dev/null
+++ b/coderd/database/tx.go
@@ -0,0 +1,49 @@
+package database
+
+import (
+ "database/sql"
+
+ "github.com/lib/pq"
+ "golang.org/x/xerrors"
+)
+
+const maxRetries = 5
+
+// ReadModifyUpdate is a helper function to run a db transaction that reads some
+// object(s), modifies some of the data, and writes the modified object(s) back
+// to the database. It is run in a transaction at RepeatableRead isolation so
+// that if another database client also modifies the data we are writing and
+// commits, then the transaction is rolled back and restarted.
+//
+// This is needed because we typically read all object columns, modify some
+// subset, and then write all columns. Consider an object with columns A, B and
+// initial values A=1, B=1. Two database clients work simultaneously, with one
+// client attempting to set A=2, and another attempting to set B=2. They both
+// initially read A=1, B=1, and then one writes A=2, B=1, and the other writes
+// A=1, B=2. With default PostgreSQL isolation of ReadCommitted, both of these
+// transactions would succeed and we end up with either A=2, B=1 or A=1, B=2.
+// One or other client gets their transaction wiped out even though the data
+// they wanted to change didn't conflict.
+//
+// If we run at RepeatableRead isolation, then one or other transaction will
+// fail. Let's say the transaction that sets A=2 succeeds. Then the first B=2
+// transaction fails, but here we retry. The second attempt we read A=2, B=1,
+// then write A=2, B=2 as desired, and this succeeds.
+func ReadModifyUpdate(db Store, f func(tx Store) error,
+) error {
+ var err error
+ for retries := 0; retries < maxRetries; retries++ {
+ err = db.InTx(f, &sql.TxOptions{
+ Isolation: sql.LevelRepeatableRead,
+ })
+ var pqe *pq.Error
+ if xerrors.As(err, &pqe) {
+ if pqe.Code == "40001" {
+ // serialization error, retry
+ continue
+ }
+ }
+ return err
+ }
+ return xerrors.Errorf("too many errors; last error: %w", err)
+}
diff --git a/coderd/database/tx_test.go b/coderd/database/tx_test.go
new file mode 100644
index 0000000000000..ff7569ef562df
--- /dev/null
+++ b/coderd/database/tx_test.go
@@ -0,0 +1,81 @@
+package database_test
+
+import (
+ "database/sql"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/lib/pq"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/xerrors"
+
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbmock"
+)
+
+func TestReadModifyUpdate_OK(t *testing.T) {
+ t.Parallel()
+
+ mDB := dbmock.NewMockStore(gomock.NewController(t))
+
+ mDB.EXPECT().
+ InTx(gomock.Any(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead}).
+ Times(1).
+ Return(nil)
+ err := database.ReadModifyUpdate(mDB, func(tx database.Store) error {
+ return nil
+ })
+ require.NoError(t, err)
+}
+
+func TestReadModifyUpdate_RetryOK(t *testing.T) {
+ t.Parallel()
+
+ mDB := dbmock.NewMockStore(gomock.NewController(t))
+
+ firstUpdate := mDB.EXPECT().
+ InTx(gomock.Any(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead}).
+ Times(1).
+ Return(&pq.Error{Code: pq.ErrorCode("40001")})
+ mDB.EXPECT().
+ InTx(gomock.Any(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead}).
+ After(firstUpdate).
+ Times(1).
+ Return(nil)
+
+ err := database.ReadModifyUpdate(mDB, func(tx database.Store) error {
+ return nil
+ })
+ require.NoError(t, err)
+}
+
+func TestReadModifyUpdate_HardError(t *testing.T) {
+ t.Parallel()
+
+ mDB := dbmock.NewMockStore(gomock.NewController(t))
+
+ mDB.EXPECT().
+ InTx(gomock.Any(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead}).
+ Times(1).
+ Return(xerrors.New("a bad thing happened"))
+
+ err := database.ReadModifyUpdate(mDB, func(tx database.Store) error {
+ return nil
+ })
+ require.ErrorContains(t, err, "a bad thing happened")
+}
+
+func TestReadModifyUpdate_TooManyRetries(t *testing.T) {
+ t.Parallel()
+
+ mDB := dbmock.NewMockStore(gomock.NewController(t))
+
+ mDB.EXPECT().
+ InTx(gomock.Any(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead}).
+ Times(5).
+ Return(&pq.Error{Code: pq.ErrorCode("40001")})
+ err := database.ReadModifyUpdate(mDB, func(tx database.Store) error {
+ return nil
+ })
+ require.ErrorContains(t, err, "too many errors")
+}
diff --git a/coderd/database/types.go b/coderd/database/types.go
index 629138de2cfda..c3a58329ac204 100644
--- a/coderd/database/types.go
+++ b/coderd/database/types.go
@@ -8,7 +8,7 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac"
)
// AuditOAuthConvertState is never stored in the database. It is stored in a cookie
diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go
index c8dbc831e8651..294b4b12d51af 100644
--- a/coderd/database/unique_constraint.go
+++ b/coderd/database/unique_constraint.go
@@ -18,6 +18,7 @@ const (
UniqueTemplateVersionParametersTemplateVersionIDNameKey UniqueConstraint = "template_version_parameters_template_version_id_name_key" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_name_key UNIQUE (template_version_id, name);
UniqueTemplateVersionVariablesTemplateVersionIDNameKey UniqueConstraint = "template_version_variables_template_version_id_name_key" // ALTER TABLE ONLY template_version_variables ADD CONSTRAINT template_version_variables_template_version_id_name_key UNIQUE (template_version_id, name);
UniqueTemplateVersionsTemplateIDNameKey UniqueConstraint = "template_versions_template_id_name_key" // ALTER TABLE ONLY template_versions ADD CONSTRAINT template_versions_template_id_name_key UNIQUE (template_id, name);
+ UniqueWorkspaceAppStatsUserIDAgentIDSessionIDKey UniqueConstraint = "workspace_app_stats_user_id_agent_id_session_id_key" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_agent_id_session_id_key UNIQUE (user_id, agent_id, session_id);
UniqueWorkspaceAppsAgentIDSlugIndex UniqueConstraint = "workspace_apps_agent_id_slug_idx" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug);
UniqueWorkspaceBuildParametersWorkspaceBuildIDNameKey UniqueConstraint = "workspace_build_parameters_workspace_build_id_name_key" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_name_key UNIQUE (workspace_build_id, name);
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
diff --git a/coderd/debug.go b/coderd/debug.go
index 045cb0076c953..eb48fd66b0308 100644
--- a/coderd/debug.go
+++ b/coderd/debug.go
@@ -5,10 +5,10 @@ import (
"net/http"
"time"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Debug Info Wireguard Coordinator
diff --git a/coderd/debug_test.go b/coderd/debug_test.go
index 2cff8f176c4c3..242d271b297b7 100644
--- a/coderd/debug_test.go
+++ b/coderd/debug_test.go
@@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/testutil"
)
func TestDebugHealth(t *testing.T) {
diff --git a/coderd/deployment.go b/coderd/deployment.go
index 5f12f39cc3461..255f9c7ac2a8d 100644
--- a/coderd/deployment.go
+++ b/coderd/deployment.go
@@ -4,10 +4,10 @@ import (
"net/http"
"net/url"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Get deployment config
diff --git a/coderd/deployment_test.go b/coderd/deployment_test.go
index d525166993aac..617947e6eb607 100644
--- a/coderd/deployment_test.go
+++ b/coderd/deployment_test.go
@@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/testutil"
)
func TestDeploymentValues(t *testing.T) {
diff --git a/coderd/deprecated.go b/coderd/deprecated.go
index 59e23b8d48136..f656451a83edd 100644
--- a/coderd/deprecated.go
+++ b/coderd/deprecated.go
@@ -3,7 +3,7 @@ package coderd
import (
"net/http"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
// @Summary Removed: Get parameters by template version
diff --git a/coderd/devtunnel/servers.go b/coderd/devtunnel/servers.go
index 1ac1b6ce26a7c..db909d2e1db0e 100644
--- a/coderd/devtunnel/servers.go
+++ b/coderd/devtunnel/servers.go
@@ -10,7 +10,8 @@ import (
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/cryptorand"
)
type Region struct {
@@ -115,8 +116,8 @@ func FindClosestNode(nodes []Node) (Node, error) {
return Node{}, err
}
- slices.SortFunc(nodes, func(i, j Node) bool {
- return i.AvgLatency < j.AvgLatency
+ slices.SortFunc(nodes, func(a, b Node) int {
+ return slice.Ascending(a.AvgLatency, b.AvgLatency)
})
return nodes[0], nil
}
diff --git a/coderd/devtunnel/tunnel.go b/coderd/devtunnel/tunnel.go
index 11bbc7dad6bee..d61976cef4f32 100644
--- a/coderd/devtunnel/tunnel.go
+++ b/coderd/devtunnel/tunnel.go
@@ -15,8 +15,8 @@ import (
"golang.zx2c4.com/wireguard/device"
"cdr.dev/slog"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/cryptorand"
"github.com/coder/wgtunnel/tunnelsdk"
)
diff --git a/coderd/devtunnel/tunnel_test.go b/coderd/devtunnel/tunnel_test.go
index a389001523375..1dc804c8ee660 100644
--- a/coderd/devtunnel/tunnel_test.go
+++ b/coderd/devtunnel/tunnel_test.go
@@ -22,8 +22,8 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/devtunnel"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/devtunnel"
+ "github.com/coder/coder/v2/testutil"
"github.com/coder/wgtunnel/tunneld"
"github.com/coder/wgtunnel/tunnelsdk"
)
diff --git a/coderd/dormancy/dormantusersjob.go b/coderd/dormancy/dormantusersjob.go
index be1e0cf0fe61b..8f69ad6260e5e 100644
--- a/coderd/dormancy/dormantusersjob.go
+++ b/coderd/dormancy/dormantusersjob.go
@@ -9,7 +9,7 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
)
const (
diff --git a/coderd/dormancy/dormantusersjob_test.go b/coderd/dormancy/dormantusersjob_test.go
index 73224da872c6e..f937589faac76 100644
--- a/coderd/dormancy/dormantusersjob_test.go
+++ b/coderd/dormancy/dormantusersjob_test.go
@@ -11,10 +11,10 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/dormancy"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/dormancy"
+ "github.com/coder/coder/v2/testutil"
)
func TestCheckInactiveUsers(t *testing.T) {
diff --git a/coderd/experiments.go b/coderd/experiments.go
index 651ae3155c369..1a8bb5ce1812a 100644
--- a/coderd/experiments.go
+++ b/coderd/experiments.go
@@ -3,7 +3,7 @@ package coderd
import (
"net/http"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
// @Summary Get experiments
diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go
index 5526d8324d7e3..0f498e7e7cf2b 100644
--- a/coderd/experiments_test.go
+++ b/coderd/experiments_test.go
@@ -6,10 +6,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func Test_Experiments(t *testing.T) {
diff --git a/coderd/files.go b/coderd/files.go
index 486ef26b90c90..842761236cb6b 100644
--- a/coderd/files.go
+++ b/coderd/files.go
@@ -12,10 +12,10 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
const (
diff --git a/coderd/files_test.go b/coderd/files_test.go
index 0841785c8c660..1a3f407a6e1f6 100644
--- a/coderd/files_test.go
+++ b/coderd/files_test.go
@@ -9,9 +9,9 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestPostFiles(t *testing.T) {
diff --git a/coderd/gitauth.go b/coderd/gitauth.go
index 537f0b8794e32..5bab419662935 100644
--- a/coderd/gitauth.go
+++ b/coderd/gitauth.go
@@ -8,11 +8,11 @@ import (
"golang.org/x/sync/errgroup"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Get git auth by ID
diff --git a/coderd/gitauth/askpass_test.go b/coderd/gitauth/askpass_test.go
index ce7cc75989603..72fd6319a2303 100644
--- a/coderd/gitauth/askpass_test.go
+++ b/coderd/gitauth/askpass_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/gitauth"
)
func TestCheckCommand(t *testing.T) {
diff --git a/coderd/gitauth/config.go b/coderd/gitauth/config.go
index 29d4804dcd538..31b0f052fcd9e 100644
--- a/coderd/gitauth/config.go
+++ b/coderd/gitauth/config.go
@@ -8,15 +8,17 @@ import (
"net/http"
"net/url"
"regexp"
+ "time"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
"github.com/google/go-github/v43/github"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/retry"
)
type OAuth2Config interface {
@@ -75,12 +77,26 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, gitAuthLin
// we aren't trying to surface an error, we're just trying to obtain a valid token.
return gitAuthLink, false, nil
}
-
+ r := retry.New(50*time.Millisecond, 200*time.Millisecond)
+ // See the comment below why the retry and cancel is required.
+ retryCtx, retryCtxCancel := context.WithTimeout(ctx, time.Second)
+ defer retryCtxCancel()
+validate:
valid, _, err := c.ValidateToken(ctx, token.AccessToken)
if err != nil {
return gitAuthLink, false, xerrors.Errorf("validate git auth token: %w", err)
}
if !valid {
+ // A customer using GitHub in Australia reported that validating immediately
+ // after refreshing the token would intermittently fail with a 401. Waiting
+ // a few milliseconds with the exact same token on the exact same request
+ // would resolve the issue. It seems likely that the write is not propagating
+ // to the read replica in time.
+ //
+ // We do an exponential backoff here to give the write time to propagate.
+ if c.Type == codersdk.GitProviderGitHub && r.Wait(retryCtx) {
+ goto validate
+ }
// The token is no longer valid!
return gitAuthLink, false, nil
}
diff --git a/coderd/gitauth/config_test.go b/coderd/gitauth/config_test.go
index 31d6392341426..bcd650e82ad3a 100644
--- a/coderd/gitauth/config_test.go
+++ b/coderd/gitauth/config_test.go
@@ -12,12 +12,12 @@ import (
"golang.org/x/oauth2"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestRefreshToken(t *testing.T) {
@@ -73,6 +73,39 @@ func TestRefreshToken(t *testing.T) {
require.NoError(t, err)
require.False(t, refreshed)
})
+ t.Run("ValidateRetryGitHub", func(t *testing.T) {
+ t.Parallel()
+ hit := false
+ // We need to ensure that the exponential backoff kicks in properly.
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !hit {
+ hit = true
+ w.WriteHeader(http.StatusUnauthorized)
+ w.Write([]byte("Not permitted"))
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ }))
+ config := &gitauth.Config{
+ ID: "test",
+ OAuth2Config: &testutil.OAuth2Config{
+ Token: &oauth2.Token{
+ AccessToken: "updated",
+ },
+ },
+ ValidateURL: srv.URL,
+ Type: codersdk.GitProviderGitHub,
+ }
+ db := dbfake.New()
+ link := dbgen.GitAuthLink(t, db, database.GitAuthLink{
+ ProviderID: config.ID,
+ OAuthAccessToken: "initial",
+ })
+ _, refreshed, err := config.RefreshToken(context.Background(), db, link)
+ require.NoError(t, err)
+ require.True(t, refreshed)
+ require.True(t, hit)
+ })
t.Run("ValidateNoUpdate", func(t *testing.T) {
t.Parallel()
validated := make(chan struct{})
diff --git a/coderd/gitauth/oauth.go b/coderd/gitauth/oauth.go
index daba6a8faf075..63e938fb756e7 100644
--- a/coderd/gitauth/oauth.go
+++ b/coderd/gitauth/oauth.go
@@ -12,8 +12,8 @@ import (
"golang.org/x/oauth2/github"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
)
// endpoint contains default SaaS URLs for each Git provider.
diff --git a/coderd/gitauth/vscode_test.go b/coderd/gitauth/vscode_test.go
index f61fb97ea681a..f940f151aadc3 100644
--- a/coderd/gitauth/vscode_test.go
+++ b/coderd/gitauth/vscode_test.go
@@ -10,7 +10,7 @@ import (
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/gitauth"
)
func TestOverrideVSCodeConfigs(t *testing.T) {
diff --git a/coderd/gitauth_test.go b/coderd/gitauth_test.go
index 578f7bea145c5..c0ad89a1b53cc 100644
--- a/coderd/gitauth_test.go
+++ b/coderd/gitauth_test.go
@@ -16,15 +16,14 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
func TestGitAuthByID(t *testing.T) {
@@ -227,7 +226,7 @@ func TestGitAuthCallback(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -256,24 +255,9 @@ func TestGitAuthCallback(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Resources: []*proto.Resource{{
- Name: "example",
- Type: "aws_instance",
- Agents: []*proto.Agent{{
- Id: uuid.NewString(),
- Auth: &proto.Agent_Token{
- Token: authToken,
- },
- }},
- }},
- },
- },
- }},
+ Parse: echo.ParseComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
@@ -342,7 +326,7 @@ func TestGitAuthCallback(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -400,7 +384,7 @@ func TestGitAuthCallback(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -443,7 +427,7 @@ func TestGitAuthCallback(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
diff --git a/coderd/gitsshkey.go b/coderd/gitsshkey.go
index 096844caa0a28..d4d3ed5f14775 100644
--- a/coderd/gitsshkey.go
+++ b/coderd/gitsshkey.go
@@ -3,13 +3,13 @@ package coderd
import (
"net/http"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/gitsshkey"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/gitsshkey"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
)
// @Summary Regenerate user SSH key
diff --git a/coderd/gitsshkey/gitsshkey_test.go b/coderd/gitsshkey/gitsshkey_test.go
index e5dc9b9c6faf8..88ddbfc598930 100644
--- a/coderd/gitsshkey/gitsshkey_test.go
+++ b/coderd/gitsshkey/gitsshkey_test.go
@@ -6,8 +6,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
- "github.com/coder/coder/coderd/gitsshkey"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/gitsshkey"
+ "github.com/coder/coder/v2/cryptorand"
)
func TestGitSSHKeys(t *testing.T) {
diff --git a/coderd/gitsshkey_test.go b/coderd/gitsshkey_test.go
index 42b227869c7a7..be1f43c52eb2f 100644
--- a/coderd/gitsshkey_test.go
+++ b/coderd/gitsshkey_test.go
@@ -8,13 +8,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/gitsshkey"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/gitsshkey"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
func TestGitSSHKey(t *testing.T) {
@@ -108,7 +108,7 @@ func TestAgentGitSSHKey(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
diff --git a/coderd/healthcheck/accessurl.go b/coderd/healthcheck/accessurl.go
index b91889b2842a2..6f86944b7ca4e 100644
--- a/coderd/healthcheck/accessurl.go
+++ b/coderd/healthcheck/accessurl.go
@@ -9,7 +9,7 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/util/ptr"
+ "github.com/coder/coder/v2/coderd/util/ptr"
)
// @typescript-generate AccessURLReport
diff --git a/coderd/healthcheck/accessurl_test.go b/coderd/healthcheck/accessurl_test.go
index 6097d6cb50810..3464030b61eb1 100644
--- a/coderd/healthcheck/accessurl_test.go
+++ b/coderd/healthcheck/accessurl_test.go
@@ -11,8 +11,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/healthcheck"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/healthcheck"
)
func TestAccessURL(t *testing.T) {
diff --git a/coderd/healthcheck/database.go b/coderd/healthcheck/database.go
index c92ef3c447d56..70005dc5b3d9f 100644
--- a/coderd/healthcheck/database.go
+++ b/coderd/healthcheck/database.go
@@ -7,7 +7,7 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
)
// @typescript-generate DatabaseReport
diff --git a/coderd/healthcheck/database_test.go b/coderd/healthcheck/database_test.go
index 615728a8b573b..f6c2782aacacd 100644
--- a/coderd/healthcheck/database_test.go
+++ b/coderd/healthcheck/database_test.go
@@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database/dbmock"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database/dbmock"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/testutil"
)
func TestDatabase(t *testing.T) {
diff --git a/coderd/healthcheck/derp.go b/coderd/healthcheck/derp.go
index 9fc88a37e40db..d3b627f29a539 100644
--- a/coderd/healthcheck/derp.go
+++ b/coderd/healthcheck/derp.go
@@ -21,7 +21,7 @@ import (
"tailscale.com/types/key"
tslogger "tailscale.com/types/logger"
- "github.com/coder/coder/coderd/util/ptr"
+ "github.com/coder/coder/v2/coderd/util/ptr"
)
// @typescript-generate DERPReport
@@ -118,7 +118,7 @@ func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) {
mu.Unlock()
}
nc := &netcheck.Client{
- PortMapper: portmapper.NewClient(tslogger.WithPrefix(ncLogf, "portmap: "), nil),
+ PortMapper: portmapper.NewClient(tslogger.WithPrefix(ncLogf, "portmap: "), nil, nil, nil),
Logf: tslogger.WithPrefix(ncLogf, "netcheck: "),
}
ncReport, netcheckErr := nc.GetReport(ctx, opts.DERPMap)
diff --git a/coderd/healthcheck/derp_test.go b/coderd/healthcheck/derp_test.go
index d27cdc182ec31..f291170531d2f 100644
--- a/coderd/healthcheck/derp_test.go
+++ b/coderd/healthcheck/derp_test.go
@@ -17,9 +17,9 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/key"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/testutil"
)
//nolint:tparallel
diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go
index 29a4398b8391c..0a29165db9839 100644
--- a/coderd/healthcheck/healthcheck.go
+++ b/coderd/healthcheck/healthcheck.go
@@ -10,9 +10,9 @@ import (
"tailscale.com/tailcfg"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/util/ptr"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/util/ptr"
)
const (
diff --git a/coderd/healthcheck/healthcheck_test.go b/coderd/healthcheck/healthcheck_test.go
index 26be9021eaf46..40f5efd586fc7 100644
--- a/coderd/healthcheck/healthcheck_test.go
+++ b/coderd/healthcheck/healthcheck_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/coderd/healthcheck"
+ "github.com/coder/coder/v2/coderd/healthcheck"
)
type testChecker struct {
diff --git a/coderd/healthcheck/websocket_test.go b/coderd/healthcheck/websocket_test.go
index cb56081197577..44df237a49cbb 100644
--- a/coderd/healthcheck/websocket_test.go
+++ b/coderd/healthcheck/websocket_test.go
@@ -11,8 +11,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/testutil"
)
func TestWebsocket(t *testing.T) {
diff --git a/coderd/httpapi/cookie.go b/coderd/httpapi/cookie.go
index 289ee2188cf06..4879478cb73b9 100644
--- a/coderd/httpapi/cookie.go
+++ b/coderd/httpapi/cookie.go
@@ -4,7 +4,7 @@ import (
"net/textproto"
"strings"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
// StripCoderCookies removes the session token from the cookie header provided.
diff --git a/coderd/httpapi/cookie_test.go b/coderd/httpapi/cookie_test.go
index 48c66abc439f0..4d44cd8f7d130 100644
--- a/coderd/httpapi/cookie_test.go
+++ b/coderd/httpapi/cookie_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
func TestStripCoderCookies(t *testing.T) {
diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go
index b7559d5feeabe..0691edd8e9d0f 100644
--- a/coderd/httpapi/httpapi.go
+++ b/coderd/httpapi/httpapi.go
@@ -16,10 +16,10 @@ import (
"github.com/go-playground/validator/v10"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
)
var Validate *validator.Validate
diff --git a/coderd/httpapi/httpapi_test.go b/coderd/httpapi/httpapi_test.go
index ea6d5a92ea5be..635ed2bdc1e29 100644
--- a/coderd/httpapi/httpapi_test.go
+++ b/coderd/httpapi/httpapi_test.go
@@ -14,8 +14,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
func TestInternalServerError(t *testing.T) {
diff --git a/coderd/httpapi/json_test.go b/coderd/httpapi/json_test.go
index 62e5c546a0b4b..a0a93e884d44f 100644
--- a/coderd/httpapi/json_test.go
+++ b/coderd/httpapi/json_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
func TestDuration(t *testing.T) {
diff --git a/coderd/httpapi/name_test.go b/coderd/httpapi/name_test.go
index b78a15867ca53..e28115eecbbd7 100644
--- a/coderd/httpapi/name_test.go
+++ b/coderd/httpapi/name_test.go
@@ -6,7 +6,7 @@ import (
"github.com/moby/moby/pkg/namesgenerator"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
func TestUsernameValid(t *testing.T) {
diff --git a/coderd/httpapi/queryparams.go b/coderd/httpapi/queryparams.go
index 3f16565e1dd20..1ff9abc7961ea 100644
--- a/coderd/httpapi/queryparams.go
+++ b/coderd/httpapi/queryparams.go
@@ -10,8 +10,8 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
)
// QueryParamParser is a helper for parsing all query params and gathering all
@@ -45,7 +45,7 @@ func (p *QueryParamParser) ErrorExcessParams(values url.Values) {
if _, ok := p.Parsed[k]; !ok {
p.Errors = append(p.Errors, codersdk.ValidationError{
Field: k,
- Detail: fmt.Sprintf("Query param %q is not a valid query param", k),
+ Detail: fmt.Sprintf("%q is not a valid query param", k),
})
}
}
diff --git a/coderd/httpapi/queryparams_test.go b/coderd/httpapi/queryparams_test.go
index ecf7b3a99cf40..da0dac4ad0aa0 100644
--- a/coderd/httpapi/queryparams_test.go
+++ b/coderd/httpapi/queryparams_test.go
@@ -10,8 +10,8 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
type queryParamTestCase[T any] struct {
diff --git a/coderd/httpapi/url_test.go b/coderd/httpapi/url_test.go
index 3beee451f7391..8c1dfd8995b94 100644
--- a/coderd/httpapi/url_test.go
+++ b/coderd/httpapi/url_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
func TestApplicationURLString(t *testing.T) {
diff --git a/coderd/httpmw/actor.go b/coderd/httpmw/actor.go
index 7df5294b17c49..af3142aed2de8 100644
--- a/coderd/httpmw/actor.go
+++ b/coderd/httpmw/actor.go
@@ -3,8 +3,8 @@ package httpmw
import (
"net/http"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
// RequireAPIKeyOrWorkspaceProxyAuth is middleware that should be inserted after
diff --git a/coderd/httpmw/actor_test.go b/coderd/httpmw/actor_test.go
index 5d30f5c072eda..deb529b0fd983 100644
--- a/coderd/httpmw/actor_test.go
+++ b/coderd/httpmw/actor_test.go
@@ -10,11 +10,11 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func TestRequireAPIKeyOrWorkspaceProxyAuth(t *testing.T) {
diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go
index 5f0ec0dc263c7..5335c29676376 100644
--- a/coderd/httpmw/apikey.go
+++ b/coderd/httpmw/apikey.go
@@ -18,11 +18,11 @@ import (
"golang.org/x/oauth2"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
type apiKeyContextKey struct{}
@@ -142,6 +142,56 @@ func ExtractAPIKeyMW(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler {
}
}
+func APIKeyFromRequest(ctx context.Context, db database.Store, sessionTokenFunc func(r *http.Request) string, r *http.Request) (*database.APIKey, codersdk.Response, bool) {
+ tokenFunc := APITokenFromRequest
+ if sessionTokenFunc != nil {
+ tokenFunc = sessionTokenFunc
+ }
+
+ token := tokenFunc(r)
+ if token == "" {
+ return nil, codersdk.Response{
+ Message: SignedOutErrorMessage,
+ Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenCookie),
+ }, false
+ }
+
+ keyID, keySecret, err := SplitAPIToken(token)
+ if err != nil {
+ return nil, codersdk.Response{
+ Message: SignedOutErrorMessage,
+ Detail: "Invalid API key format: " + err.Error(),
+ }, false
+ }
+
+ //nolint:gocritic // System needs to fetch API key to check if it's valid.
+ key, err := db.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx), keyID)
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, codersdk.Response{
+ Message: SignedOutErrorMessage,
+ Detail: "API key is invalid.",
+ }, false
+ }
+
+ return nil, codersdk.Response{
+ Message: internalErrorMessage,
+ Detail: fmt.Sprintf("Internal error fetching API key by id. %s", err.Error()),
+ }, false
+ }
+
+ // Checking to see if the secret is valid.
+ hashedSecret := sha256.Sum256([]byte(keySecret))
+ if subtle.ConstantTimeCompare(key.HashedSecret, hashedSecret[:]) != 1 {
+ return nil, codersdk.Response{
+ Message: SignedOutErrorMessage,
+ Detail: "API key secret is invalid.",
+ }, false
+ }
+
+ return &key, codersdk.Response{}, true
+}
+
// ExtractAPIKey requires authentication using a valid API key. It handles
// extending an API key if it comes close to expiry, updating the last used time
// in the database.
@@ -179,49 +229,9 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
return nil, nil, false
}
- tokenFunc := APITokenFromRequest
- if cfg.SessionTokenFunc != nil {
- tokenFunc = cfg.SessionTokenFunc
- }
- token := tokenFunc(r)
- if token == "" {
- return optionalWrite(http.StatusUnauthorized, codersdk.Response{
- Message: SignedOutErrorMessage,
- Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenCookie),
- })
- }
-
- keyID, keySecret, err := SplitAPIToken(token)
- if err != nil {
- return optionalWrite(http.StatusUnauthorized, codersdk.Response{
- Message: SignedOutErrorMessage,
- Detail: "Invalid API key format: " + err.Error(),
- })
- }
-
- //nolint:gocritic // System needs to fetch API key to check if it's valid.
- key, err := cfg.DB.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx), keyID)
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- return optionalWrite(http.StatusUnauthorized, codersdk.Response{
- Message: SignedOutErrorMessage,
- Detail: "API key is invalid.",
- })
- }
-
- return write(http.StatusInternalServerError, codersdk.Response{
- Message: internalErrorMessage,
- Detail: fmt.Sprintf("Internal error fetching API key by id. %s", err.Error()),
- })
- }
-
- // Checking to see if the secret is valid.
- hashedSecret := sha256.Sum256([]byte(keySecret))
- if subtle.ConstantTimeCompare(key.HashedSecret, hashedSecret[:]) != 1 {
- return optionalWrite(http.StatusUnauthorized, codersdk.Response{
- Message: SignedOutErrorMessage,
- Detail: "API key secret is invalid.",
- })
+ key, resp, ok := APIKeyFromRequest(ctx, cfg.DB, cfg.SessionTokenFunc, r)
+ if !ok {
+ return optionalWrite(http.StatusUnauthorized, resp)
}
var (
@@ -231,6 +241,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
changed = false
)
if key.LoginType == database.LoginTypeGithub || key.LoginType == database.LoginTypeOIDC {
+ var err error
//nolint:gocritic // System needs to fetch UserLink to check if it's valid.
link, err = cfg.DB.GetUserLinkByUserIDLoginType(dbauthz.AsSystemRestricted(ctx), database.GetUserLinkByUserIDLoginTypeParams{
UserID: key.UserID,
@@ -298,6 +309,9 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
}
// Checking if the key is expired.
+ // NOTE: The `RequireAuth` React component depends on this `Detail` to detect when
+ // the users token has expired. If you change the text here, make sure to update it
+ // in site/src/components/RequireAuth/RequireAuth.tsx as well.
if key.ExpiresAt.Before(now) {
return optionalWrite(http.StatusUnauthorized, codersdk.Response{
Message: SignedOutErrorMessage,
@@ -427,7 +441,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
}.WithCachedASTValue(),
}
- return &key, &authz, true
+ return key, &authz, true
}
// APITokenFromRequest returns the api token from the request.
diff --git a/coderd/httpmw/apikey_test.go b/coderd/httpmw/apikey_test.go
index b4b7bb01f8eeb..d7426e4087b91 100644
--- a/coderd/httpmw/apikey_test.go
+++ b/coderd/httpmw/apikey_test.go
@@ -3,11 +3,13 @@ package httpmw_test
import (
"context"
"crypto/sha256"
+ "encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
+ "strings"
"sync/atomic"
"testing"
"time"
@@ -16,14 +18,14 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/testutil"
)
func randomAPIKeyParts() (id string, secret string) {
@@ -197,6 +199,11 @@ func TestAPIKey(t *testing.T) {
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
+
+ var apiRes codersdk.Response
+ dec := json.NewDecoder(res.Body)
+ _ = dec.Decode(&apiRes)
+ require.True(t, strings.HasPrefix(apiRes.Detail, "API key expired"))
})
t.Run("Valid", func(t *testing.T) {
diff --git a/coderd/httpmw/authorize_test.go b/coderd/httpmw/authorize_test.go
index b2491d3de4707..6557b307c8a2b 100644
--- a/coderd/httpmw/authorize_test.go
+++ b/coderd/httpmw/authorize_test.go
@@ -15,11 +15,11 @@ import (
"github.com/sqlc-dev/pqtype"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
func TestExtractUserRoles(t *testing.T) {
diff --git a/coderd/httpmw/authz.go b/coderd/httpmw/authz.go
index 6fc5c396a101f..4c94ce362be2a 100644
--- a/coderd/httpmw/authz.go
+++ b/coderd/httpmw/authz.go
@@ -5,7 +5,7 @@ import (
"github.com/go-chi/chi/v5"
- "github.com/coder/coder/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
)
// AsAuthzSystem is a chained handler that temporarily sets the dbauthz context
diff --git a/coderd/httpmw/authz_test.go b/coderd/httpmw/authz_test.go
index 29474aa264bd9..b469a8f23a5ed 100644
--- a/coderd/httpmw/authz_test.go
+++ b/coderd/httpmw/authz_test.go
@@ -8,9 +8,9 @@ import (
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpmw"
)
func TestAsAuthzSystem(t *testing.T) {
diff --git a/coderd/httpmw/clitelemetry.go b/coderd/httpmw/clitelemetry.go
index 2262862beba49..7d6b67bb004b1 100644
--- a/coderd/httpmw/clitelemetry.go
+++ b/coderd/httpmw/clitelemetry.go
@@ -11,8 +11,8 @@ import (
"tailscale.com/tstime/rate"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/codersdk"
)
func ReportCLITelemetry(log slog.Logger, rep telemetry.Reporter) func(http.Handler) http.Handler {
diff --git a/coderd/httpmw/cors.go b/coderd/httpmw/cors.go
index 7206881d24f85..b00810fbf9322 100644
--- a/coderd/httpmw/cors.go
+++ b/coderd/httpmw/cors.go
@@ -7,7 +7,7 @@ import (
"github.com/go-chi/cors"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
const (
diff --git a/coderd/httpmw/cors_test.go b/coderd/httpmw/cors_test.go
index 7668771b1e6db..ae63073b237ed 100644
--- a/coderd/httpmw/cors_test.go
+++ b/coderd/httpmw/cors_test.go
@@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
)
func TestWorkspaceAppCors(t *testing.T) {
diff --git a/coderd/httpmw/csp_test.go b/coderd/httpmw/csp_test.go
index bb352537b10cd..2dca209faa5c3 100644
--- a/coderd/httpmw/csp_test.go
+++ b/coderd/httpmw/csp_test.go
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/httpmw"
)
func TestCSPConnect(t *testing.T) {
diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go
index ce25c600940b5..2a1f383a7490a 100644
--- a/coderd/httpmw/csrf.go
+++ b/coderd/httpmw/csrf.go
@@ -7,7 +7,7 @@ import (
"github.com/justinas/nosurf"
"golang.org/x/xerrors"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
// CSRF is a middleware that verifies that a CSRF token is present in the request
diff --git a/coderd/httpmw/gitauthparam.go b/coderd/httpmw/gitauthparam.go
index 2ce592d54f98a..240732275bd83 100644
--- a/coderd/httpmw/gitauthparam.go
+++ b/coderd/httpmw/gitauthparam.go
@@ -6,8 +6,8 @@ import (
"github.com/go-chi/chi/v5"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
type gitAuthParamContextKey struct{}
diff --git a/coderd/httpmw/gitauthparam_test.go b/coderd/httpmw/gitauthparam_test.go
index 01ea35470f025..665e438a23c0c 100644
--- a/coderd/httpmw/gitauthparam_test.go
+++ b/coderd/httpmw/gitauthparam_test.go
@@ -9,8 +9,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/httpmw"
)
//nolint:bodyclose
diff --git a/coderd/httpmw/groupparam.go b/coderd/httpmw/groupparam.go
index 5b6d3bfe2dd15..9cf2a113020dd 100644
--- a/coderd/httpmw/groupparam.go
+++ b/coderd/httpmw/groupparam.go
@@ -6,9 +6,9 @@ import (
"github.com/go-chi/chi/v5"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
type groupParamContextKey struct{}
diff --git a/coderd/httpmw/groupparam_test.go b/coderd/httpmw/groupparam_test.go
index b7f59528e7b34..a0c50ee0857b5 100644
--- a/coderd/httpmw/groupparam_test.go
+++ b/coderd/httpmw/groupparam_test.go
@@ -10,10 +10,10 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
)
func TestGroupParam(t *testing.T) {
diff --git a/coderd/httpmw/hsts_test.go b/coderd/httpmw/hsts_test.go
index 16dcee78cbf61..3bc3463e69e65 100644
--- a/coderd/httpmw/hsts_test.go
+++ b/coderd/httpmw/hsts_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/httpmw"
)
func TestHSTS(t *testing.T) {
diff --git a/coderd/httpmw/httpmw.go b/coderd/httpmw/httpmw.go
index f6a0dac8b0b65..552469bf044b0 100644
--- a/coderd/httpmw/httpmw.go
+++ b/coderd/httpmw/httpmw.go
@@ -7,8 +7,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
// ParseUUIDParam consumes a url parameter and parses it as a UUID.
diff --git a/coderd/httpmw/httpmw_internal_test.go b/coderd/httpmw/httpmw_internal_test.go
index 87aa3a6960822..5a6578cf3799f 100644
--- a/coderd/httpmw/httpmw_internal_test.go
+++ b/coderd/httpmw/httpmw_internal_test.go
@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
const (
diff --git a/coderd/httpmw/logger.go b/coderd/httpmw/logger.go
index e9ee400c5c581..ef0a7560bf4db 100644
--- a/coderd/httpmw/logger.go
+++ b/coderd/httpmw/logger.go
@@ -7,8 +7,8 @@ import (
"time"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/tracing"
)
func Logger(log slog.Logger) func(next http.Handler) http.Handler {
diff --git a/coderd/httpmw/oauth2.go b/coderd/httpmw/oauth2.go
index ceb5c25250ed6..e51a17a5a8394 100644
--- a/coderd/httpmw/oauth2.go
+++ b/coderd/httpmw/oauth2.go
@@ -8,9 +8,9 @@ import (
"golang.org/x/oauth2"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
)
type oauth2StateKey struct{}
diff --git a/coderd/httpmw/oauth2_test.go b/coderd/httpmw/oauth2_test.go
index 3ed3f7f354113..b0bc3f75e4f27 100644
--- a/coderd/httpmw/oauth2_test.go
+++ b/coderd/httpmw/oauth2_test.go
@@ -12,8 +12,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
type testOAuth2Provider struct {
diff --git a/coderd/httpmw/organizationparam.go b/coderd/httpmw/organizationparam.go
index 55ceec57387ff..85e94ef4a0d96 100644
--- a/coderd/httpmw/organizationparam.go
+++ b/coderd/httpmw/organizationparam.go
@@ -4,9 +4,9 @@ import (
"context"
"net/http"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
type (
diff --git a/coderd/httpmw/organizationparam_test.go b/coderd/httpmw/organizationparam_test.go
index b566b0675822e..176cb44ed3ce8 100644
--- a/coderd/httpmw/organizationparam_test.go
+++ b/coderd/httpmw/organizationparam_test.go
@@ -10,11 +10,11 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func TestOrganizationParam(t *testing.T) {
diff --git a/coderd/httpmw/patternmatcher/routepatterns_test.go b/coderd/httpmw/patternmatcher/routepatterns_test.go
index 972e22727ad32..dc7f779136360 100644
--- a/coderd/httpmw/patternmatcher/routepatterns_test.go
+++ b/coderd/httpmw/patternmatcher/routepatterns_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpmw/patternmatcher"
+ "github.com/coder/coder/v2/coderd/httpmw/patternmatcher"
)
func Test_RoutePatterns(t *testing.T) {
diff --git a/coderd/httpmw/prometheus.go b/coderd/httpmw/prometheus.go
index 5012b0023c8dd..b96be84e879e3 100644
--- a/coderd/httpmw/prometheus.go
+++ b/coderd/httpmw/prometheus.go
@@ -7,8 +7,8 @@ import (
"github.com/go-chi/chi/v5"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/tracing"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
diff --git a/coderd/httpmw/prometheus_test.go b/coderd/httpmw/prometheus_test.go
index dce55c26bc134..a51eea5d00312 100644
--- a/coderd/httpmw/prometheus_test.go
+++ b/coderd/httpmw/prometheus_test.go
@@ -10,8 +10,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/tracing"
)
func TestPrometheus(t *testing.T) {
diff --git a/coderd/httpmw/ratelimit.go b/coderd/httpmw/ratelimit.go
index ff4e888232411..bd1d1d6423fbf 100644
--- a/coderd/httpmw/ratelimit.go
+++ b/coderd/httpmw/ratelimit.go
@@ -9,11 +9,11 @@ import (
"github.com/go-chi/httprate"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
)
// RateLimit returns a handler that limits requests per-minute based
diff --git a/coderd/httpmw/ratelimit_test.go b/coderd/httpmw/ratelimit_test.go
index a201634edbcc5..edb368829cf37 100644
--- a/coderd/httpmw/ratelimit_test.go
+++ b/coderd/httpmw/ratelimit_test.go
@@ -12,12 +12,12 @@ import (
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
func randRemoteAddr() string {
diff --git a/coderd/httpmw/realip.go b/coderd/httpmw/realip.go
index e7cc679ba95c0..6f0f318b83224 100644
--- a/coderd/httpmw/realip.go
+++ b/coderd/httpmw/realip.go
@@ -8,7 +8,7 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
const (
diff --git a/coderd/httpmw/realip_test.go b/coderd/httpmw/realip_test.go
index 85036a3c63197..3070070bd90d8 100644
--- a/coderd/httpmw/realip_test.go
+++ b/coderd/httpmw/realip_test.go
@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/httpmw"
)
// TestExtractAddress checks the ExtractAddress function.
diff --git a/coderd/httpmw/recover.go b/coderd/httpmw/recover.go
index 3d19918f8d505..a8d6020561e09 100644
--- a/coderd/httpmw/recover.go
+++ b/coderd/httpmw/recover.go
@@ -6,8 +6,8 @@ import (
"runtime/debug"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/tracing"
)
func Recover(log slog.Logger) func(h http.Handler) http.Handler {
diff --git a/coderd/httpmw/recover_test.go b/coderd/httpmw/recover_test.go
index 8e2746166903e..35306e0b50f57 100644
--- a/coderd/httpmw/recover_test.go
+++ b/coderd/httpmw/recover_test.go
@@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/tracing"
)
func TestRecover(t *testing.T) {
diff --git a/coderd/httpmw/requestid_test.go b/coderd/httpmw/requestid_test.go
index c632dbbde8c4e..7dc21a8f23a43 100644
--- a/coderd/httpmw/requestid_test.go
+++ b/coderd/httpmw/requestid_test.go
@@ -8,7 +8,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/httpmw"
)
func TestRequestID(t *testing.T) {
diff --git a/coderd/httpmw/templateparam.go b/coderd/httpmw/templateparam.go
index eadb072d50131..8fe6f2a452199 100644
--- a/coderd/httpmw/templateparam.go
+++ b/coderd/httpmw/templateparam.go
@@ -6,9 +6,9 @@ import (
"github.com/go-chi/chi/v5"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
type templateParamContextKey struct{}
diff --git a/coderd/httpmw/templateparam_test.go b/coderd/httpmw/templateparam_test.go
index c630d5570b25d..d8608781905d5 100644
--- a/coderd/httpmw/templateparam_test.go
+++ b/coderd/httpmw/templateparam_test.go
@@ -10,11 +10,11 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func TestTemplateParam(t *testing.T) {
diff --git a/coderd/httpmw/templateversionparam.go b/coderd/httpmw/templateversionparam.go
index 9f8f1c58561c6..702357b3d14fa 100644
--- a/coderd/httpmw/templateversionparam.go
+++ b/coderd/httpmw/templateversionparam.go
@@ -8,9 +8,9 @@ import (
"github.com/go-chi/chi/v5"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
type templateVersionParamContextKey struct{}
diff --git a/coderd/httpmw/templateversionparam_test.go b/coderd/httpmw/templateversionparam_test.go
index febaa8f50bcdc..1cf4da6e832b0 100644
--- a/coderd/httpmw/templateversionparam_test.go
+++ b/coderd/httpmw/templateversionparam_test.go
@@ -10,11 +10,11 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func TestTemplateVersionParam(t *testing.T) {
diff --git a/coderd/httpmw/userparam.go b/coderd/httpmw/userparam.go
index f565687e00bdd..de3446d69cdaf 100644
--- a/coderd/httpmw/userparam.go
+++ b/coderd/httpmw/userparam.go
@@ -2,15 +2,16 @@ package httpmw
import (
"context"
+ "fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
type userParamContextKey struct{}
@@ -85,6 +86,7 @@ func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Han
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: userErrorMessage,
+ Detail: fmt.Sprintf("queried user=%q", userQuery),
})
return
}
@@ -96,6 +98,7 @@ func ExtractUserParam(db database.Store, redirectToLoginOnMe bool) func(http.Han
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: userErrorMessage,
+ Detail: fmt.Sprintf("queried user=%q", userQuery),
})
return
}
diff --git a/coderd/httpmw/userparam_test.go b/coderd/httpmw/userparam_test.go
index 4ce4e317f9f15..bd1b5b2b277c7 100644
--- a/coderd/httpmw/userparam_test.go
+++ b/coderd/httpmw/userparam_test.go
@@ -9,11 +9,11 @@ import (
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func TestUserParam(t *testing.T) {
diff --git a/coderd/httpmw/workspaceagent.go b/coderd/httpmw/workspaceagent.go
index f039c6bbf7afb..883a54e404c4e 100644
--- a/coderd/httpmw/workspaceagent.go
+++ b/coderd/httpmw/workspaceagent.go
@@ -9,11 +9,11 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
type workspaceAgentContextKey struct{}
@@ -74,8 +74,9 @@ func ExtractWorkspaceAgent(opts ExtractWorkspaceAgentConfig) func(http.Handler)
})
return
}
+
//nolint:gocritic // System needs to be able to get workspace agents.
- agent, err := opts.DB.GetWorkspaceAgentByAuthToken(dbauthz.AsSystemRestricted(ctx), token)
+ row, err := opts.DB.GetWorkspaceAgentAndOwnerByAuthToken(dbauthz.AsSystemRestricted(ctx), token)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
optionalWrite(http.StatusUnauthorized, codersdk.Response{
@@ -86,56 +87,23 @@ func ExtractWorkspaceAgent(opts ExtractWorkspaceAgentConfig) func(http.Handler)
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
- Message: "Internal error fetching workspace agent.",
+ Message: "Internal error checking workspace agent authorization.",
Detail: err.Error(),
})
return
}
- //nolint:gocritic // System needs to be able to get workspace agents.
- subject, err := getAgentSubject(dbauthz.AsSystemRestricted(ctx), opts.DB, agent)
- if err != nil {
- httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
- Message: "Internal error fetching workspace agent.",
- Detail: err.Error(),
- })
- return
- }
+ subject := rbac.Subject{
+ ID: row.OwnerID.String(),
+ Roles: rbac.RoleNames(row.OwnerRoles),
+ Groups: row.OwnerGroups,
+ Scope: rbac.WorkspaceAgentScope(row.WorkspaceID, row.OwnerID),
+ }.WithCachedASTValue()
- ctx = context.WithValue(ctx, workspaceAgentContextKey{}, agent)
+ ctx = context.WithValue(ctx, workspaceAgentContextKey{}, row.WorkspaceAgent)
// Also set the dbauthz actor for the request.
ctx = dbauthz.As(ctx, subject)
next.ServeHTTP(rw, r.WithContext(ctx))
})
}
}
-
-func getAgentSubject(ctx context.Context, db database.Store, agent database.WorkspaceAgent) (rbac.Subject, error) {
- // TODO: make a different query that gets the workspace owner and roles along with the agent.
- workspace, err := db.GetWorkspaceByAgentID(ctx, agent.ID)
- if err != nil {
- return rbac.Subject{}, err
- }
-
- user, err := db.GetUserByID(ctx, workspace.OwnerID)
- if err != nil {
- return rbac.Subject{}, err
- }
-
- roles, err := db.GetAuthorizationUserRoles(ctx, user.ID)
- if err != nil {
- return rbac.Subject{}, err
- }
-
- // A user that creates a workspace can use this agent auth token and
- // impersonate the workspace. So to prevent privilege escalation, the
- // subject inherits the roles of the user that owns the workspace.
- // We then add a workspace-agent scope to limit the permissions
- // to only what the workspace agent needs.
- return rbac.Subject{
- ID: user.ID.String(),
- Roles: rbac.RoleNames(roles.Roles),
- Groups: roles.Groups,
- Scope: rbac.WorkspaceAgentScope(workspace.ID, user.ID),
- }.WithCachedASTValue(), nil
-}
diff --git a/coderd/httpmw/workspaceagent_test.go b/coderd/httpmw/workspaceagent_test.go
index 5b50aa14b4802..126526e963199 100644
--- a/coderd/httpmw/workspaceagent_test.go
+++ b/coderd/httpmw/workspaceagent_test.go
@@ -9,36 +9,29 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
- setup := func(db database.Store, token uuid.UUID) *http.Request {
- r := httptest.NewRequest("GET", "/", nil)
- r.Header.Set(codersdk.SessionTokenHeader, token.String())
- return r
- }
-
t.Run("None", func(t *testing.T) {
t.Parallel()
- db := dbfake.New()
- rtr := chi.NewRouter()
- rtr.Use(
- httpmw.ExtractWorkspaceAgent(httpmw.ExtractWorkspaceAgentConfig{
+ db, _ := dbtestutil.NewDB(t)
+
+ req, rtr := setup(t, db, uuid.New(), httpmw.ExtractWorkspaceAgent(
+ httpmw.ExtractWorkspaceAgentConfig{
DB: db,
Optional: false,
- }),
- )
- rtr.Get("/", nil)
- r := setup(db, uuid.New())
+ }))
+
rw := httptest.NewRecorder()
- rtr.ServeHTTP(rw, r)
+ req.Header.Set(codersdk.SessionTokenHeader, uuid.New().String())
+ rtr.ServeHTTP(rw, req)
res := rw.Result()
defer res.Body.Close()
@@ -47,42 +40,72 @@ func TestWorkspaceAgent(t *testing.T) {
t.Run("Found", func(t *testing.T) {
t.Parallel()
- db := dbfake.New()
- var (
- user = dbgen.User(t, db, database.User{})
- workspace = dbgen.Workspace(t, db, database.Workspace{
- OwnerID: user.ID,
- })
- job = dbgen.ProvisionerJob(t, db, database.ProvisionerJob{})
- resource = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
- JobID: job.ID,
- })
- _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
- WorkspaceID: workspace.ID,
- JobID: job.ID,
- })
- agent = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
- ResourceID: resource.ID,
- })
- )
-
- rtr := chi.NewRouter()
- rtr.Use(
- httpmw.ExtractWorkspaceAgent(httpmw.ExtractWorkspaceAgentConfig{
+ db, _ := dbtestutil.NewDB(t)
+ authToken := uuid.New()
+ req, rtr := setup(t, db, authToken, httpmw.ExtractWorkspaceAgent(
+ httpmw.ExtractWorkspaceAgentConfig{
DB: db,
Optional: false,
- }),
- )
- rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
- _ = httpmw.WorkspaceAgent(r)
- rw.WriteHeader(http.StatusOK)
- })
- r := setup(db, agent.AuthToken)
+ }))
+
rw := httptest.NewRecorder()
- rtr.ServeHTTP(rw, r)
+ req.Header.Set(codersdk.SessionTokenHeader, authToken.String())
+ rtr.ServeHTTP(rw, req)
+ //nolint:bodyclose // Closed in `t.Cleanup`
res := rw.Result()
- defer res.Body.Close()
+ t.Cleanup(func() { _ = res.Body.Close() })
require.Equal(t, http.StatusOK, res.StatusCode)
})
}
+
+func setup(t testing.TB, db database.Store, authToken uuid.UUID, mw func(http.Handler) http.Handler) (*http.Request, http.Handler) {
+ t.Helper()
+ org := dbgen.Organization(t, db, database.Organization{})
+ user := dbgen.User(t, db, database.User{
+ Status: database.UserStatusActive,
+ })
+ _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
+ UserID: user.ID,
+ OrganizationID: org.ID,
+ })
+ templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
+ OrganizationID: org.ID,
+ CreatedBy: user.ID,
+ })
+ template := dbgen.Template(t, db, database.Template{
+ OrganizationID: org.ID,
+ ActiveVersionID: templateVersion.ID,
+ CreatedBy: user.ID,
+ })
+ workspace := dbgen.Workspace(t, db, database.Workspace{
+ OwnerID: user.ID,
+ OrganizationID: org.ID,
+ TemplateID: template.ID,
+ })
+ job := dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
+ OrganizationID: org.ID,
+ })
+ resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
+ JobID: job.ID,
+ })
+ _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
+ WorkspaceID: workspace.ID,
+ JobID: job.ID,
+ TemplateVersionID: templateVersion.ID,
+ })
+ _ = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
+ ResourceID: resource.ID,
+ AuthToken: authToken,
+ })
+
+ req := httptest.NewRequest("GET", "/", nil)
+ rtr := chi.NewRouter()
+ rtr.Use(mw)
+ rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) {
+ _ = httpmw.WorkspaceAgent(r)
+ rw.WriteHeader(http.StatusOK)
+ })
+
+ return req, rtr
+}
diff --git a/coderd/httpmw/workspaceagentparam.go b/coderd/httpmw/workspaceagentparam.go
index 7e31c9e15be31..67f6db0a5de4d 100644
--- a/coderd/httpmw/workspaceagentparam.go
+++ b/coderd/httpmw/workspaceagentparam.go
@@ -8,9 +8,9 @@ import (
"github.com/go-chi/chi/v5"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
type workspaceAgentParamContextKey struct{}
diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go
index f6a4c3fe1eaa7..233b5d0d8b570 100644
--- a/coderd/httpmw/workspaceagentparam_test.go
+++ b/coderd/httpmw/workspaceagentparam_test.go
@@ -10,11 +10,11 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func TestWorkspaceAgentParam(t *testing.T) {
diff --git a/coderd/httpmw/workspacebuildparam.go b/coderd/httpmw/workspacebuildparam.go
index 518029465eb12..895f73ac0a9ff 100644
--- a/coderd/httpmw/workspacebuildparam.go
+++ b/coderd/httpmw/workspacebuildparam.go
@@ -6,9 +6,9 @@ import (
"github.com/go-chi/chi/v5"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
type workspaceBuildParamContextKey struct{}
diff --git a/coderd/httpmw/workspacebuildparam_test.go b/coderd/httpmw/workspacebuildparam_test.go
index ac495537a1de3..bade2b19d8dfc 100644
--- a/coderd/httpmw/workspacebuildparam_test.go
+++ b/coderd/httpmw/workspacebuildparam_test.go
@@ -10,11 +10,11 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func TestWorkspaceBuildParam(t *testing.T) {
diff --git a/coderd/httpmw/workspaceparam.go b/coderd/httpmw/workspaceparam.go
index b0f264abe3619..21e8dcfd62863 100644
--- a/coderd/httpmw/workspaceparam.go
+++ b/coderd/httpmw/workspaceparam.go
@@ -9,9 +9,9 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
type workspaceParamContextKey struct{}
diff --git a/coderd/httpmw/workspaceparam_test.go b/coderd/httpmw/workspaceparam_test.go
index 1c7504b793f64..36a2d0e600714 100644
--- a/coderd/httpmw/workspaceparam_test.go
+++ b/coderd/httpmw/workspaceparam_test.go
@@ -15,12 +15,12 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
)
func TestWorkspaceParam(t *testing.T) {
diff --git a/coderd/httpmw/workspaceproxy.go b/coderd/httpmw/workspaceproxy.go
index 5df5e64a0cc07..d3a93962aaf6e 100644
--- a/coderd/httpmw/workspaceproxy.go
+++ b/coderd/httpmw/workspaceproxy.go
@@ -12,10 +12,10 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
const (
diff --git a/coderd/httpmw/workspaceproxy_test.go b/coderd/httpmw/workspaceproxy_test.go
index 818acf85c4442..27b85643ce43d 100644
--- a/coderd/httpmw/workspaceproxy_test.go
+++ b/coderd/httpmw/workspaceproxy_test.go
@@ -11,13 +11,13 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
)
func TestExtractWorkspaceProxy(t *testing.T) {
diff --git a/coderd/httpmw/workspaceresourceparam.go b/coderd/httpmw/workspaceresourceparam.go
index 41d19a4ea0519..f2a9661b5c6c7 100644
--- a/coderd/httpmw/workspaceresourceparam.go
+++ b/coderd/httpmw/workspaceresourceparam.go
@@ -8,9 +8,9 @@ import (
"github.com/go-chi/chi/v5"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
type workspaceResourceParamContextKey struct{}
diff --git a/coderd/httpmw/workspaceresourceparam_test.go b/coderd/httpmw/workspaceresourceparam_test.go
index bee32b0d304c3..61c4d77fbf3da 100644
--- a/coderd/httpmw/workspaceresourceparam_test.go
+++ b/coderd/httpmw/workspaceresourceparam_test.go
@@ -10,10 +10,10 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
)
func TestWorkspaceResourceParam(t *testing.T) {
diff --git a/coderd/insights.go b/coderd/insights.go
index b643303dd0df2..e19f95d40dc0c 100644
--- a/coderd/insights.go
+++ b/coderd/insights.go
@@ -2,20 +2,22 @@ package coderd
import (
"context"
- "encoding/json"
"fmt"
"net/http"
- "sort"
+ "strings"
"time"
"github.com/google/uuid"
"golang.org/x/exp/slices"
+ "golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/codersdk"
)
// Duplicated in codersdk.
@@ -132,8 +134,8 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) {
for templateID := range templateIDSet {
seenTemplateIDs = append(seenTemplateIDs, templateID)
}
- slices.SortFunc(seenTemplateIDs, func(a, b uuid.UUID) bool {
- return a.String() < b.String()
+ slices.SortFunc(seenTemplateIDs, func(a, b uuid.UUID) int {
+ return slice.Ascending(a.String(), b.String())
})
resp := codersdk.UserLatencyInsightsResponse{
@@ -188,15 +190,20 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
}
var usage database.GetTemplateInsightsRow
+ var appUsage []database.GetTemplateAppInsightsRow
var dailyUsage []database.GetTemplateDailyInsightsRow
+ var parameterRows []database.GetTemplateParameterInsightsRow
- // Use a transaction to ensure that we get consistent data between
- // the full and interval report.
- err := api.Database.InTx(func(tx database.Store) error {
- var err error
+ eg, egCtx := errgroup.WithContext(ctx)
+ eg.SetLimit(4)
+ // The following insights data queries have a theoretical chance to be
+ // inconsistent between eachother when looking at "today", however, the
+ // overhead from a transaction is not worth it.
+ eg.Go(func() error {
+ var err error
if interval != "" {
- dailyUsage, err = tx.GetTemplateDailyInsights(ctx, database.GetTemplateDailyInsightsParams{
+ dailyUsage, err = api.Database.GetTemplateDailyInsights(egCtx, database.GetTemplateDailyInsightsParams{
StartTime: startTime,
EndTime: endTime,
TemplateIDs: templateIDs,
@@ -205,8 +212,11 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
return xerrors.Errorf("get template daily insights: %w", err)
}
}
-
- usage, err = tx.GetTemplateInsights(ctx, database.GetTemplateInsightsParams{
+ return nil
+ })
+ eg.Go(func() error {
+ var err error
+ usage, err = api.Database.GetTemplateInsights(egCtx, database.GetTemplateInsightsParams{
StartTime: startTime,
EndTime: endTime,
TemplateIDs: templateIDs,
@@ -214,9 +224,37 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
if err != nil {
return xerrors.Errorf("get template insights: %w", err)
}
+ return nil
+ })
+ eg.Go(func() error {
+ var err error
+ appUsage, err = api.Database.GetTemplateAppInsights(egCtx, database.GetTemplateAppInsightsParams{
+ StartTime: startTime,
+ EndTime: endTime,
+ TemplateIDs: templateIDs,
+ })
+ if err != nil {
+ return xerrors.Errorf("get template app insights: %w", err)
+ }
+ return nil
+ })
+ // Template parameter insights have no risk of inconsistency with the other
+ // insights.
+ eg.Go(func() error {
+ var err error
+ parameterRows, err = api.Database.GetTemplateParameterInsights(ctx, database.GetTemplateParameterInsightsParams{
+ StartTime: startTime,
+ EndTime: endTime,
+ TemplateIDs: templateIDs,
+ })
+ if err != nil {
+ return xerrors.Errorf("get template parameter insights: %w", err)
+ }
return nil
- }, nil)
+ })
+
+ err := eg.Wait()
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
@@ -229,22 +267,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
return
}
- // Template parameter insights have no risk of inconsistency with the other
- // insights, so we don't need to perform this in a transaction.
- parameterRows, err := api.Database.GetTemplateParameterInsights(ctx, database.GetTemplateParameterInsightsParams{
- StartTime: startTime,
- EndTime: endTime,
- TemplateIDs: templateIDs,
- })
- if err != nil {
- httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
- Message: "Internal error fetching template parameter insights.",
- Detail: err.Error(),
- })
- return
- }
-
- parametersUsage, err := convertTemplateInsightsParameters(parameterRows)
+ parametersUsage, err := db2sdk.TemplateInsightsParameters(parameterRows)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting template parameter insights.",
@@ -257,17 +280,19 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
Report: codersdk.TemplateInsightsReport{
StartTime: startTime,
EndTime: endTime,
- TemplateIDs: usage.TemplateIDs,
- ActiveUsers: usage.ActiveUsers,
- AppsUsage: convertTemplateInsightsBuiltinApps(usage),
+ TemplateIDs: convertTemplateInsightsTemplateIDs(usage, appUsage),
+ ActiveUsers: convertTemplateInsightsActiveUsers(usage, appUsage),
+ AppsUsage: convertTemplateInsightsApps(usage, appUsage),
ParametersUsage: parametersUsage,
},
IntervalReports: []codersdk.TemplateInsightsIntervalReport{},
}
for _, row := range dailyUsage {
resp.IntervalReports = append(resp.IntervalReports, codersdk.TemplateInsightsIntervalReport{
- StartTime: row.StartTime,
- EndTime: row.EndTime,
+ // NOTE(mafredri): This might not be accurate over DST since the
+ // parsed location only contains the offset.
+ StartTime: row.StartTime.In(startTime.Location()),
+ EndTime: row.EndTime.In(startTime.Location()),
Interval: interval,
TemplateIDs: row.TemplateIDs,
ActiveUsers: row.ActiveUsers,
@@ -276,10 +301,45 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, resp)
}
-// convertTemplateInsightsBuiltinApps builds the list of builtin apps from the
-// database row, these are apps that are implicitly a part of all templates.
-func convertTemplateInsightsBuiltinApps(usage database.GetTemplateInsightsRow) []codersdk.TemplateAppUsage {
- return []codersdk.TemplateAppUsage{
+func convertTemplateInsightsTemplateIDs(usage database.GetTemplateInsightsRow, appUsage []database.GetTemplateAppInsightsRow) []uuid.UUID {
+ templateIDSet := make(map[uuid.UUID]struct{})
+ for _, id := range usage.TemplateIDs {
+ templateIDSet[id] = struct{}{}
+ }
+ for _, app := range appUsage {
+ for _, id := range app.TemplateIDs {
+ templateIDSet[id] = struct{}{}
+ }
+ }
+ templateIDs := make([]uuid.UUID, 0, len(templateIDSet))
+ for id := range templateIDSet {
+ templateIDs = append(templateIDs, id)
+ }
+ slices.SortFunc(templateIDs, func(a, b uuid.UUID) int {
+ return slice.Ascending(a.String(), b.String())
+ })
+ return templateIDs
+}
+
+func convertTemplateInsightsActiveUsers(usage database.GetTemplateInsightsRow, appUsage []database.GetTemplateAppInsightsRow) int64 {
+ activeUserIDSet := make(map[uuid.UUID]struct{})
+ for _, id := range usage.ActiveUserIDs {
+ activeUserIDSet[id] = struct{}{}
+ }
+ for _, app := range appUsage {
+ for _, id := range app.ActiveUserIDs {
+ activeUserIDSet[id] = struct{}{}
+ }
+ }
+ return int64(len(activeUserIDSet))
+}
+
+// convertTemplateInsightsApps builds the list of builtin apps and template apps
+// from the provided database rows, builtin apps are implicitly a part of all
+// templates.
+func convertTemplateInsightsApps(usage database.GetTemplateInsightsRow, appUsage []database.GetTemplateAppInsightsRow) []codersdk.TemplateAppUsage {
+ // Builtin apps.
+ apps := []codersdk.TemplateAppUsage{
{
TemplateIDs: usage.TemplateIDs,
Type: codersdk.TemplateAppsTypeBuiltin,
@@ -296,6 +356,12 @@ func convertTemplateInsightsBuiltinApps(usage database.GetTemplateInsightsRow) [
Icon: "/icon/intellij.svg",
Seconds: usage.UsageJetbrainsSeconds,
},
+ // TODO(mafredri): We could take Web Terminal usage from appUsage since
+ // that should be more accurate. The difference is that this reflects
+ // the rpty session as seen by the agent (can live past the connection),
+ // whereas appUsage reflects the lifetime of the client connection. The
+ // condition finding the corresponding app entry in appUsage is:
+ // !app.IsApp && app.AccessMethod == "terminal" && app.SlugOrPort == ""
{
TemplateIDs: usage.TemplateIDs,
Type: codersdk.TemplateAppsTypeBuiltin,
@@ -313,39 +379,49 @@ func convertTemplateInsightsBuiltinApps(usage database.GetTemplateInsightsRow) [
Seconds: usage.UsageSshSeconds,
},
}
-}
-func convertTemplateInsightsParameters(parameterRows []database.GetTemplateParameterInsightsRow) ([]codersdk.TemplateParameterUsage, error) {
- parametersByNum := make(map[int64]*codersdk.TemplateParameterUsage)
- for _, param := range parameterRows {
- if _, ok := parametersByNum[param.Num]; !ok {
- var opts []codersdk.TemplateVersionParameterOption
- err := json.Unmarshal(param.Options, &opts)
- if err != nil {
- return nil, xerrors.Errorf("unmarshal template parameter options: %w", err)
- }
- parametersByNum[param.Num] = &codersdk.TemplateParameterUsage{
- TemplateIDs: param.TemplateIDs,
- Name: param.Name,
- DisplayName: param.DisplayName,
- Options: opts,
- }
+ // Use a stable sort, similarly to how we would sort in the query, note that
+ // we don't sort in the query because order varies depending on the table
+ // collation.
+ //
+ // ORDER BY access_method, slug_or_port, display_name, icon, is_app
+ slices.SortFunc(appUsage, func(a, b database.GetTemplateAppInsightsRow) int {
+ if a.AccessMethod != b.AccessMethod {
+ return strings.Compare(a.AccessMethod, b.AccessMethod)
+ }
+ if a.SlugOrPort != b.SlugOrPort {
+ return strings.Compare(a.SlugOrPort, b.SlugOrPort)
+ }
+ if a.DisplayName.String != b.DisplayName.String {
+ return strings.Compare(a.DisplayName.String, b.DisplayName.String)
}
- parametersByNum[param.Num].Values = append(parametersByNum[param.Num].Values, codersdk.TemplateParameterValue{
- Value: param.Value,
- Count: param.Count,
+ if a.Icon.String != b.Icon.String {
+ return strings.Compare(a.Icon.String, b.Icon.String)
+ }
+ if !a.IsApp && b.IsApp {
+ return -1
+ } else if a.IsApp && !b.IsApp {
+ return 1
+ }
+ return 0
+ })
+
+ // Template apps.
+ for _, app := range appUsage {
+ if !app.IsApp {
+ continue
+ }
+ apps = append(apps, codersdk.TemplateAppUsage{
+ TemplateIDs: app.TemplateIDs,
+ Type: codersdk.TemplateAppsTypeApp,
+ DisplayName: app.DisplayName.String,
+ Slug: app.SlugOrPort,
+ Icon: app.Icon.String,
+ Seconds: app.UsageSeconds,
})
}
- parametersUsage := []codersdk.TemplateParameterUsage{}
- for _, param := range parametersByNum {
- parametersUsage = append(parametersUsage, *param)
- }
-
- sort.Slice(parametersUsage, func(i, j int) bool {
- return parametersUsage[i].Name < parametersUsage[j].Name
- })
- return parametersUsage, nil
+ return apps
}
// parseInsightsStartAndEndTime parses the start and end time query parameters
diff --git a/coderd/insights_test.go b/coderd/insights_test.go
index 2469cc0f4b362..83498bbb365fd 100644
--- a/coderd/insights_test.go
+++ b/coderd/insights_test.go
@@ -2,26 +2,37 @@ package coderd_test
import (
"context"
+ "encoding/json"
"fmt"
"io"
"net/http"
+ "os"
+ "path/filepath"
+ "strings"
"testing"
"time"
+ "github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "golang.org/x/exp/slices"
+ "cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/batchstats"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func TestDeploymentInsights(t *testing.T) {
@@ -37,7 +48,7 @@ func TestDeploymentInsights(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -123,7 +134,7 @@ func TestUserLatencyInsights(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -229,211 +240,849 @@ func TestUserLatencyInsights_BadRequest(t *testing.T) {
assert.Error(t, err, "want error for end time partial day when not today")
}
-func TestTemplateInsights(t *testing.T) {
+func TestTemplateInsights_Golden(t *testing.T) {
t.Parallel()
- const (
- firstParameterName = "first_parameter"
- firstParameterDisplayName = "First PARAMETER"
- firstParameterType = "string"
- firstParameterDescription = "This is first parameter"
- firstParameterValue = "abc"
-
- secondParameterName = "second_parameter"
- secondParameterDisplayName = "Second PARAMETER"
- secondParameterType = "number"
- secondParameterDescription = "This is second parameter"
- secondParameterValue = "123"
-
- thirdParameterName = "third_parameter"
- thirdParameterDisplayName = "Third PARAMETER"
- thirdParameterType = "string"
- thirdParameterDescription = "This is third parameter"
- thirdParameterValue = "bbb"
- thirdParameterOptionName1 = "This is AAA"
- thirdParameterOptionValue1 = "aaa"
- thirdParameterOptionName2 = "This is BBB"
- thirdParameterOptionValue2 = "bbb"
- thirdParameterOptionName3 = "This is CCC"
- thirdParameterOptionValue3 = "ccc"
- )
+ // Prepare test data types.
+ type templateParameterOption struct {
+ name string
+ value string
+ }
+ type templateParameter struct {
+ name string
+ description string
+ options []templateParameterOption
+ }
+ type templateApp struct {
+ name string
+ icon string
+ }
+ type testTemplate struct {
+ name string
+ parameters []*templateParameter
+ apps []templateApp
- logger := slogtest.Make(t, nil)
- opts := &coderdtest.Options{
- IncludeProvisionerDaemon: true,
- AgentStatsRefreshInterval: time.Millisecond * 100,
+ // Filled later.
+ id uuid.UUID
}
- client := coderdtest.New(t, opts)
+ type buildParameter struct {
+ templateParameter *templateParameter
+ value string
+ }
+ type workspaceApp templateApp
+ type testWorkspace struct {
+ name string
+ template *testTemplate
+ buildParameters []buildParameter
+
+ // Filled later.
+ id uuid.UUID
+ user any // *testUser, but it's not available yet, defined below.
+ agentID uuid.UUID
+ apps []*workspaceApp
+ agentClient *agentsdk.Client
+ }
+ type testUser struct {
+ name string
+ workspaces []*testWorkspace
- user := coderdtest.CreateFirstUser(t, client)
- authToken := uuid.NewString()
- version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
- Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
- {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: []*proto.RichParameter{
- {Name: firstParameterName, DisplayName: firstParameterDisplayName, Type: firstParameterType, Description: firstParameterDescription, Required: true},
- {Name: secondParameterName, DisplayName: secondParameterDisplayName, Type: secondParameterType, Description: secondParameterDescription, Required: true},
- {Name: thirdParameterName, DisplayName: thirdParameterDisplayName, Type: thirdParameterType, Description: thirdParameterDescription, Required: true, Options: []*proto.RichParameterOption{
- {Name: thirdParameterOptionName1, Value: thirdParameterOptionValue1},
- {Name: thirdParameterOptionName2, Value: thirdParameterOptionValue2},
- {Name: thirdParameterOptionName3, Value: thirdParameterOptionValue3},
- }},
- },
- },
- },
- },
- },
- ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
- })
- template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
- require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
- coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ client *codersdk.Client
+ sdk codersdk.User
+ }
- buildParameters := []codersdk.WorkspaceBuildParameter{
- {Name: firstParameterName, Value: firstParameterValue},
- {Name: secondParameterName, Value: secondParameterValue},
- {Name: thirdParameterName, Value: thirdParameterValue},
+ // Represent agent stats, to be inserted via stats batcher.
+ type agentStat struct {
+ // Set a range via start/end, multiple stats will be generated
+ // within the range.
+ startedAt time.Time
+ endedAt time.Time
+
+ sessionCountVSCode int64
+ sessionCountJetBrains int64
+ sessionCountReconnectingPTY int64
+ sessionCountSSH int64
+ noConnections bool
+ }
+ // Represent app usage stats, to be inserted via stats reporter.
+ type appUsage struct {
+ app *workspaceApp
+ startedAt time.Time
+ endedAt time.Time
+ requests int
}
- workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
- cwr.RichParameterValues = buildParameters
- })
- coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
+ // Represent actual data being generated on a per-workspace basis.
+ type testDataGen struct {
+ agentStats []agentStat
+ appUsage []appUsage
+ }
- // Start an agent so that we can generate stats.
- agentClient := agentsdk.New(client.URL)
- agentClient.SetSessionToken(authToken)
- agentCloser := agent.New(agent.Options{
- Logger: logger.Named("agent"),
- Client: agentClient,
- })
- defer func() {
- _ = agentCloser.Close()
- }()
- resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
+ prepareFixtureAndTestData := func(t *testing.T, makeFixture func() ([]*testTemplate, []*testUser), makeData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen) ([]*testTemplate, []*testUser, map[*testWorkspace]testDataGen) {
+ var stableIDs []uuid.UUID
+ newStableUUID := func() uuid.UUID {
+ stableIDs = append(stableIDs, uuid.MustParse(fmt.Sprintf("00000000-0000-0000-0000-%012d", len(stableIDs)+1)))
+ stableID := stableIDs[len(stableIDs)-1]
+ return stableID
+ }
- // Start must be at the beginning of the day, initialize it early in case
- // the day changes so that we get the relevant stats faster.
- y, m, d := time.Now().UTC().Date()
- today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
+ templates, users := makeFixture()
+ for _, template := range templates {
+ template.id = newStableUUID()
+ }
+ for _, user := range users {
+ for _, workspace := range user.workspaces {
+ workspace.user = user
+ for _, app := range workspace.template.apps {
+ app := workspaceApp(app)
+ workspace.apps = append(workspace.apps, &app)
+ }
+ for _, bp := range workspace.buildParameters {
+ foundBuildParam := false
+ for _, param := range workspace.template.parameters {
+ if bp.templateParameter == param {
+ foundBuildParam = true
+ break
+ }
+ }
+ require.True(t, foundBuildParam, "test bug: parameter not in workspace %s template %q", workspace.name, workspace.template.name)
+ }
+ }
+ }
- ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
- defer cancel()
+ testData := makeData(templates, users)
+ // Sanity check.
+ for ws, data := range testData {
+ for _, usage := range data.appUsage {
+ found := false
+ for _, app := range ws.apps {
+ if usage.app == app { // Pointer equality
+ found = true
+ break
+ }
+ }
+ if !found {
+ for _, user := range users {
+ for _, workspace := range user.workspaces {
+ for _, app := range workspace.apps {
+ if usage.app == app { // Pointer equality
+ require.True(t, found, "test bug: app %q not in workspace %q: want user=%s workspace=%s; got user=%s workspace=%s ", usage.app.name, ws.name, ws.user.(*testUser).name, ws.name, user.name, workspace.name)
+ break
+ }
+ }
+ }
+ }
+ require.True(t, found, "test bug: app %q not in workspace %q", usage.app.name, ws.name)
+ }
+ }
+ }
- // Connect to the agent to generate usage/latency stats.
- conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{
- Logger: logger.Named("client"),
- })
- require.NoError(t, err)
- defer conn.Close()
+ return templates, users, testData
+ }
- sshConn, err := conn.SSHClient(ctx)
- require.NoError(t, err)
- defer sshConn.Close()
+ prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) *codersdk.Client {
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug)
+ db, pubsub := dbtestutil.NewDB(t)
+ client := coderdtest.New(t, &coderdtest.Options{
+ Database: db,
+ Pubsub: pubsub,
+ Logger: &logger,
+ IncludeProvisionerDaemon: true,
+ AgentStatsRefreshInterval: time.Hour, // Not relevant for this test.
+ })
+ firstUser := coderdtest.CreateFirstUser(t, client)
- // Start an SSH session to generate SSH usage stats.
- sess, err := sshConn.NewSession()
- require.NoError(t, err)
- defer sess.Close()
+ // Prepare all test users.
+ for _, user := range users {
+ user.client, user.sdk = coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
+ r.Username = user.name
+ })
+ user.client.SetLogger(logger.Named("user").With(slog.Field{Name: "name", Value: user.name}))
+ }
- r, w := io.Pipe()
- defer r.Close()
- defer w.Close()
- sess.Stdin = r
- err = sess.Start("cat")
- require.NoError(t, err)
+ // Prepare all the templates.
+ for _, template := range templates {
+ template := template
+
+ var parameters []*proto.RichParameter
+ for _, parameter := range template.parameters {
+ var options []*proto.RichParameterOption
+ var defaultValue string
+ for _, option := range parameter.options {
+ if defaultValue == "" {
+ defaultValue = option.value
+ }
+ options = append(options, &proto.RichParameterOption{
+ Name: option.name,
+ Value: option.value,
+ })
+ }
+ parameters = append(parameters, &proto.RichParameter{
+ Name: parameter.name,
+ DisplayName: parameter.name,
+ Type: "string",
+ Description: parameter.description,
+ Options: options,
+ DefaultValue: defaultValue,
+ })
+ }
- // Start an rpty session to generate rpty usage stats.
- rpty, err := client.WorkspaceAgentReconnectingPTY(ctx, codersdk.WorkspaceAgentReconnectingPTYOpts{
- AgentID: resources[0].Agents[0].ID,
- Reconnect: uuid.New(),
- Width: 80,
- Height: 24,
- })
- require.NoError(t, err)
- defer rpty.Close()
-
- var resp codersdk.TemplateInsightsResponse
- var req codersdk.TemplateInsightsRequest
- waitForAppSeconds := func(slug string) func() bool {
- return func() bool {
- req = codersdk.TemplateInsightsRequest{
- StartTime: today,
- EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour),
- Interval: codersdk.InsightsReportIntervalDay,
+ // Prepare all workspace resources (agents and apps).
+ var (
+ createWorkspaces []func(uuid.UUID)
+ waitWorkspaces []func()
+ )
+ var resources []*proto.Resource
+ for _, user := range users {
+ user := user
+ for _, workspace := range user.workspaces {
+ workspace := workspace
+
+ if workspace.template != template {
+ continue
+ }
+ authToken := uuid.New()
+ agentClient := agentsdk.New(client.URL)
+ agentClient.SetSessionToken(authToken.String())
+ workspace.agentClient = agentClient
+
+ var apps []*proto.App
+ for _, app := range workspace.apps {
+ apps = append(apps, &proto.App{
+ Slug: app.name,
+ DisplayName: app.name,
+ Icon: app.icon,
+ SharingLevel: proto.AppSharingLevel_OWNER,
+ Url: "http://",
+ })
+ }
+
+ resources = append(resources, &proto.Resource{
+ Name: "example",
+ Type: "aws_instance",
+ Agents: []*proto.Agent{{
+ Id: uuid.NewString(), // Doesn't matter, not used in DB.
+ Name: "dev",
+ Auth: &proto.Agent_Token{
+ Token: authToken.String(),
+ },
+ Apps: apps,
+ }},
+ })
+
+ var buildParameters []codersdk.WorkspaceBuildParameter
+ for _, buildParameter := range workspace.buildParameters {
+ buildParameters = append(buildParameters, codersdk.WorkspaceBuildParameter{
+ Name: buildParameter.templateParameter.name,
+ Value: buildParameter.value,
+ })
+ }
+
+ createWorkspaces = append(createWorkspaces, func(templateID uuid.UUID) {
+ // Create workspace using the users client.
+ createdWorkspace := coderdtest.CreateWorkspace(t, user.client, firstUser.OrganizationID, templateID, func(cwr *codersdk.CreateWorkspaceRequest) {
+ cwr.RichParameterValues = buildParameters
+ })
+ workspace.id = createdWorkspace.ID
+ waitWorkspaces = append(waitWorkspaces, func() {
+ coderdtest.AwaitWorkspaceBuildJob(t, user.client, createdWorkspace.LatestBuild.ID)
+ ctx := testutil.Context(t, testutil.WaitShort)
+ ws, err := user.client.Workspace(ctx, workspace.id)
+ require.NoError(t, err, "want no error getting workspace")
+
+ workspace.agentID = ws.LatestBuild.Resources[0].Agents[0].ID
+ })
+ })
+ }
+ }
+
+ // Create the template version and template.
+ version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: []*proto.Response{
+ {
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Parameters: parameters,
+ },
+ },
+ },
+ },
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
+ Resources: resources,
+ },
+ },
+ }},
+ })
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+
+ // Create template, essentially a modified version of CreateTemplate
+ // where we can control the template ID.
+ // createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
+ createdTemplate := dbgen.Template(t, db, database.Template{
+ ID: template.id,
+ ActiveVersionID: version.ID,
+ OrganizationID: firstUser.OrganizationID,
+ CreatedBy: firstUser.UserID,
+ GroupACL: database.TemplateACL{
+ firstUser.OrganizationID.String(): []rbac.Action{rbac.ActionRead},
+ },
+ })
+ err := db.UpdateTemplateVersionByID(context.Background(), database.UpdateTemplateVersionByIDParams{
+ ID: version.ID,
+ TemplateID: uuid.NullUUID{
+ UUID: createdTemplate.ID,
+ Valid: true,
+ },
+ })
+ require.NoError(t, err, "want no error updating template version")
+
+ // Create all workspaces and wait for them.
+ for _, createWorkspace := range createWorkspaces {
+ createWorkspace(template.id)
}
- resp, err = client.TemplateInsights(ctx, req)
- if !assert.NoError(t, err) {
- return false
+ for _, waitWorkspace := range waitWorkspaces {
+ waitWorkspace()
}
+ }
- if slices.IndexFunc(resp.Report.AppsUsage, func(au codersdk.TemplateAppUsage) bool {
- return au.Slug == slug && au.Seconds > 0
- }) != -1 {
- return true
+ ctx := testutil.Context(t, testutil.WaitSuperLong)
+
+ // Use agent stats batcher to insert agent stats, similar to live system.
+ // NOTE(mafredri): Ideally we would pass batcher as a coderd option and
+ // insert using the agentClient, but we have a circular dependency on
+ // the database.
+ batcher, batcherCloser, err := batchstats.New(
+ ctx,
+ batchstats.WithStore(db),
+ batchstats.WithLogger(logger.Named("batchstats")),
+ batchstats.WithInterval(time.Hour),
+ )
+ require.NoError(t, err)
+ defer batcherCloser() // Flushes the stats, this is to ensure they're written.
+
+ for workspace, data := range testData {
+ for _, stat := range data.agentStats {
+ createdAt := stat.startedAt
+ connectionCount := int64(1)
+ if stat.noConnections {
+ connectionCount = 0
+ }
+ for createdAt.Before(stat.endedAt) {
+ err = batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, agentsdk.Stats{
+ ConnectionCount: connectionCount,
+ SessionCountVSCode: stat.sessionCountVSCode,
+ SessionCountJetBrains: stat.sessionCountJetBrains,
+ SessionCountReconnectingPTY: stat.sessionCountReconnectingPTY,
+ SessionCountSSH: stat.sessionCountSSH,
+ })
+ require.NoError(t, err, "want no error inserting agent stats")
+ createdAt = createdAt.Add(30 * time.Second)
+ }
}
- return false
}
+
+ // Insert app usage.
+ var stats []workspaceapps.StatsReport
+ for workspace, data := range testData {
+ for _, usage := range data.appUsage {
+ appName := usage.app.name
+ accessMethod := workspaceapps.AccessMethodPath
+ if usage.app.name == "terminal" {
+ appName = ""
+ accessMethod = workspaceapps.AccessMethodTerminal
+ }
+ stats = append(stats, workspaceapps.StatsReport{
+ UserID: workspace.user.(*testUser).sdk.ID,
+ WorkspaceID: workspace.id,
+ AgentID: workspace.agentID,
+ AccessMethod: accessMethod,
+ SlugOrPort: appName,
+ SessionID: uuid.New(),
+ SessionStartedAt: usage.startedAt,
+ SessionEndedAt: usage.endedAt,
+ Requests: usage.requests,
+ })
+ }
+ }
+ reporter := workspaceapps.NewStatsDBReporter(db, workspaceapps.DefaultStatsDBReporterBatchSize)
+ //nolint:gocritic // This is a test.
+ err = reporter.Report(dbauthz.AsSystemRestricted(ctx), stats)
+ require.NoError(t, err, "want no error inserting app stats")
+
+ return client
}
- require.Eventually(t, waitForAppSeconds("reconnecting-pty"), testutil.WaitMedium, testutil.IntervalFast, "reconnecting-pty seconds missing")
- require.Eventually(t, waitForAppSeconds("ssh"), testutil.WaitMedium, testutil.IntervalFast, "ssh seconds missing")
- // We got our data, close down sessions and connections.
- _ = rpty.Close()
- _ = sess.Close()
- _ = sshConn.Close()
+ baseTemplateAndUserFixture := func() ([]*testTemplate, []*testUser) {
+ // Test templates and configuration to generate.
+ templates := []*testTemplate{
+ // Create two templates with near-identical apps and parameters
+ // to allow testing for grouping similar data.
+ {
+ name: "template1",
+ parameters: []*templateParameter{
+ {name: "param1", description: "This is first parameter"},
+ {name: "param2", description: "This is second parameter"},
+ {name: "param3", description: "This is third parameter"},
+ {
+ name: "param4",
+ description: "This is fourth parameter",
+ options: []templateParameterOption{
+ {name: "option1", value: "option1"},
+ {name: "option2", value: "option2"},
+ },
+ },
+ },
+ apps: []templateApp{
+ {name: "app1", icon: "/icon1.png"},
+ {name: "app2", icon: "/icon2.png"},
+ {name: "app3", icon: "/icon2.png"},
+ },
+ },
+ {
+ name: "template2",
+ parameters: []*templateParameter{
+ {name: "param1", description: "This is first parameter"},
+ {name: "param2", description: "This is second parameter"},
+ {name: "param3", description: "This is third parameter"},
+ },
+ apps: []templateApp{
+ {name: "app1", icon: "/icon1.png"},
+ {name: "app2", icon: "/icon2.png"},
+ {name: "app3", icon: "/icon2.png"},
+ },
+ },
+ // Create another template with different parameters and apps.
+ {
+ name: "othertemplate",
+ parameters: []*templateParameter{
+ {name: "otherparam1", description: "This is another parameter"},
+ },
+ apps: []templateApp{
+ {name: "otherapp1", icon: "/icon1.png"},
- assert.WithinDuration(t, req.StartTime, resp.Report.StartTime, 0)
- assert.WithinDuration(t, req.EndTime, resp.Report.EndTime, 0)
- assert.Equal(t, resp.Report.ActiveUsers, int64(1), "want one active user")
- for _, app := range resp.Report.AppsUsage {
- if slices.Contains([]string{"reconnecting-pty", "ssh"}, app.Slug) {
- assert.Equal(t, app.Seconds, int64(300), "want app %q to have 5 minutes of usage", app.Slug)
- } else {
- assert.Equal(t, app.Seconds, int64(0), "want app %q to have 0 minutes of usage", app.Slug)
+ // This "special test app" will be converted into web
+ // terminal usage, this is not included in stats since we
+ // currently rely on agent stats for this data.
+ {name: "terminal", icon: "/terminal.png"},
+ },
+ },
}
+
+ // Users and workspaces to generate.
+ users := []*testUser{
+ {
+ name: "user1",
+ workspaces: []*testWorkspace{
+ {
+ name: "workspace1",
+ template: templates[0],
+ buildParameters: []buildParameter{
+ {templateParameter: templates[0].parameters[0], value: "abc"},
+ {templateParameter: templates[0].parameters[1], value: "123"},
+ {templateParameter: templates[0].parameters[2], value: "bbb"},
+ {templateParameter: templates[0].parameters[3], value: "option1"},
+ },
+ },
+ {
+ name: "workspace2",
+ template: templates[1],
+ buildParameters: []buildParameter{
+ {templateParameter: templates[1].parameters[0], value: "ABC"},
+ {templateParameter: templates[1].parameters[1], value: "123"},
+ {templateParameter: templates[1].parameters[2], value: "BBB"},
+ },
+ },
+ {
+ name: "otherworkspace3",
+ template: templates[2],
+ },
+ },
+ },
+ {
+ name: "user2",
+ workspaces: []*testWorkspace{
+ {
+ name: "workspace1",
+ template: templates[0],
+ buildParameters: []buildParameter{
+ {templateParameter: templates[0].parameters[0], value: "abc"},
+ {templateParameter: templates[0].parameters[1], value: "123"},
+ {templateParameter: templates[0].parameters[2], value: "BBB"},
+ {templateParameter: templates[0].parameters[3], value: "option1"},
+ },
+ },
+ },
+ },
+ {
+ name: "user3",
+ workspaces: []*testWorkspace{
+ {
+ name: "otherworkspace1",
+ template: templates[2],
+ buildParameters: []buildParameter{
+ {templateParameter: templates[2].parameters[0], value: "xyz"},
+ },
+ },
+ {
+ name: "workspace2",
+ template: templates[0],
+ buildParameters: []buildParameter{
+ {templateParameter: templates[0].parameters[3], value: "option2"},
+ },
+ },
+ },
+ },
+ }
+
+ return templates, users
+ }
+
+ // Time range for report, test data will be generated within and
+ // outside this range, but only data within the range should be
+ // included in the report.
+ frozenLastNight := time.Date(2023, 8, 22, 0, 0, 0, 0, time.UTC)
+ frozenWeekAgo := frozenLastNight.AddDate(0, 0, -7)
+
+ saoPaulo, err := time.LoadLocation("America/Sao_Paulo")
+ require.NoError(t, err)
+ frozenWeekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, frozenWeekAgo.Format(time.DateTime), saoPaulo)
+ require.NoError(t, err)
+
+ makeBaseTestData := func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen {
+ return map[*testWorkspace]testDataGen{
+ users[0].workspaces[0]: {
+ agentStats: []agentStat{
+ { // One hour of usage.
+ startedAt: frozenWeekAgo,
+ endedAt: frozenWeekAgo.Add(time.Hour),
+ sessionCountVSCode: 1,
+ sessionCountSSH: 1,
+ },
+ { // 12 minutes of usage.
+ startedAt: frozenWeekAgo.AddDate(0, 0, 1),
+ endedAt: frozenWeekAgo.AddDate(0, 0, 1).Add(12 * time.Minute),
+ sessionCountSSH: 1,
+ },
+ { // 1m30s of usage -> 2m rounded.
+ startedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(4*time.Minute + 30*time.Second),
+ endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Minute),
+ sessionCountJetBrains: 1,
+ },
+ },
+ appUsage: []appUsage{
+ { // One hour of usage.
+ app: users[0].workspaces[0].apps[0],
+ startedAt: frozenWeekAgo,
+ endedAt: frozenWeekAgo.Add(time.Hour),
+ requests: 1,
+ },
+ { // 30s of app usage -> 1m rounded.
+ app: users[0].workspaces[0].apps[0],
+ startedAt: frozenWeekAgo.Add(2*time.Hour + 10*time.Second),
+ endedAt: frozenWeekAgo.Add(2*time.Hour + 40*time.Second),
+ requests: 1,
+ },
+ { // 1m30s of app usage -> 2m rounded (included in São Paulo).
+ app: users[0].workspaces[0].apps[0],
+ startedAt: frozenWeekAgo.Add(3*time.Hour + 30*time.Second),
+ endedAt: frozenWeekAgo.Add(3*time.Hour + 90*time.Second),
+ requests: 1,
+ },
+ { // used an app on the last day, counts as active user, 12m.
+ app: users[0].workspaces[0].apps[2],
+ startedAt: frozenWeekAgo.AddDate(0, 0, 6),
+ endedAt: frozenWeekAgo.AddDate(0, 0, 6).Add(12 * time.Minute),
+ requests: 1,
+ },
+ },
+ },
+ users[0].workspaces[1]: {
+ agentStats: []agentStat{
+ {
+ // One hour of usage in second template at the same time
+ // as in first template. When selecting both templates
+ // this user and their app usage will only be counted
+ // once but the template ID will show up in the data.
+ startedAt: frozenWeekAgo,
+ endedAt: frozenWeekAgo.Add(time.Hour),
+ sessionCountVSCode: 1,
+ sessionCountSSH: 1,
+ },
+ },
+ appUsage: []appUsage{
+ // TODO(mafredri): This doesn't behave correctly right now
+ // and will add more usage to the app. This could be
+ // considered both correct and incorrect behavior.
+ // { // One hour of usage, but same user and same template app, only count once.
+ // app: users[0].workspaces[1].apps[0],
+ // startedAt: frozenWeekAgo,
+ // endedAt: frozenWeekAgo.Add(time.Hour),
+ // requests: 1,
+ // },
+ {
+ // Different templates but identical apps, apps will be
+ // combined and usage will be summed.
+ app: users[0].workspaces[1].apps[0],
+ startedAt: frozenWeekAgo.AddDate(0, 0, 2),
+ endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Hour),
+ requests: 1,
+ },
+ },
+ },
+ users[0].workspaces[2]: {
+ agentStats: []agentStat{},
+ appUsage: []appUsage{},
+ },
+ users[1].workspaces[0]: {
+ agentStats: []agentStat{
+ { // One hour of agent usage before timeframe (exclude).
+ startedAt: frozenWeekAgo.Add(-time.Hour),
+ endedAt: frozenWeekAgo,
+ sessionCountVSCode: 1,
+ sessionCountSSH: 1,
+ },
+ { // One hour of usage.
+ startedAt: frozenWeekAgo,
+ endedAt: frozenWeekAgo.Add(time.Hour),
+ sessionCountSSH: 1,
+ },
+ { // One hour of agent usage after timeframe (exclude in UTC, include in São Paulo).
+ startedAt: frozenWeekAgo.AddDate(0, 0, 7),
+ endedAt: frozenWeekAgo.AddDate(0, 0, 7).Add(time.Hour),
+ sessionCountVSCode: 1,
+ sessionCountSSH: 1,
+ },
+ },
+ appUsage: []appUsage{
+ { // One hour of app usage before timeframe (exclude).
+ app: users[1].workspaces[0].apps[2],
+ startedAt: frozenWeekAgo.Add(-time.Hour),
+ endedAt: frozenWeekAgo,
+ requests: 1,
+ },
+ { // One hour of app usage after timeframe (exclude in UTC, include in São Paulo).
+ app: users[1].workspaces[0].apps[2],
+ startedAt: frozenWeekAgo.AddDate(0, 0, 7),
+ endedAt: frozenWeekAgo.AddDate(0, 0, 7).Add(time.Hour),
+ requests: 1,
+ },
+ },
+ },
+ users[2].workspaces[0]: {
+ agentStats: []agentStat{
+ { // One hour of usage.
+ startedAt: frozenWeekAgo,
+ endedAt: frozenWeekAgo.Add(time.Hour),
+ sessionCountSSH: 1,
+ sessionCountReconnectingPTY: 1,
+ },
+ },
+ appUsage: []appUsage{
+ {
+ app: users[2].workspaces[0].apps[0],
+ startedAt: frozenWeekAgo.AddDate(0, 0, 2),
+ endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(5 * time.Minute),
+ requests: 1,
+ },
+ { // Special app; excluded from apps, but counted as active during the day.
+ app: users[2].workspaces[0].apps[1],
+ startedAt: frozenWeekAgo.AddDate(0, 0, 3),
+ endedAt: frozenWeekAgo.AddDate(0, 0, 3).Add(5 * time.Minute),
+ requests: 1,
+ },
+ },
+ },
+ }
+ }
+ type testRequest struct {
+ name string
+ makeRequest func([]*testTemplate) codersdk.TemplateInsightsRequest
+ ignoreTimes bool
+ }
+ tests := []struct {
+ name string
+ makeFixture func() ([]*testTemplate, []*testUser)
+ makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen
+ requests []testRequest
+ }{
+ {
+ name: "multiple users and workspaces",
+ makeFixture: baseTemplateAndUserFixture,
+ makeTestData: makeBaseTestData,
+ requests: []testRequest{
+ {
+ name: "week deployment wide",
+ makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
+ return codersdk.TemplateInsightsRequest{
+ StartTime: frozenWeekAgo,
+ EndTime: frozenWeekAgo.AddDate(0, 0, 7),
+ Interval: codersdk.InsightsReportIntervalDay,
+ }
+ },
+ },
+ {
+ name: "week all templates",
+ makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
+ return codersdk.TemplateInsightsRequest{
+ TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id},
+ StartTime: frozenWeekAgo,
+ EndTime: frozenWeekAgo.AddDate(0, 0, 7),
+ Interval: codersdk.InsightsReportIntervalDay,
+ }
+ },
+ },
+ {
+ name: "week first template",
+ makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
+ return codersdk.TemplateInsightsRequest{
+ TemplateIDs: []uuid.UUID{templates[0].id},
+ StartTime: frozenWeekAgo,
+ EndTime: frozenWeekAgo.AddDate(0, 0, 7),
+ Interval: codersdk.InsightsReportIntervalDay,
+ }
+ },
+ },
+ {
+ name: "week second template",
+ makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
+ return codersdk.TemplateInsightsRequest{
+ TemplateIDs: []uuid.UUID{templates[1].id},
+ StartTime: frozenWeekAgo,
+ EndTime: frozenWeekAgo.AddDate(0, 0, 7),
+ Interval: codersdk.InsightsReportIntervalDay,
+ }
+ },
+ },
+ {
+ name: "week third template",
+ makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
+ return codersdk.TemplateInsightsRequest{
+ TemplateIDs: []uuid.UUID{templates[2].id},
+ StartTime: frozenWeekAgo,
+ EndTime: frozenWeekAgo.AddDate(0, 0, 7),
+ Interval: codersdk.InsightsReportIntervalDay,
+ }
+ },
+ },
+ {
+ // São Paulo is three hours behind UTC, so we should not see
+ // any data between weekAgo and weekAgo.Add(3 * time.Hour).
+ name: "week other timezone (São Paulo)",
+ makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
+ return codersdk.TemplateInsightsRequest{
+ StartTime: frozenWeekAgoSaoPaulo,
+ EndTime: frozenWeekAgoSaoPaulo.AddDate(0, 0, 7),
+ Interval: codersdk.InsightsReportIntervalDay,
+ }
+ },
+ },
+ },
+ },
+ {
+ name: "parameters",
+ makeFixture: baseTemplateAndUserFixture,
+ makeTestData: func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen {
+ return map[*testWorkspace]testDataGen{}
+ },
+ requests: []testRequest{
+ {
+ // Since workspaces are created "now", we can only get
+ // parameters using a time range that includes "now".
+ // We check yesterday and today for stability just in case
+ // the test runs at UTC midnight.
+ name: "yesterday and today deployment wide",
+ ignoreTimes: true,
+ makeRequest: func(_ []*testTemplate) codersdk.TemplateInsightsRequest {
+ now := time.Now().UTC()
+ return codersdk.TemplateInsightsRequest{
+ StartTime: now.Truncate(24*time.Hour).AddDate(0, 0, -1),
+ EndTime: now.Truncate(time.Hour).Add(time.Hour),
+ }
+ },
+ },
+ {
+ name: "two days ago, no data",
+ ignoreTimes: true,
+ makeRequest: func(_ []*testTemplate) codersdk.TemplateInsightsRequest {
+ twoDaysAgo := time.Now().UTC().Truncate(24*time.Hour).AddDate(0, 0, -2)
+ return codersdk.TemplateInsightsRequest{
+ StartTime: twoDaysAgo,
+ EndTime: twoDaysAgo.AddDate(0, 0, 1),
+ }
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ require.NotNil(t, tt.makeFixture, "test bug: makeFixture must be set")
+ require.NotNil(t, tt.makeTestData, "test bug: makeTestData must be set")
+ templates, users, testData := prepareFixtureAndTestData(t, tt.makeFixture, tt.makeTestData)
+ client := prepare(t, templates, users, testData)
+
+ for _, req := range tt.requests {
+ req := req
+ t.Run(req.name, func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitMedium)
+
+ report, err := client.TemplateInsights(ctx, req.makeRequest(templates))
+ require.NoError(t, err, "want no error getting template insights")
+
+ if req.ignoreTimes {
+ // Ignore times, we're only interested in the data.
+ report.Report.StartTime = time.Time{}
+ report.Report.EndTime = time.Time{}
+ for i := range report.IntervalReports {
+ report.IntervalReports[i].StartTime = time.Time{}
+ report.IntervalReports[i].EndTime = time.Time{}
+ }
+ }
+
+ partialName := strings.Join(strings.Split(t.Name(), "/")[1:], "_")
+ goldenFile := filepath.Join("testdata", "insights", partialName+".json.golden")
+ if *updateGoldenFiles {
+ err = os.MkdirAll(filepath.Dir(goldenFile), 0o755)
+ require.NoError(t, err, "want no error creating golden file directory")
+ f, err := os.Create(goldenFile)
+ require.NoError(t, err, "want no error creating golden file")
+ defer f.Close()
+ enc := json.NewEncoder(f)
+ enc.SetIndent("", " ")
+ enc.Encode(report)
+ return
+ }
+
+ f, err := os.Open(goldenFile)
+ require.NoError(t, err, "open golden file, run \"make update-golden-files\" and commit the changes")
+ defer f.Close()
+ var want codersdk.TemplateInsightsResponse
+ err = json.NewDecoder(f).Decode(&want)
+ require.NoError(t, err, "want no error decoding golden file")
+
+ cmpOpts := []cmp.Option{
+ // Ensure readable UUIDs in diff.
+ cmp.Transformer("UUIDs", func(in []uuid.UUID) (s []string) {
+ for _, id := range in {
+ s = append(s, id.String())
+ }
+ return s
+ }),
+ }
+ // Use cmp.Diff here because it produces more readable diffs.
+ assert.Empty(t, cmp.Diff(want, report, cmpOpts...), "golden file mismatch (-want +got): %s, run \"make update-golden-files\", verify and commit the changes", goldenFile)
+ })
+ }
+ })
}
- // The full timeframe is <= 24h, so the interval matches exactly.
- require.Len(t, resp.IntervalReports, 1, "want one interval report")
- assert.WithinDuration(t, req.StartTime, resp.IntervalReports[0].StartTime, 0)
- assert.WithinDuration(t, req.EndTime, resp.IntervalReports[0].EndTime, 0)
- assert.Equal(t, resp.IntervalReports[0].ActiveUsers, int64(1), "want one active user in the interval report")
-
- // The workspace uses 3 parameters
- require.Len(t, resp.Report.ParametersUsage, 3)
- assert.Equal(t, firstParameterName, resp.Report.ParametersUsage[0].Name)
- assert.Equal(t, firstParameterDisplayName, resp.Report.ParametersUsage[0].DisplayName)
- assert.Contains(t, resp.Report.ParametersUsage[0].Values, codersdk.TemplateParameterValue{
- Value: firstParameterValue,
- Count: 1,
- })
- assert.Contains(t, resp.Report.ParametersUsage[0].TemplateIDs, template.ID)
- assert.Empty(t, resp.Report.ParametersUsage[0].Options)
-
- assert.Equal(t, secondParameterName, resp.Report.ParametersUsage[1].Name)
- assert.Equal(t, secondParameterDisplayName, resp.Report.ParametersUsage[1].DisplayName)
- assert.Contains(t, resp.Report.ParametersUsage[1].Values, codersdk.TemplateParameterValue{
- Value: secondParameterValue,
- Count: 1,
- })
- assert.Contains(t, resp.Report.ParametersUsage[1].TemplateIDs, template.ID)
- assert.Empty(t, resp.Report.ParametersUsage[1].Options)
-
- assert.Equal(t, thirdParameterName, resp.Report.ParametersUsage[2].Name)
- assert.Equal(t, thirdParameterDisplayName, resp.Report.ParametersUsage[2].DisplayName)
- assert.Contains(t, resp.Report.ParametersUsage[2].Values, codersdk.TemplateParameterValue{
- Value: thirdParameterValue,
- Count: 1,
- })
- assert.Contains(t, resp.Report.ParametersUsage[2].TemplateIDs, template.ID)
- assert.Equal(t, []codersdk.TemplateVersionParameterOption{
- {Name: thirdParameterOptionName1, Value: thirdParameterOptionValue1},
- {Name: thirdParameterOptionName2, Value: thirdParameterOptionValue2},
- {Name: thirdParameterOptionName3, Value: thirdParameterOptionValue3},
- }, resp.Report.ParametersUsage[2].Options)
}
func TestTemplateInsights_BadRequest(t *testing.T) {
diff --git a/coderd/members.go b/coderd/members.go
index 67ab19cf45687..91083cbb89814 100644
--- a/coderd/members.go
+++ b/coderd/members.go
@@ -8,13 +8,13 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/rbac"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Assign role to organization member
diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go
index d25b716f2df15..f5aaeb89f7a24 100644
--- a/coderd/metricscache/metricscache.go
+++ b/coderd/metricscache/metricscache.go
@@ -15,9 +15,9 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/codersdk"
"github.com/coder/retry"
)
@@ -146,8 +146,14 @@ func convertDAUResponse[T dauRow](rows []T, tzOffset int) codersdk.DAUsResponse
}
dates := maps.Keys(respMap)
- slices.SortFunc(dates, func(a, b time.Time) bool {
- return a.Before(b)
+ slices.SortFunc(dates, func(a, b time.Time) int {
+ if a.Before(b) {
+ return -1
+ } else if a.Equal(b) {
+ return 0
+ } else {
+ return 1
+ }
})
var resp codersdk.DAUsResponse
diff --git a/coderd/metricscache/metricscache_test.go b/coderd/metricscache/metricscache_test.go
index 52c0df8a3ebd8..fc22cc1139c09 100644
--- a/coderd/metricscache/metricscache_test.go
+++ b/coderd/metricscache/metricscache_test.go
@@ -10,12 +10,12 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/metricscache"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/metricscache"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func dateH(year, month, day, hour int) time.Time {
diff --git a/coderd/oauthpki/oidcpki.go b/coderd/oauthpki/oidcpki.go
new file mode 100644
index 0000000000000..c44d130e5be9f
--- /dev/null
+++ b/coderd/oauthpki/oidcpki.go
@@ -0,0 +1,275 @@
+package oauthpki
+
+import (
+ "context"
+ "crypto/rsa"
+ "crypto/sha1" //#nosec // Not used for cryptography.
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "encoding/pem"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/golang-jwt/jwt/v4"
+ "github.com/google/uuid"
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/jws"
+ "golang.org/x/xerrors"
+
+ "github.com/coder/coder/v2/coderd/httpmw"
+)
+
+// Config uses jwt assertions over client_secret for oauth2 authentication of
+// the application. This implementation was made specifically for Azure AD.
+//
+// https://learn.microsoft.com/en-us/azure/active-directory/develop/certificate-credentials
+//
+// However this does mostly follow the standard. We can generalize this as we
+// include support for more IDPs.
+//
+// https://datatracker.ietf.org/doc/html/rfc7523
+type Config struct {
+ cfg httpmw.OAuth2Config
+
+ // These values should match those provided in the oauth2.Config.
+ // Because the inner config is an interface, we need to duplicate these
+ // values here.
+ scopes []string
+ clientID string
+ tokenURL string
+
+ // ClientSecret is the private key of the PKI cert.
+ // Azure AD only supports RS256 signing algorithm.
+ clientKey *rsa.PrivateKey
+ // Base64url-encoded SHA-1 thumbprint of the X.509 certificate's DER encoding.
+ // This is specific to Azure AD
+ x5t string
+}
+
+type ConfigParams struct {
+ ClientID string
+ TokenURL string
+ Scopes []string
+ PemEncodedKey []byte
+ PemEncodedCert []byte
+
+ Config httpmw.OAuth2Config
+}
+
+// NewOauth2PKIConfig creates the oauth2 config for PKI based auth. It requires the certificate and it's private key.
+// The values should be passed in as PEM encoded values, which is the standard encoding for x509 certs saved to disk.
+// It should look like:
+//
+// -----BEGIN RSA PRIVATE KEY----
+// ...
+// -----END RSA PRIVATE KEY-----
+//
+// -----BEGIN CERTIFICATE-----
+// ...
+// -----END CERTIFICATE-----
+func NewOauth2PKIConfig(params ConfigParams) (*Config, error) {
+ if params.ClientID == "" {
+ return nil, xerrors.Errorf("")
+ }
+ if len(params.Scopes) == 0 {
+ return nil, xerrors.Errorf("scopes are required")
+ }
+
+ rsaKey, err := decodeClientKey(params.PemEncodedKey)
+ if err != nil {
+ return nil, err
+ }
+
+ // Azure AD requires a certificate. The sha1 of the cert is used to identify the signer.
+ // This is not required in the general specification.
+ if strings.Contains(strings.ToLower(params.TokenURL), "microsoftonline") && len(params.PemEncodedCert) == 0 {
+ return nil, xerrors.Errorf("oidc client certificate is required and missing")
+ }
+
+ block, _ := pem.Decode(params.PemEncodedCert)
+ // Used as an identifier, not an actual cryptographic hash.
+ //nolint:gosec
+ hashed := sha1.Sum(block.Bytes)
+
+ return &Config{
+ clientID: params.ClientID,
+ tokenURL: params.TokenURL,
+ scopes: params.Scopes,
+ cfg: params.Config,
+ clientKey: rsaKey,
+ x5t: base64.StdEncoding.EncodeToString(hashed[:]),
+ }, nil
+}
+
+// decodeClientKey decodes a PEM encoded rsa secret.
+func decodeClientKey(pemEncoded []byte) (*rsa.PrivateKey, error) {
+ block, _ := pem.Decode(pemEncoded)
+ key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to parse private key: %w", err)
+ }
+
+ return key, nil
+}
+
+func (ja *Config) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
+ return ja.cfg.AuthCodeURL(state, opts...)
+}
+
+// Exchange includes the client_assertion signed JWT.
+func (ja *Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
+ signed, err := ja.jwtToken()
+ if err != nil {
+ return nil, xerrors.Errorf("failed jwt assertion: %w", err)
+ }
+ opts = append(opts,
+ oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
+ oauth2.SetAuthURLParam("client_assertion", signed),
+ )
+ return ja.cfg.Exchange(ctx, code, opts...)
+}
+
+func (ja *Config) jwtToken() (string, error) {
+ now := time.Now()
+ token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
+ "iss": ja.clientID,
+ "sub": ja.clientID,
+ "aud": ja.tokenURL,
+ // 5-10 minutes is recommended in the Azure docs.
+ // So we'll use 5 minutes.
+ "exp": now.Add(time.Minute * 5).Unix(),
+ "jti": uuid.New().String(),
+ "nbf": now.Unix(),
+ "iat": now.Unix(),
+ })
+ token.Header["x5t"] = ja.x5t
+
+ signed, err := token.SignedString(ja.clientKey)
+ if err != nil {
+ return "", xerrors.Errorf("sign jwt assertion: %w", err)
+ }
+ return signed, nil
+}
+
+func (ja *Config) TokenSource(ctx context.Context, token *oauth2.Token) oauth2.TokenSource {
+ return oauth2.ReuseTokenSource(token, &jwtTokenSource{
+ cfg: ja,
+ ctx: ctx,
+ refreshToken: token.RefreshToken,
+ })
+}
+
+type jwtTokenSource struct {
+ cfg *Config
+ ctx context.Context
+ refreshToken string
+}
+
+// Token must be safe for concurrent use by multiple go routines
+// Very similar to the RetrieveToken implementation by the oauth2 package.
+// https://github.com/golang/oauth2/blob/master/internal/token.go#L212
+// Oauth2 package keeps this code unexported or in an /internal package,
+// so we have to copy the implementation :(
+func (src *jwtTokenSource) Token() (*oauth2.Token, error) {
+ if src.refreshToken == "" {
+ return nil, xerrors.New("oauth2: token expired and refresh token is not set")
+ }
+ cli := http.DefaultClient
+ if v, ok := src.ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
+ cli = v
+ }
+
+ token, err := src.cfg.jwtToken()
+ if err != nil {
+ return nil, xerrors.Errorf("failed jwt assertion: %w", err)
+ }
+
+ v := url.Values{
+ "client_assertion": {token},
+ "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
+ "client_id": {src.cfg.clientID},
+ "grant_type": {"refresh_token"},
+ "scope": {strings.Join(src.cfg.scopes, " ")},
+ "refresh_token": {src.refreshToken},
+ }
+ // Using params based auth
+ req, err := http.NewRequest("POST", src.cfg.tokenURL, strings.NewReader(v.Encode()))
+ if err != nil {
+ return nil, xerrors.Errorf("oauth2: make token refresh request: %w", err)
+ }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req = req.WithContext(src.ctx)
+ resp, err := cli.Do(req)
+ if err != nil {
+ return nil, xerrors.Errorf("oauth2: cannot get token: %w", err)
+ }
+
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, xerrors.Errorf("oauth2: cannot fetch token reading response body: %w", err)
+ }
+
+ var tokenRes struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type,omitempty"`
+ RefreshToken string `json:"refresh_token,omitempty"`
+
+ // Extra fields returned by the refresh that are needed
+ IDToken string `json:"id_token"`
+ ExpiresIn int64 `json:"expires_in"` // relative seconds from now
+ // error fields
+ // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
+ ErrorCode string `json:"error"`
+ ErrorDescription string `json:"error_description"`
+ ErrorURI string `json:"error_uri"`
+ }
+
+ unmarshalError := json.Unmarshal(body, &tokenRes)
+
+ if resp.StatusCode < 200 || resp.StatusCode > 299 {
+ // Return a standard oauth2 error. Attempt to read some error fields. The error fields
+ // can be encoded in a few places, so this does not catch all of them.
+ return nil, &oauth2.RetrieveError{
+ Response: resp,
+ Body: body,
+ // Best effort for error fields
+ ErrorCode: tokenRes.ErrorCode,
+ ErrorDescription: tokenRes.ErrorDescription,
+ ErrorURI: tokenRes.ErrorURI,
+ }
+ }
+
+ if unmarshalError != nil {
+ return nil, xerrors.Errorf("oauth2: cannot unmarshal token: %w", err)
+ }
+
+ newToken := &oauth2.Token{
+ AccessToken: tokenRes.AccessToken,
+ TokenType: tokenRes.TokenType,
+ RefreshToken: tokenRes.RefreshToken,
+ }
+
+ if secs := tokenRes.ExpiresIn; secs > 0 {
+ newToken.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
+ }
+
+ // ID token is a JWT token. We can decode it to get the expiry.
+ // Not really sure what to do if the ExpiresIn and JWT expiry differ,
+ // but this one is attached in the JWT and guaranteed to be right for local
+ // validation. So use this one if found.
+ if v := tokenRes.IDToken; v != "" {
+ // decode returned id token to get expiry
+ claimSet, err := jws.Decode(v)
+ if err != nil {
+ return nil, xerrors.Errorf("oauth2: error decoding JWT token: %w", err)
+ }
+ newToken.Expiry = time.Unix(claimSet.Exp, 0)
+ }
+
+ return newToken, nil
+}
diff --git a/coderd/oauthpki/okidcpki_test.go b/coderd/oauthpki/okidcpki_test.go
new file mode 100644
index 0000000000000..ab6e3e3a08179
--- /dev/null
+++ b/coderd/oauthpki/okidcpki_test.go
@@ -0,0 +1,351 @@
+package oauthpki_test
+
+import (
+ "context"
+ "encoding/base64"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/golang-jwt/jwt/v4"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/oauth2"
+ "golang.org/x/xerrors"
+
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/coderdtest/oidctest"
+ "github.com/coder/coder/v2/coderd/oauthpki"
+ "github.com/coder/coder/v2/testutil"
+)
+
+const (
+ testClientKey = `-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAnUryZEfn5kA8wuk9a7ogFuWbk3uPHEhioYuAg9m3/tIdqSqu
+ASpRzw8+1nORTf3ykWRRlhxZWnKimmkB0Ux5Yrz9TDVWDQbzEH3B8ibMlmaNcoN8
+wYVzeEpqCe3fJagnV0lh0sHB1Z+vhcJ/M2nEAdyfhIgQEbG6Xtl2+WcGqyMWUJpV
+g8+ebK+JkXELAGN1hg3DdV52gjodEjoe1/ibHz8y3NR7j2tOKix7iKOhccyFkD35
+xqSnfyZJK5yxIfmGiWdVOIGqc2rYpgvrXJLTOjLoeyDSNi+Q604T64ZxsqfuM4LX
+BakVG3EwHFXPBfsBKjUE9HYvXEXw3fJP9K6mIwIDAQABAoIBAQCb+aH7x0IylSir
+r1Z06RDBI9bunOwBA9aqkwdRuCg4zGsVQXljNnABgACz7837JQPRIUW2MU553otX
+yyE+RzNnsjkLxSgbqvSFOe+FDOx7iB5jm/euf4NNmZ0lU3iggurgJ6iVsgVgrQUF
+AyXX+d2gawLUDYjBwxgozkSodH2sXYSX+SWfSOXHsFzSa3tLtUMbAIflM0rlRXf7
+Z57M8mMomZUvmmojH+TnBQljJlU8lhrvOaDD4DT8qAtVHE3VluDBQ9/3E8OIjz+E
+EqUgWLgrdq1rIMhJbHN90NwLwWs+2PcRfdB6hqKPktLne2KZFOgVKlxPKOYByBq1
+PX/vJ/HBAoGBAMFmJ6nYqyUVl26ajlXmnXBjQ+iBLHo9lcUu84+rpqRf90Bsm5bd
+jMmYr3Yo3yXNiit3rvZzBfPElo+IVa1HpPtgOaa2AU5B3QzxWCNT0FNRQqMG2LcA
+CvB10pOdJEABQxr7d4eFRg2/KbF1fr0r0vqMEelwa5ejTg6ROD3DtadpAoGBANA0
+4EClniCwvd1IECy2oTuTDosXgmRKwRAcwgE34YXy1Y/L4X/ghFeCHi3ybrep0uwL
+ptJNK+0sqvPu6UhC356GfMqfuzOKNMkXybnPUbHrz5KTkN+QQMfPc73Veel2gpD3
+xNataEmHtxcOx0X1OnjwyZZpmMbrUY3Cackn+durAoGBAKYR5nU+jJfnloVvSlIR
+GZhsZN++LEc7ouQTkSoJp6r2jQZRPLmrvT1PUzwPlK6NdNwmhaMy2iWc5fySgZ+u
+KcmBs3+oQi7E9+ApThnn2rfwy1vagTWDX+FkC1KeWYZsjwcYcGd61dDwGgk8b3xZ
+qW1j4e2mj31CycBQiw7eg5ohAoGADvkOe3etlHpBXS12hFCp7afYruYE6YN6uNbo
+mL/VBxX8h7fIwrJ5sfVYiENb9PdQhMsdtxf3pbnFnX875Ydxn2vag5PTGZTB0QhV
+6HfhTyM/LTJRg9JS5kuj7i3w83ojT5uR20JjMo6A+zaD3CMTjmj6hkeXxg5cMg6e
+HuoyDLsCgYBcbboYMFT1cUSxBeMtPGt3CxxZUYnUQaRUeOcjqYYlFL+DCWhY7pxH
+EnLhwW/KzkDzOmwRmmNOMqD7UhR/ayxR+avRt6v5d5l8fVCuNexgs7kR9L5IQp9l
+YV2wsCoXBCcuPmio/te44U//BlzprEu0w1iHpb3ibmQg4y291R0TvQ==
+-----END RSA PRIVATE KEY-----`
+
+ testClientCert = `
+-----BEGIN CERTIFICATE-----
+MIIEOjCCAiKgAwIBAgIQMO50KnWsRbmrrthPQgyubjANBgkqhkiG9w0BAQsFADAY
+MRYwFAYDVQQDEw1Mb2NhbGhvc3RDZXJ0MB4XDTIzMDgxMDE2MjYxOFoXDTI1MDIx
+MDE2MjU0M1owFDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAnUryZEfn5kA8wuk9a7ogFuWbk3uPHEhioYuAg9m3/tId
+qSquASpRzw8+1nORTf3ykWRRlhxZWnKimmkB0Ux5Yrz9TDVWDQbzEH3B8ibMlmaN
+coN8wYVzeEpqCe3fJagnV0lh0sHB1Z+vhcJ/M2nEAdyfhIgQEbG6Xtl2+WcGqyMW
+UJpVg8+ebK+JkXELAGN1hg3DdV52gjodEjoe1/ibHz8y3NR7j2tOKix7iKOhccyF
+kD35xqSnfyZJK5yxIfmGiWdVOIGqc2rYpgvrXJLTOjLoeyDSNi+Q604T64Zxsqfu
+M4LXBakVG3EwHFXPBfsBKjUE9HYvXEXw3fJP9K6mIwIDAQABo4GDMIGAMA4GA1Ud
+DwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0O
+BBYEFAYCdgydG3h2SNWF+BfAyJtNliJtMB8GA1UdIwQYMBaAFHR/aptP0RUNNFyf
+5uky527SECt1MA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAI6P
+ymG7l06JvJ3p6xgaMyOxgkpQl6WkY4LJHVEhfeDSoO3qsJc4PxUdSExJsT84weXb
+lF+tK6D/CPlvjmG720IlB5cSKJ71rWjwmaMWKxWKXyoZdDrHAp55+FNdXegUZF2o
+EF/ZM5CHaO8iHMkuWEv1OASHBQWC/o4spUN5HGQ9HepwLVvO/aX++LYfvfL9faKA
+IT+w9i8pJbfItFmfA8x2OEVZk8aEA0WtKdfsMwzGmZ1GSGa4UYcynxQGCMiB5h4L
+C/dpoJRbEzdGLuTZgV2SCaN3k5BrH4aaILI9tqZaq0gamN9Rd2yji3cGiduCeAAo
+RmVcl9fBliMLxylWEP5+B2JmCZEc8Lfm0TBNnjaG17KY40gzbfBYixBxBTYgsPua
+bfprtfksSG++zcsDbkC8CtPamtlNWtDAiFp4yQRkP79PlJO6qCdTrFWPukTMCMso
+25hjLvxj1fLy/jSMDEZu/oQ14TMCZSGHRjz4CPiaCfXqgqOtVOD+5+yWInwUGp/i
+Nb1vIq4ruEAbyCbdWKHbE0yT5AP7hm5ZNybpZ4/311AEBD2HKip/OqB05p99XcLw
+BIC4ODNvwCn6x00KZoqWz/MX2dEQ/HqWiWaDB/OSemfTVE3I94mzEWnqpF2cQpcT
+B1B7CpkMU55hPP+7nsofCszNrMDXT8Z5w2a3zLKM
+-----END CERTIFICATE-----
+`
+)
+
+// TestAzureADPKIOIDC ensures we do not break Azure AD compatibility.
+// It runs an oauth2.Exchange method and hijacks the request to only check the
+// request side of the transaction.
+func TestAzureADPKIOIDC(t *testing.T) {
+ t.Parallel()
+
+ oauthCfg := &oauth2.Config{
+ ClientID: "random-client-id",
+ Endpoint: oauth2.Endpoint{
+ TokenURL: "https://login.microsoftonline.com/6a1e9139-13f2-4afb-8f46-036feac8bd79/v2.0/token",
+ },
+ }
+
+ pkiConfig, err := oauthpki.NewOauth2PKIConfig(oauthpki.ConfigParams{
+ ClientID: oauthCfg.ClientID,
+ TokenURL: oauthCfg.Endpoint.TokenURL,
+ PemEncodedKey: []byte(testClientKey),
+ PemEncodedCert: []byte(testClientCert),
+ Config: oauthCfg,
+ Scopes: []string{"openid", "email", "profile"},
+ })
+ require.NoError(t, err, "failed to create pki config")
+
+ ctx := testutil.Context(t, testutil.WaitMedium)
+ ctx = oidc.ClientContext(ctx, &http.Client{
+ Transport: &fakeRoundTripper{
+ roundTrip: func(req *http.Request) (*http.Response, error) {
+ resp := &http.Response{
+ Status: "500 Internal Service Error",
+ }
+ // This is the easiest way to hijack the request and check
+ // the params. The oauth2 package uses unexported types and
+ // options, so we need to view the actual request created.
+ assertJWTAuth(t, req)
+ return resp, nil
+ },
+ },
+ })
+ _, err = pkiConfig.Exchange(ctx, base64.StdEncoding.EncodeToString([]byte("random-code")))
+ // We hijack the request and return an error intentionally
+ require.Error(t, err, "error expected")
+}
+
+// TestAzureAKPKIWithCoderd uses a fake IDP and a real Coderd to test PKI auth.
+// nolint:bodyclose
+func TestAzureAKPKIWithCoderd(t *testing.T) {
+ t.Parallel()
+
+ scopes := []string{"openid", "email", "profile", "offline_access"}
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithIssuer("https://login.microsoftonline.com/fake_app"),
+ oidctest.WithCustomClientAuth(func(t testing.TB, req *http.Request) (url.Values, error) {
+ values := assertJWTAuth(t, req)
+ if values == nil {
+ return nil, xerrors.New("authorizatin failed in request")
+ }
+ return values, nil
+ }),
+ oidctest.WithServing(),
+ )
+ cfg := fake.OIDCConfig(t, scopes, func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ })
+
+ oauthCfg := cfg.OAuth2Config.(*oauth2.Config)
+ // Create the oauthpki config
+ pki, err := oauthpki.NewOauth2PKIConfig(oauthpki.ConfigParams{
+ ClientID: oauthCfg.ClientID,
+ TokenURL: oauthCfg.Endpoint.TokenURL,
+ Scopes: scopes,
+ PemEncodedKey: []byte(testClientKey),
+ PemEncodedCert: []byte(testClientCert),
+ Config: oauthCfg,
+ })
+ require.NoError(t, err)
+ cfg.OAuth2Config = pki
+
+ owner, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
+ OIDCConfig: cfg,
+ })
+
+ // Create a user and login
+ const email = "alice@coder.com"
+ claims := jwt.MapClaims{
+ "email": email,
+ }
+ helper := oidctest.NewLoginHelper(owner, fake)
+ user, _ := helper.Login(t, claims)
+
+ // Try refreshing the token more than once.
+ for i := 0; i < 2; i++ {
+ helper.ForceRefresh(t, api.Database, user, claims)
+ }
+}
+
+// TestSavedAzureADPKIOIDC was created by capturing actual responses from an Azure
+// AD instance and saving them to replay, removing some details.
+// The reason this is done is that this is the only way to assert values
+// passed to the oauth2 provider via http requests.
+// It is not feasible to run against an actual Azure AD instance, so this attempts
+// to prevent some regressions by running a full "e2e" oauth and asserting some
+// of the request values.
+func TestSavedAzureADPKIOIDC(t *testing.T) {
+ t.Parallel()
+
+ var (
+ stateString = "random-state"
+ oauth2Code = base64.StdEncoding.EncodeToString([]byte("random-code"))
+ )
+
+ // Real oauth config. We will hijack all http requests so some of these values
+ // are fake.
+ cfg := &oauth2.Config{
+ ClientID: "fake_app",
+ ClientSecret: "",
+ Endpoint: oauth2.Endpoint{
+ AuthURL: "https://login.microsoftonline.com/fake_app/oauth2/v2.0/authorize",
+ TokenURL: "https://login.microsoftonline.com/fake_app/oauth2/v2.0/token",
+ AuthStyle: 0,
+ },
+ RedirectURL: "http://localhost/api/v2/users/oidc/callback",
+ Scopes: []string{"openid", "profile", "email", "offline_access"},
+ }
+
+ initialExchange := false
+ tokenRefreshed := false
+
+ // Create the oauthpki config
+ pki, err := oauthpki.NewOauth2PKIConfig(oauthpki.ConfigParams{
+ ClientID: cfg.ClientID,
+ TokenURL: cfg.Endpoint.TokenURL,
+ Scopes: []string{"openid", "email", "profile", "offline_access"},
+ PemEncodedKey: []byte(testClientKey),
+ PemEncodedCert: []byte(testClientCert),
+ Config: cfg,
+ })
+ require.NoError(t, err)
+
+ var fakeCtx context.Context
+ fakeClient := &http.Client{
+ Transport: fakeRoundTripper{
+ roundTrip: func(req *http.Request) (*http.Response, error) {
+ if strings.Contains(req.URL.String(), "authorize") {
+ // This is the user hitting the browser endpoint to begin the OIDC
+ // auth flow.
+
+ // Authorize should redirect the user back to the app after authentication on
+ // the IDP.
+ resp := httptest.NewRecorder()
+ v := url.Values{
+ "code": {oauth2Code},
+ "state": {stateString},
+ "session_state": {"a18cf797-1e2b-4bc3-baf9-66b41a4997cf"},
+ }
+
+ // This url doesn't really matter since the fake client will hiject this actual request.
+ http.Redirect(resp, req, "http://localhost:3000/api/v2/users/oidc/callback?"+v.Encode(), http.StatusTemporaryRedirect)
+ return resp.Result(), nil
+ }
+ if strings.Contains(req.URL.String(), "v2.0/token") {
+ vals := assertJWTAuth(t, req)
+ switch vals.Get("grant_type") {
+ case "authorization_code":
+ // Initial token
+ initialExchange = true
+ assert.Equal(t, oauth2Code, vals.Get("code"), "initial exchange code mismatch")
+ case "refresh_token":
+ // refreshed token
+ tokenRefreshed = true
+ assert.Equal(t, "", vals.Get("refresh_token"), "refresh token required")
+ }
+
+ resp := httptest.NewRecorder()
+ // Taken from an actual response
+ // Just always return a token no matter what.
+ resp.Header().Set("Content-Type", "application/json")
+ _, _ = resp.Write([]byte(`{
+ "token_type":"Bearer",
+ "scope":"email openid profile AccessReview.ReadWrite.Membership Group.Read.All Group.ReadWrite.All User.Read",
+ "expires_in":4009,
+ "ext_expires_in":4009,
+ "access_token":"",
+ "refresh_token":"",
+ "id_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyJ9.eyJhdWQiOiIxZjAxODMyYS1mZWViLTQyZGMtODFkOS01ZjBhYjZhMDQxZTAiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vMTEwZjBjMGYtY2Q3Ni00NzE3LWE2ZjgtNGVlYTNkMGY4MTA5L3YyLjAiLCJpYXQiOjE2OTE3OTI2MzQsIm5iZiI6MTY5MTc5MjYzNCwiZXhwIjoxNjkxNzk2NTM0LCJhaW8iOiJBWVFBZS84VUFBQUE1eEtqMmVTdWFXVmZsRlhCeGJJTnMvSkVyVHFvUGlaQW5ENmJIZWF3a2RRcisyRVRwM3RGNGY3akxicnh3ODhhVm9QOThrY0xMNjhON1hVV3FCN1I1N2JQRU9EclRlSUI1S0lyUHBjbCtIeXR0a1ljOVdWQklVVEErSllQbzl1a0ZjbGNWZ1krWUc3eHlmdi90K3Q1ZEczblNuZEdEQ1FYRVIxbDlTNko1T2c9IiwiZW1haWwiOiJzdGV2ZW5AY29kZXIuY29tIiwiZ3JvdXBzIjpbImM4MDQ4ZTkxLWY1YzMtNDdlNS05NjkzLTgzNGRlODQwMzRhZCIsIjcwYjQ4MTc1LTEwN2ItNGFkOC1iNDA1LTRkODg4YTFjNDY2ZiJdLCJpZHAiOiJtYWlsIiwibmFtZSI6IlN0ZXZlbiBNIiwib2lkIjoiN2JhNDYzNjAtZTAyNy00OTVhLTlhZTUtM2FlYWZlMzY3MGEyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoic3RldmVuQGNvZGVyLmNvbSIsInByb3ZfZGF0YSI6W3siQXQiOnRydWUsIlByb3YiOiJnaXRodWIuY29tIiwiQWx0c2VjaWQiOiI1NDQ2Mjk4IiwiQWNjZXNzVG9rZW4iOm51bGx9XSwicmgiOiIwLkFUZ0FEd3dQRVhiTkYwZW0tRTdxUFEtQkNTcURBUl9yX3R4Q2dkbGZDcmFnUWVBNEFPRS4iLCJyb2xlcyI6WyJUZW1wbGF0ZUF1dGhvcnMiXSwic3ViIjoib0JTN3FjUERKdWlDMEYyQ19XdDJycVlvanhpT0o3S3JFWjlkQ1RkTGVYNCIsInRpZCI6IjExMGYwYzBmLWNkNzYtNDcxNy1hNmY4LTRlZWEzZDBmODEwOSIsInV0aSI6IktReGlIWGtaZUVxcC1tQWlVdTlyQUEiLCJ2ZXIiOiIyLjAiLCJyb2xlczIiOiJUZW1wbGF0ZUF1dGhvcnMifQ.JevFI4Xm9dW7kQq4xEgZnUaU0SqbeOAFtT0YIKQNefR9Db4sjxCaKRmX0pPt-CM9j45d6fAiAkLFDAqjlSbi4Zi0GbEomT3yegmuxKgEgjPpJlGjF2TBUpsNNyn5gJ9Wkct9BfwALJhX2ePJFzIlkvx9opNNbNK1qHKMMjOSRFG6AGExKRDiQAME0a4hVgCwrAdUs4JrCcj4LqB84dODN-eoh-jx2-1wDvf6fovfwLHDQwjY4lfBxaYdNavKM369hrhU-U067rSnCzvDD26f4VLhPF52hiQIbTVN5t7p_1XmcduUiaNnmr9AZiZxZ-94mctSRRR8xG0pNwO2yv84iA"
+ }`))
+ return resp.Result(), nil
+ }
+ // This is the "Coder" half of things. We can keep this in the fake
+ // client, essentially being the fake client on both sides of the OIDC
+ // flow.
+ if strings.Contains(req.URL.String(), "v2/users/oidc/callback") {
+ // This is the callback from the IDP.
+ code := req.URL.Query().Get("code")
+ require.Equal(t, oauth2Code, code, "code mismatch")
+ state := req.URL.Query().Get("state")
+ require.Equal(t, stateString, state, "state mismatch")
+
+ // Exchange for token should work
+ token, err := pki.Exchange(fakeCtx, code)
+ if !assert.NoError(t, err) {
+ return httptest.NewRecorder().Result(), nil
+ }
+
+ // Also try a refresh
+ cpy := token
+ cpy.Expiry = time.Now().Add(time.Minute * -1)
+ src := pki.TokenSource(fakeCtx, cpy)
+ _, err = src.Token()
+ tokenRefreshed = true
+ assert.NoError(t, err, "token refreshed")
+ return httptest.NewRecorder().Result(), nil
+ }
+
+ return nil, xerrors.Errorf("not implemented")
+ },
+ },
+ }
+ fakeCtx = oidc.ClientContext(context.Background(), fakeClient)
+ _ = fakeCtx
+
+ // This simulates a client logging into the browser. The 307 redirect will
+ // make sure this goes through the full flow.
+ // nolint: noctx
+ resp, err := fakeClient.Get(pki.AuthCodeURL("state", oauth2.AccessTypeOffline))
+ require.NoError(t, err)
+ _ = resp.Body.Close()
+
+ require.True(t, initialExchange, "initial token exchange complete")
+ require.True(t, tokenRefreshed, "token was refreshed")
+}
+
+type fakeRoundTripper struct {
+ roundTrip func(req *http.Request) (*http.Response, error)
+}
+
+func (f fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ return f.roundTrip(req)
+}
+
+// assertJWTAuth will assert the basic JWT auth assertions. It will return the
+// url.Values from the request body for any additional assertions to be made.
+func assertJWTAuth(t testing.TB, r *http.Request) url.Values {
+ body, err := io.ReadAll(r.Body)
+ if !assert.NoError(t, err) {
+ return nil
+ }
+ vals, err := url.ParseQuery(string(body))
+ if !assert.NoError(t, err) {
+ return nil
+ }
+
+ assert.Equal(t, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", vals.Get("client_assertion_type"))
+ jwtToken := vals.Get("client_assertion")
+ // No need to actually verify the jwt is signed right.
+ parsedToken, _, err := (&jwt.Parser{}).ParseUnverified(jwtToken, jwt.MapClaims{})
+ if !assert.NoError(t, err, "failed to parse jwt token") {
+ return nil
+ }
+
+ // Azure requirements
+ assert.NotEmpty(t, parsedToken.Header["x5t"], "hashed cert missing")
+ assert.Equal(t, "RS256", parsedToken.Header["alg"], "azure only accepts RS256")
+ assert.Equal(t, "JWT", parsedToken.Header["typ"], "azure only accepts JWT")
+
+ return vals
+}
diff --git a/coderd/organizations.go b/coderd/organizations.go
index 13906baef63b3..3353324482fbf 100644
--- a/coderd/organizations.go
+++ b/coderd/organizations.go
@@ -9,11 +9,11 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Get organization by ID
@@ -94,7 +94,7 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
_, err = tx.InsertAllUsersGroup(ctx, organization.ID)
if err != nil {
- return xerrors.Errorf("create %q group: %w", database.AllUsersGroup, err)
+ return xerrors.Errorf("create %q group: %w", database.EveryoneGroup, err)
}
return nil
}, nil)
diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go
index 87d69fc508c61..c8cde696e22a2 100644
--- a/coderd/organizations_test.go
+++ b/coderd/organizations_test.go
@@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestOrganizationsByUser(t *testing.T) {
diff --git a/coderd/pagination.go b/coderd/pagination.go
index 8bc4228b3bcea..b50358e1f57f5 100644
--- a/coderd/pagination.go
+++ b/coderd/pagination.go
@@ -5,8 +5,8 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
// parsePagination extracts pagination query params from the http request.
diff --git a/coderd/pagination_internal_test.go b/coderd/pagination_internal_test.go
index d1e6bd107cfd8..94077b1083f4f 100644
--- a/coderd/pagination_internal_test.go
+++ b/coderd/pagination_internal_test.go
@@ -10,7 +10,7 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
func TestPagination(t *testing.T) {
diff --git a/coderd/parameter/plaintext_test.go b/coderd/parameter/plaintext_test.go
index bb11f376d31b5..78945d9984e10 100644
--- a/coderd/parameter/plaintext_test.go
+++ b/coderd/parameter/plaintext_test.go
@@ -3,7 +3,7 @@ package parameter_test
import (
"testing"
- "github.com/coder/coder/coderd/parameter"
+ "github.com/coder/coder/v2/coderd/parameter"
"github.com/stretchr/testify/require"
)
diff --git a/coderd/prometheusmetrics/aggregator.go b/coderd/prometheusmetrics/aggregator.go
index 7704f5cfae7ec..b1091b2451405 100644
--- a/coderd/prometheusmetrics/aggregator.go
+++ b/coderd/prometheusmetrics/aggregator.go
@@ -10,7 +10,7 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/codersdk/agentsdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
)
const (
diff --git a/coderd/prometheusmetrics/aggregator_test.go b/coderd/prometheusmetrics/aggregator_test.go
index a5e5b0f11eaef..45f0de14851c3 100644
--- a/coderd/prometheusmetrics/aggregator_test.go
+++ b/coderd/prometheusmetrics/aggregator_test.go
@@ -12,10 +12,10 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/prometheusmetrics"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/prometheusmetrics"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/testutil"
)
const (
diff --git a/coderd/prometheusmetrics/collector_test.go b/coderd/prometheusmetrics/collector_test.go
index 9d63f6669113d..651be04477c7c 100644
--- a/coderd/prometheusmetrics/collector_test.go
+++ b/coderd/prometheusmetrics/collector_test.go
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/prometheusmetrics"
+ "github.com/coder/coder/v2/coderd/prometheusmetrics"
)
func TestCollector_Add(t *testing.T) {
@@ -115,11 +115,11 @@ func TestCollector_Set_Add(t *testing.T) {
assert.Equal(t, 6, int(metrics[1].Gauge.GetValue())) // Metric value
}
-func collectAndSortMetrics(t *testing.T, collector prometheus.Collector, count int) []dto.Metric {
+func collectAndSortMetrics(t *testing.T, collector prometheus.Collector, count int) []*dto.Metric {
ch := make(chan prometheus.Metric, count)
defer close(ch)
- var metrics []dto.Metric
+ var metrics []*dto.Metric
collector.Collect(ch)
for i := 0; i < count; i++ {
@@ -129,7 +129,7 @@ func collectAndSortMetrics(t *testing.T, collector prometheus.Collector, count i
err := m.Write(&metric)
require.NoError(t, err)
- metrics = append(metrics, metric)
+ metrics = append(metrics, &metric)
}
// Ensure always the same order of metrics
diff --git a/coderd/prometheusmetrics/prometheusmetrics.go b/coderd/prometheusmetrics/prometheusmetrics.go
index c1f749622accc..3db7985c4818e 100644
--- a/coderd/prometheusmetrics/prometheusmetrics.go
+++ b/coderd/prometheusmetrics/prometheusmetrics.go
@@ -15,10 +15,10 @@ import (
"tailscale.com/tailcfg"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/tailnet"
)
const (
diff --git a/coderd/prometheusmetrics/prometheusmetrics_test.go b/coderd/prometheusmetrics/prometheusmetrics_test.go
index 3ea774df1186d..bf6f475ad1be6 100644
--- a/coderd/prometheusmetrics/prometheusmetrics_test.go
+++ b/coderd/prometheusmetrics/prometheusmetrics_test.go
@@ -11,6 +11,9 @@ import (
"testing"
"time"
+ "github.com/coder/coder/v2/coderd/batchstats"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
@@ -20,18 +23,18 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/prometheusmetrics"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/tailnet/tailnettest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/prometheusmetrics"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/tailnet/tailnettest"
+ "github.com/coder/coder/v2/testutil"
)
func TestActiveUsers(t *testing.T) {
@@ -265,10 +268,10 @@ func TestAgents(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -372,9 +375,29 @@ func TestAgents(t *testing.T) {
func TestAgentStats(t *testing.T) {
t.Parallel()
+ ctx, cancelFunc := context.WithCancel(context.Background())
+ t.Cleanup(cancelFunc)
+
+ db, pubsub := dbtestutil.NewDB(t)
+ log := slogtest.Make(t, nil)
+
+ batcher, closeBatcher, err := batchstats.New(ctx,
+ batchstats.WithStore(db),
+ // We want our stats, and we want them NOW.
+ batchstats.WithBatchSize(1),
+ batchstats.WithInterval(time.Hour),
+ batchstats.WithLogger(log),
+ )
+ require.NoError(t, err, "create stats batcher failed")
+ t.Cleanup(closeBatcher)
+
// Build sample workspaces with test agents and fake agent client
- client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
- db := api.Database
+ client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{
+ Database: db,
+ IncludeProvisionerDaemon: true,
+ Pubsub: pubsub,
+ StatsBatcher: batcher,
+ })
user := coderdtest.CreateFirstUser(t, client)
@@ -384,11 +407,7 @@ func TestAgentStats(t *testing.T) {
registry := prometheus.NewRegistry()
- ctx, cancelFunc := context.WithCancel(context.Background())
- defer cancelFunc()
-
// given
- var err error
var i int64
for i = 0; i < 3; i++ {
_, err = agent1.PostStats(ctx, &agentsdk.Stats{
@@ -475,7 +494,7 @@ func prepareWorkspaceAndAgent(t *testing.T, client *codersdk.Client, user coders
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go
index ad0d5b6b4c86f..695892c86c9cc 100644
--- a/coderd/provisionerdserver/provisionerdserver.go
+++ b/coderd/provisionerdserver/provisionerdserver.go
@@ -26,21 +26,21 @@ import (
protobuf "google.golang.org/protobuf/proto"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/apikey"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner"
- "github.com/coder/coder/provisionerd/proto"
- "github.com/coder/coder/provisionersdk"
- sdkproto "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/apikey"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner"
+ "github.com/coder/coder/v2/provisionerd/proto"
+ "github.com/coder/coder/v2/provisionersdk"
+ sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
)
var (
@@ -280,7 +280,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
RichParameterValues: convertRichParameterValues(workspaceBuildParameters),
VariableValues: asVariableValues(templateVariables),
GitAuthProviders: gitAuthProviders,
- Metadata: &sdkproto.Provision_Metadata{
+ Metadata: &sdkproto.Metadata{
CoderUrl: server.AccessURL.String(),
WorkspaceTransition: transition,
WorkspaceName: workspace.Name,
@@ -316,7 +316,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
TemplateDryRun: &proto.AcquiredJob_TemplateDryRun{
RichParameterValues: convertRichParameterValues(input.RichParameterValues),
VariableValues: asVariableValues(templateVariables),
- Metadata: &sdkproto.Provision_Metadata{
+ Metadata: &sdkproto.Metadata{
CoderUrl: server.AccessURL.String(),
WorkspaceName: input.WorkspaceName,
},
@@ -337,7 +337,7 @@ func (server *Server) AcquireJob(ctx context.Context, _ *proto.Empty) (*proto.Ac
protoJob.Type = &proto.AcquiredJob_TemplateImport_{
TemplateImport: &proto.AcquiredJob_TemplateImport{
UserVariableValues: convertVariableValues(userVariableValues),
- Metadata: &sdkproto.Provision_Metadata{
+ Metadata: &sdkproto.Metadata{
CoderUrl: server.AccessURL.String(),
},
},
@@ -744,8 +744,6 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
}
// CompleteJob is triggered by a provision daemon to mark a provisioner job as completed.
-//
-//nolint:gocyclo
func (server *Server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) (*proto.Empty, error) {
ctx, span := server.startTrace(ctx, tracing.FuncName())
defer span.End()
diff --git a/coderd/provisionerdserver/provisionerdserver_internal_test.go b/coderd/provisionerdserver/provisionerdserver_internal_test.go
index 9652f8e7c7f82..bd232d3d16d85 100644
--- a/coderd/provisionerdserver/provisionerdserver_internal_test.go
+++ b/coderd/provisionerdserver/provisionerdserver_internal_test.go
@@ -9,10 +9,10 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/testutil"
)
func TestObtainOIDCAccessToken(t *testing.T) {
diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go
index ee0faca6d2e84..5a317cd531530 100644
--- a/coderd/provisionerdserver/provisionerdserver_test.go
+++ b/coderd/provisionerdserver/provisionerdserver_test.go
@@ -17,21 +17,21 @@ import (
"golang.org/x/oauth2"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/provisionerdserver"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisionerd/proto"
- "github.com/coder/coder/provisionersdk"
- sdkproto "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/provisionerdserver"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisionerd/proto"
+ "github.com/coder/coder/v2/provisionersdk"
+ sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func mockAuditor() *atomic.Pointer[audit.Auditor] {
@@ -267,7 +267,7 @@ func TestAcquireJob(t *testing.T) {
Id: gitAuthProvider,
AccessToken: "access_token",
}},
- Metadata: &sdkproto.Provision_Metadata{
+ Metadata: &sdkproto.Metadata{
CoderUrl: srv.AccessURL.String(),
WorkspaceTransition: sdkproto.WorkspaceTransition_START,
WorkspaceName: workspace.Name,
@@ -359,7 +359,7 @@ func TestAcquireJob(t *testing.T) {
want, err := json.Marshal(&proto.AcquiredJob_TemplateDryRun_{
TemplateDryRun: &proto.AcquiredJob_TemplateDryRun{
- Metadata: &sdkproto.Provision_Metadata{
+ Metadata: &sdkproto.Metadata{
CoderUrl: srv.AccessURL.String(),
WorkspaceName: "testing",
},
@@ -391,7 +391,7 @@ func TestAcquireJob(t *testing.T) {
want, err := json.Marshal(&proto.AcquiredJob_TemplateImport_{
TemplateImport: &proto.AcquiredJob_TemplateImport{
- Metadata: &sdkproto.Provision_Metadata{
+ Metadata: &sdkproto.Metadata{
CoderUrl: srv.AccessURL.String(),
},
},
@@ -434,7 +434,7 @@ func TestAcquireJob(t *testing.T) {
UserVariableValues: []*sdkproto.VariableValue{
{Name: "first", Sensitive: true, Value: "first_value"},
},
- Metadata: &sdkproto.Provision_Metadata{
+ Metadata: &sdkproto.Metadata{
CoderUrl: srv.AccessURL.String(),
},
},
diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go
index 1be1a56518d28..4b49c385c80f4 100644
--- a/coderd/provisionerjobs.go
+++ b/coderd/provisionerjobs.go
@@ -16,13 +16,13 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisionersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisionersdk"
)
// Returns provisioner logs based on query parameters.
@@ -402,7 +402,7 @@ func (f *logFollower) follow() {
if f.ctx.Err() == nil && !xerrors.Is(err, io.EOF) {
// neither context expiry, nor EOF, close and log
f.logger.Error(f.ctx, "failed to query logs", slog.Error(err))
- err = f.conn.Close(websocket.StatusInternalError, err.Error())
+ err = f.conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("%s", err.Error()))
if err != nil {
f.logger.Warn(f.ctx, "failed to close webscoket", slog.Error(err))
}
diff --git a/coderd/provisionerjobs_internal_test.go b/coderd/provisionerjobs_internal_test.go
index fd05eb3e62219..31ae0fb608ac5 100644
--- a/coderd/provisionerjobs_internal_test.go
+++ b/coderd/provisionerjobs_internal_test.go
@@ -18,12 +18,12 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbmock"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisionersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbmock"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestConvertProvisionerJob_Unit(t *testing.T) {
diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go
index 505031d50c949..5d1715a9fe52d 100644
--- a/coderd/provisionerjobs_test.go
+++ b/coderd/provisionerjobs_test.go
@@ -6,10 +6,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func TestProvisionerJobLogs(t *testing.T) {
@@ -20,16 +20,16 @@ func TestProvisionerJobLogs(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "log-output",
},
},
}, {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
},
}},
})
@@ -59,16 +59,16 @@ func TestProvisionerJobLogs(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "log-output",
},
},
}, {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
},
}},
})
diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go
index 51b71302525e7..42625b07c3e0a 100644
--- a/coderd/rbac/authz.go
+++ b/coderd/rbac/authz.go
@@ -18,10 +18,10 @@ import (
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/rbac/regosql"
- "github.com/coder/coder/coderd/rbac/regosql/sqltypes"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/rbac/regosql"
+ "github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/util/slice"
)
// Action represents the allowed actions to be done on an object.
diff --git a/coderd/rbac/authz_internal_test.go b/coderd/rbac/authz_internal_test.go
index 6211aafd44677..e264e31c73a8c 100644
--- a/coderd/rbac/authz_internal_test.go
+++ b/coderd/rbac/authz_internal_test.go
@@ -13,8 +13,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/rbac/regosql"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/rbac/regosql"
+ "github.com/coder/coder/v2/testutil"
)
type fakeObject struct {
diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go
index e13df4176abcd..05f402abe4507 100644
--- a/coderd/rbac/authz_test.go
+++ b/coderd/rbac/authz_test.go
@@ -9,8 +9,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/rbac"
)
type benchmarkCase struct {
diff --git a/coderd/rbac/error_test.go b/coderd/rbac/error_test.go
index 23bbc7b3bc54c..cd9d319dabba8 100644
--- a/coderd/rbac/error_test.go
+++ b/coderd/rbac/error_test.go
@@ -3,7 +3,7 @@ package rbac_test
import (
"testing"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go
index 39f57c7fcc6da..1e3f1f45e59ea 100644
--- a/coderd/rbac/object.go
+++ b/coderd/rbac/object.go
@@ -37,10 +37,10 @@ var (
Type: "workspace_build",
}
- // ResourceWorkspaceLocked is returned if a workspace is locked.
+ // ResourceWorkspaceDormant is returned if a workspace is dormant.
// It grants restricted permissions on workspace builds.
- ResourceWorkspaceLocked = Object{
- Type: "workspace_locked",
+ ResourceWorkspaceDormant = Object{
+ Type: "workspace_dormant",
}
// ResourceWorkspaceProxy CRUD. Org
diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go
index 10506b3f719c2..86a03d4552d45 100644
--- a/coderd/rbac/object_gen.go
+++ b/coderd/rbac/object_gen.go
@@ -26,8 +26,8 @@ func AllResources() []Object {
ResourceWorkspace,
ResourceWorkspaceApplicationConnect,
ResourceWorkspaceBuild,
+ ResourceWorkspaceDormant,
ResourceWorkspaceExecution,
- ResourceWorkspaceLocked,
ResourceWorkspaceProxy,
}
}
diff --git a/coderd/rbac/object_test.go b/coderd/rbac/object_test.go
index cbd043c753983..505f12b8cc7b0 100644
--- a/coderd/rbac/object_test.go
+++ b/coderd/rbac/object_test.go
@@ -3,8 +3,8 @@ package rbac_test
import (
"testing"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/util/slice"
)
func TestObjectEqual(t *testing.T) {
diff --git a/coderd/rbac/regosql/acl_group_var.go b/coderd/rbac/regosql/acl_group_var.go
index d695683a72d61..328dfbcd48d0a 100644
--- a/coderd/rbac/regosql/acl_group_var.go
+++ b/coderd/rbac/regosql/acl_group_var.go
@@ -7,7 +7,7 @@ import (
"github.com/open-policy-agent/opa/ast"
- "github.com/coder/coder/coderd/rbac/regosql/sqltypes"
+ "github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
)
var (
diff --git a/coderd/rbac/regosql/compile.go b/coderd/rbac/regosql/compile.go
index 398cbbf54c08b..69ef2a018f36c 100644
--- a/coderd/rbac/regosql/compile.go
+++ b/coderd/rbac/regosql/compile.go
@@ -8,7 +8,7 @@ import (
"github.com/open-policy-agent/opa/rego"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/rbac/regosql/sqltypes"
+ "github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
)
// ConvertConfig is required to generate SQL from the rego queries.
diff --git a/coderd/rbac/regosql/compile_test.go b/coderd/rbac/regosql/compile_test.go
index 5673b8621c2c7..be0385bf83699 100644
--- a/coderd/rbac/regosql/compile_test.go
+++ b/coderd/rbac/regosql/compile_test.go
@@ -7,8 +7,8 @@ import (
"github.com/open-policy-agent/opa/rego"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/rbac/regosql"
- "github.com/coder/coder/coderd/rbac/regosql/sqltypes"
+ "github.com/coder/coder/v2/coderd/rbac/regosql"
+ "github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
)
// TestRegoQueriesNoVariables handles cases without variables. These should be
diff --git a/coderd/rbac/regosql/configs.go b/coderd/rbac/regosql/configs.go
index b683a11af3123..68d3b6264cb3b 100644
--- a/coderd/rbac/regosql/configs.go
+++ b/coderd/rbac/regosql/configs.go
@@ -1,6 +1,6 @@
package regosql
-import "github.com/coder/coder/coderd/rbac/regosql/sqltypes"
+import "github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
func resourceIDMatcher() sqltypes.VariableMatcher {
return sqltypes.StringVarMatcher("id :: text", []string{"input", "object", "id"})
diff --git a/coderd/rbac/regosql/sqltypes/equality_test.go b/coderd/rbac/regosql/sqltypes/equality_test.go
index 8764508ad858a..17a3d7f45eed1 100644
--- a/coderd/rbac/regosql/sqltypes/equality_test.go
+++ b/coderd/rbac/regosql/sqltypes/equality_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/rbac/regosql/sqltypes"
+ "github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
)
func TestEquality(t *testing.T) {
diff --git a/coderd/rbac/regosql/sqltypes/member_test.go b/coderd/rbac/regosql/sqltypes/member_test.go
index 91259e286ee1c..0fedcc176c49f 100644
--- a/coderd/rbac/regosql/sqltypes/member_test.go
+++ b/coderd/rbac/regosql/sqltypes/member_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/rbac/regosql/sqltypes"
+ "github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
)
func TestMembership(t *testing.T) {
diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go
index 93aeaca017592..4f159480a0491 100644
--- a/coderd/rbac/roles.go
+++ b/coderd/rbac/roles.go
@@ -121,7 +121,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
opts = &RoleOptions{}
}
- ownerAndAdminExceptions := []Object{ResourceWorkspaceLocked}
+ ownerAndAdminExceptions := []Object{ResourceWorkspaceDormant}
if opts.NoOwnerWorkspaceExec {
ownerAndAdminExceptions = append(ownerAndAdminExceptions,
ResourceWorkspaceExecution,
@@ -150,7 +150,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceProvisionerDaemon.Type: {ActionRead},
}),
Org: map[string][]Permission{},
- User: append(allPermsExcept(ResourceWorkspaceLocked, ResourceUser, ResourceOrganizationMember),
+ User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember),
Permissions(map[string][]Action{
// Users cannot do create/update/delete on themselves, but they
// can read their own details.
@@ -246,7 +246,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Site: []Permission{},
Org: map[string][]Permission{
// Org admins should not have workspace exec perms.
- organizationID: allPermsExcept(ResourceWorkspaceExecution, ResourceWorkspaceLocked),
+ organizationID: allPermsExcept(ResourceWorkspaceExecution, ResourceWorkspaceDormant),
},
User: []Permission{},
}
diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go
index 4ecb53e1832d2..fc47413fd19f2 100644
--- a/coderd/rbac/roles_test.go
+++ b/coderd/rbac/roles_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac"
)
type authSubject struct {
@@ -319,9 +319,9 @@ func TestRolePermissions(t *testing.T) {
},
},
{
- Name: "WorkspaceLocked",
+ Name: "WorkspaceDormant",
Actions: rbac.AllActions(),
- Resource: rbac.ResourceWorkspaceLocked.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
+ Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
AuthorizeMap: map[bool][]authSubject{
true: {},
false: {memberMe, orgAdmin, userAdmin, otherOrgAdmin, otherOrgMember, orgMemberMe, owner, templateAdmin},
diff --git a/coderd/rbac/subject_test.go b/coderd/rbac/subject_test.go
index 294200aff8f8c..330ad7403797b 100644
--- a/coderd/rbac/subject_test.go
+++ b/coderd/rbac/subject_test.go
@@ -3,7 +3,7 @@ package rbac_test
import (
"testing"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/rbac"
)
func TestSubjectEqual(t *testing.T) {
diff --git a/coderd/roles.go b/coderd/roles.go
index 177a5301eae00..bbee06d6927dd 100644
--- a/coderd/roles.go
+++ b/coderd/roles.go
@@ -3,11 +3,11 @@ package coderd
import (
"net/http"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
)
// assignableSiteRoles returns all site wide roles that can be assigned.
diff --git a/coderd/roles_test.go b/coderd/roles_test.go
index 2b9eb35f34e15..275edc25bfcd2 100644
--- a/coderd/roles_test.go
+++ b/coderd/roles_test.go
@@ -7,10 +7,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestListRoles(t *testing.T) {
diff --git a/coderd/schedule/autostop.go b/coderd/schedule/autostop.go
index 6934640045506..6fe3d5848db02 100644
--- a/coderd/schedule/autostop.go
+++ b/coderd/schedule/autostop.go
@@ -4,9 +4,12 @@ import (
"context"
"time"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/tracing"
)
const (
@@ -72,6 +75,13 @@ type AutostopTime struct {
// Deadline is a cost saving measure, while max deadline is a
// compliance/updating measure.
func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (AutostopTime, error) {
+ ctx, span := tracing.StartSpan(ctx,
+ trace.WithAttributes(attribute.String("coder.workspace_id", params.Workspace.ID.String())),
+ trace.WithAttributes(attribute.String("coder.template_id", params.Workspace.TemplateID.String())),
+ )
+ defer span.End()
+ defer span.End()
+
var (
db = params.Database
workspace = params.Workspace
diff --git a/coderd/schedule/autostop_test.go b/coderd/schedule/autostop_test.go
index 6be5c5eaf81d4..8a93c819698b4 100644
--- a/coderd/schedule/autostop_test.go
+++ b/coderd/schedule/autostop_test.go
@@ -11,11 +11,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/testutil"
)
func TestCalculateAutoStop(t *testing.T) {
diff --git a/coderd/schedule/cron_test.go b/coderd/schedule/cron_test.go
index d09feb5578b20..b8ab1600ef2c5 100644
--- a/coderd/schedule/cron_test.go
+++ b/coderd/schedule/cron_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/schedule"
)
func Test_Weekly(t *testing.T) {
diff --git a/coderd/schedule/mock.go b/coderd/schedule/mock.go
index 4a22197b57dc4..1fe33bb549e81 100644
--- a/coderd/schedule/mock.go
+++ b/coderd/schedule/mock.go
@@ -5,7 +5,7 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
)
type MockTemplateScheduleStore struct {
diff --git a/coderd/schedule/template.go b/coderd/schedule/template.go
index de97dffc9ac2a..9c1b2fa5aa787 100644
--- a/coderd/schedule/template.go
+++ b/coderd/schedule/template.go
@@ -7,7 +7,8 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/tracing"
)
const MaxTemplateRestartRequirementWeeks = 16
@@ -98,12 +99,24 @@ type TemplateScheduleOptions struct {
// FailureTTL dictates the duration after which failed workspaces will be
// stopped automatically.
FailureTTL time.Duration `json:"failure_ttl"`
- // InactivityTTL dictates the duration after which inactive workspaces will
- // be locked.
- InactivityTTL time.Duration `json:"inactivity_ttl"`
- // LockedTTL dictates the duration after which locked workspaces will be
+ // TimeTilDormant dictates the duration after which inactive workspaces will
+ // go dormant.
+ TimeTilDormant time.Duration `json:"time_til_dormant"`
+ // TimeTilDormantAutoDelete dictates the duration after which dormant workspaces will be
// permanently deleted.
- LockedTTL time.Duration `json:"locked_ttl"`
+ TimeTilDormantAutoDelete time.Duration `json:"time_til_dormant_autodelete"`
+ // UpdateWorkspaceLastUsedAt updates the template's workspaces'
+ // last_used_at field. This is useful for preventing updates to the
+ // templates inactivity_ttl immediately triggering a dormant action against
+ // workspaces whose last_used_at field violates the new template
+ // inactivity_ttl threshold.
+ UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"`
+ // UpdateWorkspaceDormantAt updates the template's workspaces'
+ // dormant_at field. This is useful for preventing updates to the
+ // templates locked_ttl immediately triggering a delete action against
+ // workspaces whose dormant_at field violates the new template time_til_dormant_autodelete
+ // threshold.
+ UpdateWorkspaceDormantAt bool `json:"update_workspace_dormant_at"`
}
// TemplateScheduleStore provides an interface for retrieving template
@@ -122,6 +135,9 @@ func NewAGPLTemplateScheduleStore() TemplateScheduleStore {
}
func (*agplTemplateScheduleStore) Get(ctx context.Context, db database.Store, templateID uuid.UUID) (TemplateScheduleOptions, error) {
+ ctx, span := tracing.StartSpan(ctx)
+ defer span.End()
+
tpl, err := db.GetTemplateByID(ctx, templateID)
if err != nil {
return TemplateScheduleOptions{}, err
@@ -134,20 +150,23 @@ func (*agplTemplateScheduleStore) Get(ctx context.Context, db database.Store, te
UserAutostopEnabled: true,
DefaultTTL: time.Duration(tpl.DefaultTTL),
// Disregard the values in the database, since RestartRequirement,
- // FailureTTL, InactivityTTL, and LockedTTL are enterprise features.
+ // FailureTTL, TimeTilDormant, and TimeTilDormantAutoDelete are enterprise features.
UseRestartRequirement: false,
MaxTTL: 0,
RestartRequirement: TemplateRestartRequirement{
DaysOfWeek: 0,
Weeks: 0,
},
- FailureTTL: 0,
- InactivityTTL: 0,
- LockedTTL: 0,
+ FailureTTL: 0,
+ TimeTilDormant: 0,
+ TimeTilDormantAutoDelete: 0,
}, nil
}
func (*agplTemplateScheduleStore) Set(ctx context.Context, db database.Store, tpl database.Template, opts TemplateScheduleOptions) (database.Template, error) {
+ ctx, span := tracing.StartSpan(ctx)
+ defer span.End()
+
if int64(opts.DefaultTTL) == tpl.DefaultTTL {
// Avoid updating the UpdatedAt timestamp if nothing will be changed.
return tpl, nil
@@ -167,8 +186,8 @@ func (*agplTemplateScheduleStore) Set(ctx context.Context, db database.Store, tp
AllowUserAutostart: tpl.AllowUserAutostart,
AllowUserAutostop: tpl.AllowUserAutostop,
FailureTTL: tpl.FailureTTL,
- InactivityTTL: tpl.InactivityTTL,
- LockedTTL: tpl.LockedTTL,
+ TimeTilDormant: tpl.TimeTilDormant,
+ TimeTilDormantAutoDelete: tpl.TimeTilDormantAutoDelete,
})
if err != nil {
return xerrors.Errorf("update template schedule: %w", err)
diff --git a/coderd/schedule/user.go b/coderd/schedule/user.go
index 967a430fcccd2..18df0b40e7a37 100644
--- a/coderd/schedule/user.go
+++ b/coderd/schedule/user.go
@@ -5,7 +5,7 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
)
type UserQuietHoursScheduleOptions struct {
diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go
index 9b216d0180e15..efdde1bb1d2e7 100644
--- a/coderd/searchquery/search.go
+++ b/coderd/searchquery/search.go
@@ -10,10 +10,10 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
)
func AuditLogs(query string) (database.GetAuditLogsOffsetParams, []codersdk.ValidationError) {
@@ -114,9 +114,17 @@ func Workspaces(query string, page codersdk.Pagination, agentInactiveDisconnectT
filter.Name = parser.String(values, "", "name")
filter.Status = string(httpapi.ParseCustom(parser, values, "", "status", httpapi.ParseEnum[database.WorkspaceStatus]))
filter.HasAgent = parser.String(values, "", "has-agent")
+ filter.DormantAt = parser.Time(values, time.Time{}, "dormant_at", "2006-01-02")
+ filter.LastUsedAfter = parser.Time3339Nano(values, time.Time{}, "last_used_after")
+ filter.LastUsedBefore = parser.Time3339Nano(values, time.Time{}, "last_used_before")
if _, ok := values["deleting_by"]; ok {
postFilter.DeletingBy = ptr.Ref(parser.Time(values, time.Time{}, "deleting_by", "2006-01-02"))
+ // We want to make sure to grab dormant workspaces since they
+ // are omitted by default.
+ if filter.DormantAt.IsZero() {
+ filter.DormantAt = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
+ }
}
parser.ErrorExcessParams(values)
diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go
index 4a7f61331a5f2..929a17b169643 100644
--- a/coderd/searchquery/search_test.go
+++ b/coderd/searchquery/search_test.go
@@ -9,11 +9,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/searchquery"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/searchquery"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
)
func TestSearchWorkspace(t *testing.T) {
@@ -142,7 +142,7 @@ func TestSearchWorkspace(t *testing.T) {
{
Name: "ExtraKeys",
Query: `foo:bar`,
- ExpectedErrorContains: `Query param "foo" is not a valid query param`,
+ ExpectedErrorContains: `"foo" is not a valid query param`,
},
}
@@ -239,7 +239,7 @@ func TestSearchAudit(t *testing.T) {
{
Name: "ExtraKeys",
Query: `foo:bar`,
- ExpectedErrorContains: `Query param "foo" is not a valid query param`,
+ ExpectedErrorContains: `"foo" is not a valid query param`,
},
{
Name: "Dates",
@@ -370,7 +370,7 @@ func TestSearchUsers(t *testing.T) {
{
Name: "ExtraKeys",
Query: `foo:bar`,
- ExpectedErrorContains: `Query param "foo" is not a valid query param`,
+ ExpectedErrorContains: `"foo" is not a valid query param`,
},
}
diff --git a/coderd/tailnet.go b/coderd/tailnet.go
index c37f583da252e..ca2a86d27f71e 100644
--- a/coderd/tailnet.go
+++ b/coderd/tailnet.go
@@ -14,15 +14,17 @@ import (
"time"
"github.com/google/uuid"
+ "go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"tailscale.com/derp"
"tailscale.com/tailcfg"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/wsconncache"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/site"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/wsconncache"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/site"
+ "github.com/coder/coder/v2/tailnet"
"github.com/coder/retry"
)
@@ -42,31 +44,59 @@ func NewServerTailnet(
ctx context.Context,
logger slog.Logger,
derpServer *derp.Server,
- derpMap *tailcfg.DERPMap,
+ derpMapFn func() *tailcfg.DERPMap,
+ derpForceWebSockets bool,
getMultiAgent func(context.Context) (tailnet.MultiAgentConn, error),
cache *wsconncache.Cache,
+ traceProvider trace.TracerProvider,
) (*ServerTailnet, error) {
logger = logger.Named("servertailnet")
+ originalDerpMap := derpMapFn()
conn, err := tailnet.NewConn(&tailnet.Options{
- Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
- DERPMap: derpMap,
- Logger: logger,
+ Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
+ DERPMap: originalDerpMap,
+ DERPForceWebSockets: derpForceWebSockets,
+ Logger: logger,
})
if err != nil {
return nil, xerrors.Errorf("create tailnet conn: %w", err)
}
serverCtx, cancel := context.WithCancel(ctx)
+ derpMapUpdaterClosed := make(chan struct{})
+ go func() {
+ defer close(derpMapUpdaterClosed)
+
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-serverCtx.Done():
+ return
+ case <-ticker.C:
+ }
+
+ newDerpMap := derpMapFn()
+ if !tailnet.CompareDERPMaps(originalDerpMap, newDerpMap) {
+ conn.SetDERPMap(newDerpMap)
+ originalDerpMap = newDerpMap
+ }
+ }
+ }()
+
tn := &ServerTailnet{
- ctx: serverCtx,
- cancel: cancel,
- logger: logger,
- conn: conn,
- getMultiAgent: getMultiAgent,
- cache: cache,
- agentNodes: map[uuid.UUID]time.Time{},
- agentTickets: map[uuid.UUID]map[uuid.UUID]struct{}{},
- transport: tailnetTransport.Clone(),
+ ctx: serverCtx,
+ cancel: cancel,
+ derpMapUpdaterClosed: derpMapUpdaterClosed,
+ logger: logger,
+ tracer: traceProvider.Tracer(tracing.TracerName),
+ conn: conn,
+ getMultiAgent: getMultiAgent,
+ cache: cache,
+ agentConnectionTimes: map[uuid.UUID]time.Time{},
+ agentTickets: map[uuid.UUID]map[uuid.UUID]struct{}{},
+ transport: tailnetTransport.Clone(),
}
tn.transport.DialContext = tn.dialContext
tn.transport.MaxIdleConnsPerHost = 10
@@ -139,25 +169,50 @@ func (s *ServerTailnet) expireOldAgents() {
case <-ticker.C:
}
- s.nodesMu.Lock()
- agentConn := s.getAgentConn()
- for agentID, lastConnection := range s.agentNodes {
- // If no one has connected since the cutoff and there are no active
- // connections, remove the agent.
- if time.Since(lastConnection) > cutoff && len(s.agentTickets[agentID]) == 0 {
- _ = agentConn
- // err := agentConn.UnsubscribeAgent(agentID)
- // if err != nil {
- // s.logger.Error(s.ctx, "unsubscribe expired agent", slog.Error(err), slog.F("agent_id", agentID))
- // }
- // delete(s.agentNodes, agentID)
-
- // TODO(coadler): actually remove from the netmap, then reenable
- // the above
+ s.doExpireOldAgents(cutoff)
+ }
+}
+
+func (s *ServerTailnet) doExpireOldAgents(cutoff time.Duration) {
+ // TODO: add some attrs to this.
+ ctx, span := s.tracer.Start(s.ctx, tracing.FuncName())
+ defer span.End()
+
+ start := time.Now()
+ deletedCount := 0
+
+ s.nodesMu.Lock()
+ s.logger.Debug(ctx, "pruning inactive agents", slog.F("agent_count", len(s.agentConnectionTimes)))
+ agentConn := s.getAgentConn()
+ for agentID, lastConnection := range s.agentConnectionTimes {
+ // If no one has connected since the cutoff and there are no active
+ // connections, remove the agent.
+ if time.Since(lastConnection) > cutoff && len(s.agentTickets[agentID]) == 0 {
+ deleted, err := s.conn.RemovePeer(tailnet.PeerSelector{
+ ID: tailnet.NodeID(agentID),
+ IP: netip.PrefixFrom(tailnet.IPFromUUID(agentID), 128),
+ })
+ if err != nil {
+ s.logger.Warn(ctx, "failed to remove peer from server tailnet", slog.Error(err))
+ continue
+ }
+ if !deleted {
+ s.logger.Warn(ctx, "peer didn't exist in tailnet", slog.Error(err))
+ }
+
+ deletedCount++
+ delete(s.agentConnectionTimes, agentID)
+ err = agentConn.UnsubscribeAgent(agentID)
+ if err != nil {
+ s.logger.Error(ctx, "unsubscribe expired agent", slog.Error(err), slog.F("agent_id", agentID))
}
}
- s.nodesMu.Unlock()
}
+ s.nodesMu.Unlock()
+ s.logger.Debug(s.ctx, "successfully pruned inactive agents",
+ slog.F("deleted", deletedCount),
+ slog.F("took", time.Since(start)),
+ )
}
func (s *ServerTailnet) watchAgentUpdates() {
@@ -196,7 +251,7 @@ func (s *ServerTailnet) reinitCoordinator() {
s.agentConn.Store(&agentConn)
// Resubscribe to all of the agents we're tracking.
- for agentID := range s.agentNodes {
+ for agentID := range s.agentConnectionTimes {
err := agentConn.SubscribeAgent(agentID)
if err != nil {
s.logger.Warn(s.ctx, "resubscribe to agent", slog.Error(err), slog.F("agent_id", agentID))
@@ -208,18 +263,21 @@ func (s *ServerTailnet) reinitCoordinator() {
}
type ServerTailnet struct {
- ctx context.Context
- cancel func()
+ ctx context.Context
+ cancel func()
+ derpMapUpdaterClosed chan struct{}
logger slog.Logger
+ tracer trace.Tracer
conn *tailnet.Conn
getMultiAgent func(context.Context) (tailnet.MultiAgentConn, error)
agentConn atomic.Pointer[tailnet.MultiAgentConn]
cache *wsconncache.Cache
nodesMu sync.Mutex
- // agentNodes is a map of agent tailnetNodes the server wants to keep a
- // connection to. It contains the last time the agent was connected to.
- agentNodes map[uuid.UUID]time.Time
+ // agentConnectionTimes is a map of agent tailnetNodes the server wants to
+ // keep a connection to. It contains the last time the agent was connected
+ // to.
+ agentConnectionTimes map[uuid.UUID]time.Time
// agentTockets holds a map of all open connections to an agent.
agentTickets map[uuid.UUID]map[uuid.UUID]struct{}
@@ -268,7 +326,7 @@ func (s *ServerTailnet) ensureAgent(agentID uuid.UUID) error {
s.nodesMu.Lock()
defer s.nodesMu.Unlock()
- _, ok := s.agentNodes[agentID]
+ _, ok := s.agentConnectionTimes[agentID]
// If we don't have the node, subscribe.
if !ok {
s.logger.Debug(s.ctx, "subscribing to agent", slog.F("agent_id", agentID))
@@ -279,14 +337,27 @@ func (s *ServerTailnet) ensureAgent(agentID uuid.UUID) error {
s.agentTickets[agentID] = map[uuid.UUID]struct{}{}
}
- s.agentNodes[agentID] = time.Now()
+ s.agentConnectionTimes[agentID] = time.Now()
return nil
}
+func (s *ServerTailnet) acquireTicket(agentID uuid.UUID) (release func()) {
+ id := uuid.New()
+ s.nodesMu.Lock()
+ s.agentTickets[agentID][id] = struct{}{}
+ s.nodesMu.Unlock()
+
+ return func() {
+ s.nodesMu.Lock()
+ delete(s.agentTickets[agentID], id)
+ s.nodesMu.Unlock()
+ }
+}
+
func (s *ServerTailnet) AgentConn(ctx context.Context, agentID uuid.UUID) (*codersdk.WorkspaceAgentConn, func(), error) {
var (
conn *codersdk.WorkspaceAgentConn
- ret = func() {}
+ ret func()
)
if s.getAgentConn().AgentIsLegacy(agentID) {
@@ -299,12 +370,13 @@ func (s *ServerTailnet) AgentConn(ctx context.Context, agentID uuid.UUID) (*code
conn = cconn.WorkspaceAgentConn
ret = release
} else {
+ s.logger.Debug(s.ctx, "acquiring agent", slog.F("agent_id", agentID))
err := s.ensureAgent(agentID)
if err != nil {
return nil, nil, xerrors.Errorf("ensure agent: %w", err)
}
+ ret = s.acquireTicket(agentID)
- s.logger.Debug(s.ctx, "acquiring agent", slog.F("agent_id", agentID))
conn = codersdk.NewWorkspaceAgentConn(s.conn, codersdk.WorkspaceAgentConnOptions{
AgentID: agentID,
CloseFunc: func() error { return codersdk.ErrSkipClose },
@@ -317,7 +389,6 @@ func (s *ServerTailnet) AgentConn(ctx context.Context, agentID uuid.UUID) (*code
reachable := conn.AwaitReachable(ctx)
if !reachable {
ret()
- conn.Close()
return nil, nil, xerrors.New("agent is unreachable")
}
@@ -336,13 +407,11 @@ func (s *ServerTailnet) DialAgentNetConn(ctx context.Context, agentID uuid.UUID,
nc, err := conn.DialContext(ctx, network, addr)
if err != nil {
release()
- conn.Close()
return nil, xerrors.Errorf("dial context: %w", err)
}
return &netConnCloser{Conn: nc, close: func() {
release()
- conn.Close()
}}, err
}
@@ -361,5 +430,6 @@ func (s *ServerTailnet) Close() error {
_ = s.cache.Close()
_ = s.conn.Close()
s.transport.CloseIdleConnections()
+ <-s.derpMapUpdaterClosed
return nil
}
diff --git a/coderd/tailnet_test.go b/coderd/tailnet_test.go
index 05adbcc4fb597..2a0b0dfdbae70 100644
--- a/coderd/tailnet_test.go
+++ b/coderd/tailnet_test.go
@@ -14,18 +14,20 @@ import (
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "go.opentelemetry.io/otel/trace"
+ "tailscale.com/tailcfg"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/agent/agenttest"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/wsconncache"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/tailnet/tailnettest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/agent/agenttest"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/wsconncache"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/tailnet/tailnettest"
+ "github.com/coder/coder/v2/testutil"
)
func TestServerTailnet_AgentConn_OK(t *testing.T) {
@@ -229,9 +231,11 @@ func setupAgent(t *testing.T, agentAddresses []netip.Prefix) (uuid.UUID, agent.A
context.Background(),
logger,
derpServer,
- manifest.DERPMap,
+ func() *tailcfg.DERPMap { return manifest.DERPMap },
+ false,
func(context.Context) (tailnet.MultiAgentConn, error) { return coord.ServeMultiAgent(uuid.New()), nil },
cache,
+ trace.NewNoopTracerProvider(),
)
require.NoError(t, err)
diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go
index ced52a58fd273..f190599aa3dd6 100644
--- a/coderd/telemetry/telemetry.go
+++ b/coderd/telemetry/telemetry.go
@@ -22,9 +22,8 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
-
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/database"
)
const (
@@ -460,6 +459,17 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
}
return nil
})
+ eg.Go(func() error {
+ proxies, err := r.options.Database.GetWorkspaceProxies(ctx)
+ if err != nil {
+ return xerrors.Errorf("get workspace proxies: %w", err)
+ }
+ snapshot.WorkspaceProxies = make([]WorkspaceProxy, 0, len(proxies))
+ for _, proxy := range proxies {
+ snapshot.WorkspaceProxies = append(snapshot.WorkspaceProxies, ConvertWorkspaceProxy(proxy))
+ }
+ return nil
+ })
err := eg.Wait()
if err != nil {
@@ -534,6 +544,11 @@ func ConvertProvisionerJob(job database.ProvisionerJob) ProvisionerJob {
// ConvertWorkspaceAgent anonymizes a workspace agent.
func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent {
+ subsystems := []string{}
+ for _, subsystem := range agent.Subsystems {
+ subsystems = append(subsystems, string(subsystem))
+ }
+
snapAgent := WorkspaceAgent{
ID: agent.ID,
CreatedAt: agent.CreatedAt,
@@ -546,7 +561,7 @@ func ConvertWorkspaceAgent(agent database.WorkspaceAgent) WorkspaceAgent {
Directory: agent.Directory != "",
ConnectionTimeoutSeconds: agent.ConnectionTimeoutSeconds,
ShutdownScript: agent.ShutdownScript.Valid,
- Subsystem: string(agent.Subsystem),
+ Subsystems: subsystems,
}
if agent.FirstConnectedAt.Valid {
snapAgent.FirstConnectedAt = &agent.FirstConnectedAt.Time
@@ -665,6 +680,19 @@ func ConvertLicense(license database.License) License {
}
}
+// ConvertWorkspaceProxy anonymizes a workspace proxy.
+func ConvertWorkspaceProxy(proxy database.WorkspaceProxy) WorkspaceProxy {
+ return WorkspaceProxy{
+ ID: proxy.ID,
+ Name: proxy.Name,
+ DisplayName: proxy.DisplayName,
+ DerpEnabled: proxy.DerpEnabled,
+ DerpOnly: proxy.DerpOnly,
+ CreatedAt: proxy.CreatedAt,
+ UpdatedAt: proxy.UpdatedAt,
+ }
+}
+
// Snapshot represents a point-in-time anonymized database dump.
// Data is aggregated by latest on the server-side, so partial data
// can be sent without issue.
@@ -684,6 +712,7 @@ type Snapshot struct {
WorkspaceBuilds []WorkspaceBuild `json:"workspace_build"`
WorkspaceResources []WorkspaceResource `json:"workspace_resources"`
WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"`
+ WorkspaceProxies []WorkspaceProxy `json:"workspace_proxies"`
CLIInvocations []CLIInvocation `json:"cli_invocations"`
}
@@ -768,7 +797,7 @@ type WorkspaceAgent struct {
DisconnectedAt *time.Time `json:"disconnected_at"`
ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"`
ShutdownScript bool `json:"shutdown_script"`
- Subsystem string `json:"subsystem"`
+ Subsystems []string `json:"subsystems"`
}
type WorkspaceAgentStat struct {
@@ -872,6 +901,18 @@ type CLIInvocation struct {
InvokedAt time.Time `json:"invoked_at"`
}
+type WorkspaceProxy struct {
+ ID uuid.UUID `json:"id"`
+ Name string `json:"name"`
+ DisplayName string `json:"display_name"`
+ // No URLs since we don't send deployment URL.
+ DerpEnabled bool `json:"derp_enabled"`
+ DerpOnly bool `json:"derp_only"`
+ // No Status since it may contain sensitive information.
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
type noopReporter struct{}
func (*noopReporter) Report(_ *Snapshot) {}
diff --git a/coderd/telemetry/telemetry_test.go b/coderd/telemetry/telemetry_test.go
index 93e1a5295475b..670df9ab43ab0 100644
--- a/coderd/telemetry/telemetry_test.go
+++ b/coderd/telemetry/telemetry_test.go
@@ -8,7 +8,7 @@ import (
"testing"
"time"
- "github.com/go-chi/chi"
+ "github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -16,12 +16,12 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
@@ -54,15 +54,16 @@ func TestTelemetry(t *testing.T) {
SharingLevel: database.AppSharingLevelOwner,
Health: database.WorkspaceAppHealthDisabled,
})
- wsagent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
- Subsystem: database.WorkspaceAgentSubsystemEnvbox,
- })
+ wsagent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{})
// Update the workspace agent to have a valid subsystem.
err = db.UpdateWorkspaceAgentStartupByID(ctx, database.UpdateWorkspaceAgentStartupByIDParams{
ID: wsagent.ID,
Version: wsagent.Version,
ExpandedDirectory: wsagent.ExpandedDirectory,
- Subsystem: database.WorkspaceAgentSubsystemEnvbox,
+ Subsystems: []database.WorkspaceAgentSubsystem{
+ database.WorkspaceAgentSubsystemEnvbox,
+ database.WorkspaceAgentSubsystemExectrace,
+ },
})
require.NoError(t, err)
@@ -81,6 +82,8 @@ func TestTelemetry(t *testing.T) {
UUID: uuid.New(),
})
assert.NoError(t, err)
+ _, _ = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{})
+
_, snapshot := collectSnapshot(t, db)
require.Len(t, snapshot.ProvisionerJobs, 1)
require.Len(t, snapshot.Licenses, 1)
@@ -93,9 +96,12 @@ func TestTelemetry(t *testing.T) {
require.Len(t, snapshot.WorkspaceBuilds, 1)
require.Len(t, snapshot.WorkspaceResources, 1)
require.Len(t, snapshot.WorkspaceAgentStats, 1)
+ require.Len(t, snapshot.WorkspaceProxies, 1)
wsa := snapshot.WorkspaceAgents[0]
- require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystem)
+ require.Len(t, wsa.Subsystems, 2)
+ require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystems[0])
+ require.Equal(t, string(database.WorkspaceAgentSubsystemExectrace), wsa.Subsystems[1])
})
t.Run("HashedEmail", func(t *testing.T) {
t.Parallel()
diff --git a/coderd/templates.go b/coderd/templates.go
index 95fa9032d28ff..7d6dc46d2bf90 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -12,17 +12,17 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/examples"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/examples"
)
// Returns a single template.
@@ -219,8 +219,8 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
restartRequirementDaysOfWeek []string
restartRequirementWeeks int64
failureTTL time.Duration
- inactivityTTL time.Duration
- lockedTTL time.Duration
+ dormantTTL time.Duration
+ dormantAutoDeletionTTL time.Duration
)
if createTemplate.DefaultTTLMillis != nil {
defaultTTL = time.Duration(*createTemplate.DefaultTTLMillis) * time.Millisecond
@@ -232,11 +232,11 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
if createTemplate.FailureTTLMillis != nil {
failureTTL = time.Duration(*createTemplate.FailureTTLMillis) * time.Millisecond
}
- if createTemplate.InactivityTTLMillis != nil {
- inactivityTTL = time.Duration(*createTemplate.InactivityTTLMillis) * time.Millisecond
+ if createTemplate.TimeTilDormantMillis != nil {
+ dormantTTL = time.Duration(*createTemplate.TimeTilDormantMillis) * time.Millisecond
}
- if createTemplate.LockedTTLMillis != nil {
- lockedTTL = time.Duration(*createTemplate.LockedTTLMillis) * time.Millisecond
+ if createTemplate.TimeTilDormantAutoDeleteMillis != nil {
+ dormantAutoDeletionTTL = time.Duration(*createTemplate.TimeTilDormantAutoDeleteMillis) * time.Millisecond
}
var (
@@ -270,11 +270,11 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
if failureTTL < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Must be a positive integer."})
}
- if inactivityTTL < 0 {
- validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."})
+ if dormantTTL < 0 {
+ validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodeletion_ms", Detail: "Must be a positive integer."})
}
- if lockedTTL < 0 {
- validErrs = append(validErrs, codersdk.ValidationError{Field: "locked_ttl_ms", Detail: "Must be a positive integer."})
+ if dormantAutoDeletionTTL < 0 {
+ validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodeletion_ms", Detail: "Must be a positive integer."})
}
if len(validErrs) > 0 {
@@ -340,9 +340,9 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
DaysOfWeek: restartRequirementDaysOfWeekParsed,
Weeks: restartRequirementWeeks,
},
- FailureTTL: failureTTL,
- InactivityTTL: inactivityTTL,
- LockedTTL: lockedTTL,
+ FailureTTL: failureTTL,
+ TimeTilDormant: dormantTTL,
+ TimeTilDormantAutoDelete: dormantAutoDeletionTTL,
})
if err != nil {
return xerrors.Errorf("set template schedule options: %s", err)
@@ -533,13 +533,13 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
if req.FailureTTLMillis < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Must be a positive integer."})
}
- if req.InactivityTTLMillis < 0 {
+ if req.TimeTilDormantMillis < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."})
}
- if req.InactivityTTLMillis < 0 {
+ if req.TimeTilDormantMillis < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."})
}
- if req.LockedTTLMillis < 0 {
+ if req.TimeTilDormantAutoDeleteMillis < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "locked_ttl_ms", Detail: "Must be a positive integer."})
}
@@ -565,8 +565,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
restartRequirementDaysOfWeekParsed == scheduleOpts.RestartRequirement.DaysOfWeek &&
req.RestartRequirement.Weeks == scheduleOpts.RestartRequirement.Weeks &&
req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() &&
- req.InactivityTTLMillis == time.Duration(template.InactivityTTL).Milliseconds() &&
- req.LockedTTLMillis == time.Duration(template.LockedTTL).Milliseconds() {
+ req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() &&
+ req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() {
return nil
}
@@ -598,16 +598,16 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond
failureTTL := time.Duration(req.FailureTTLMillis) * time.Millisecond
- inactivityTTL := time.Duration(req.InactivityTTLMillis) * time.Millisecond
- lockedTTL := time.Duration(req.LockedTTLMillis) * time.Millisecond
+ inactivityTTL := time.Duration(req.TimeTilDormantMillis) * time.Millisecond
+ timeTilDormantAutoDelete := time.Duration(req.TimeTilDormantAutoDeleteMillis) * time.Millisecond
if defaultTTL != time.Duration(template.DefaultTTL) ||
maxTTL != time.Duration(template.MaxTTL) ||
restartRequirementDaysOfWeekParsed != scheduleOpts.RestartRequirement.DaysOfWeek ||
req.RestartRequirement.Weeks != scheduleOpts.RestartRequirement.Weeks ||
failureTTL != time.Duration(template.FailureTTL) ||
- inactivityTTL != time.Duration(template.InactivityTTL) ||
- lockedTTL != time.Duration(template.LockedTTL) ||
+ inactivityTTL != time.Duration(template.TimeTilDormant) ||
+ timeTilDormantAutoDelete != time.Duration(template.TimeTilDormantAutoDelete) ||
req.AllowUserAutostart != template.AllowUserAutostart ||
req.AllowUserAutostop != template.AllowUserAutostop {
updated, err = (*api.TemplateScheduleStore.Load()).Set(ctx, tx, updated, schedule.TemplateScheduleOptions{
@@ -622,9 +622,11 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
DaysOfWeek: restartRequirementDaysOfWeekParsed,
Weeks: req.RestartRequirement.Weeks,
},
- FailureTTL: failureTTL,
- InactivityTTL: inactivityTTL,
- LockedTTL: lockedTTL,
+ FailureTTL: failureTTL,
+ TimeTilDormant: inactivityTTL,
+ TimeTilDormantAutoDelete: timeTilDormantAutoDelete,
+ UpdateWorkspaceLastUsedAt: req.UpdateWorkspaceLastUsedAt,
+ UpdateWorkspaceDormantAt: req.UpdateWorkspaceDormantAt,
})
if err != nil {
return xerrors.Errorf("set template schedule options: %w", err)
@@ -736,28 +738,28 @@ func (api *API) convertTemplate(
buildTimeStats := api.metricsCache.TemplateBuildTimeStats(template.ID)
return codersdk.Template{
- ID: template.ID,
- CreatedAt: template.CreatedAt,
- UpdatedAt: template.UpdatedAt,
- OrganizationID: template.OrganizationID,
- Name: template.Name,
- DisplayName: template.DisplayName,
- Provisioner: codersdk.ProvisionerType(template.Provisioner),
- ActiveVersionID: template.ActiveVersionID,
- ActiveUserCount: activeCount,
- BuildTimeStats: buildTimeStats,
- Description: template.Description,
- Icon: template.Icon,
- DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(),
- MaxTTLMillis: time.Duration(template.MaxTTL).Milliseconds(),
- CreatedByID: template.CreatedBy,
- CreatedByName: template.CreatedByUsername,
- AllowUserAutostart: template.AllowUserAutostart,
- AllowUserAutostop: template.AllowUserAutostop,
- AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
- FailureTTLMillis: time.Duration(template.FailureTTL).Milliseconds(),
- InactivityTTLMillis: time.Duration(template.InactivityTTL).Milliseconds(),
- LockedTTLMillis: time.Duration(template.LockedTTL).Milliseconds(),
+ ID: template.ID,
+ CreatedAt: template.CreatedAt,
+ UpdatedAt: template.UpdatedAt,
+ OrganizationID: template.OrganizationID,
+ Name: template.Name,
+ DisplayName: template.DisplayName,
+ Provisioner: codersdk.ProvisionerType(template.Provisioner),
+ ActiveVersionID: template.ActiveVersionID,
+ ActiveUserCount: activeCount,
+ BuildTimeStats: buildTimeStats,
+ Description: template.Description,
+ Icon: template.Icon,
+ DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(),
+ MaxTTLMillis: time.Duration(template.MaxTTL).Milliseconds(),
+ CreatedByID: template.CreatedBy,
+ CreatedByName: template.CreatedByUsername,
+ AllowUserAutostart: template.AllowUserAutostart,
+ AllowUserAutostop: template.AllowUserAutostop,
+ AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ FailureTTLMillis: time.Duration(template.FailureTTL).Milliseconds(),
+ TimeTilDormantMillis: time.Duration(template.TimeTilDormant).Milliseconds(),
+ TimeTilDormantAutoDeleteMillis: time.Duration(template.TimeTilDormantAutoDelete).Milliseconds(),
RestartRequirement: codersdk.TemplateRestartRequirement{
DaysOfWeek: codersdk.BitmapToWeekdays(uint8(template.RestartRequirementDaysOfWeek)),
Weeks: template.RestartRequirementWeeks,
diff --git a/coderd/templates_test.go b/coderd/templates_test.go
index 2b7c41c9ad8f8..403370b5da670 100644
--- a/coderd/templates_test.go
+++ b/coderd/templates_test.go
@@ -12,16 +12,16 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
func TestTemplate(t *testing.T) {
@@ -270,8 +270,8 @@ func TestPostTemplateByOrganization(t *testing.T) {
RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek),
RestartRequirementWeeks: options.RestartRequirement.Weeks,
FailureTTL: int64(options.FailureTTL),
- InactivityTTL: int64(options.InactivityTTL),
- LockedTTL: int64(options.LockedTTL),
+ TimeTilDormant: int64(options.TimeTilDormant),
+ TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete),
})
if !assert.NoError(t, err) {
return database.Template{}, err
@@ -320,8 +320,8 @@ func TestPostTemplateByOrganization(t *testing.T) {
RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek),
RestartRequirementWeeks: options.RestartRequirement.Weeks,
FailureTTL: int64(options.FailureTTL),
- InactivityTTL: int64(options.InactivityTTL),
- LockedTTL: int64(options.LockedTTL),
+ TimeTilDormant: int64(options.TimeTilDormant),
+ TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete),
})
if !assert.NoError(t, err) {
return database.Template{}, err
@@ -598,8 +598,8 @@ func TestPatchTemplateMeta(t *testing.T) {
RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek),
RestartRequirementWeeks: options.RestartRequirement.Weeks,
FailureTTL: int64(options.FailureTTL),
- InactivityTTL: int64(options.InactivityTTL),
- LockedTTL: int64(options.LockedTTL),
+ TimeTilDormant: int64(options.TimeTilDormant),
+ TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete),
})
if !assert.NoError(t, err) {
return database.Template{}, err
@@ -697,9 +697,9 @@ func TestPatchTemplateMeta(t *testing.T) {
t.Parallel()
const (
- failureTTL = 7 * 24 * time.Hour
- inactivityTTL = 180 * 24 * time.Hour
- lockedTTL = 360 * 24 * time.Hour
+ failureTTL = 7 * 24 * time.Hour
+ inactivityTTL = 180 * 24 * time.Hour
+ timeTilDormantAutoDelete = 360 * 24 * time.Hour
)
t.Run("OK", func(t *testing.T) {
@@ -711,12 +711,12 @@ func TestPatchTemplateMeta(t *testing.T) {
SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
if atomic.AddInt64(&setCalled, 1) == 2 {
require.Equal(t, failureTTL, options.FailureTTL)
- require.Equal(t, inactivityTTL, options.InactivityTTL)
- require.Equal(t, lockedTTL, options.LockedTTL)
+ require.Equal(t, inactivityTTL, options.TimeTilDormant)
+ require.Equal(t, timeTilDormantAutoDelete, options.TimeTilDormantAutoDelete)
}
template.FailureTTL = int64(options.FailureTTL)
- template.InactivityTTL = int64(options.InactivityTTL)
- template.LockedTTL = int64(options.LockedTTL)
+ template.TimeTilDormant = int64(options.TimeTilDormant)
+ template.TimeTilDormantAutoDelete = int64(options.TimeTilDormantAutoDelete)
return template, nil
},
},
@@ -725,31 +725,31 @@ func TestPatchTemplateMeta(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
- ctr.InactivityTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
- ctr.LockedTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
+ ctr.TimeTilDormantMillis = ptr.Ref(0 * time.Hour.Milliseconds())
+ ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref(0 * time.Hour.Milliseconds())
})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
- Name: template.Name,
- DisplayName: template.DisplayName,
- Description: template.Description,
- Icon: template.Icon,
- DefaultTTLMillis: 0,
- RestartRequirement: &template.RestartRequirement,
- AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
- FailureTTLMillis: failureTTL.Milliseconds(),
- InactivityTTLMillis: inactivityTTL.Milliseconds(),
- LockedTTLMillis: lockedTTL.Milliseconds(),
+ Name: template.Name,
+ DisplayName: template.DisplayName,
+ Description: template.Description,
+ Icon: template.Icon,
+ DefaultTTLMillis: 0,
+ RestartRequirement: &template.RestartRequirement,
+ AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ FailureTTLMillis: failureTTL.Milliseconds(),
+ TimeTilDormantMillis: inactivityTTL.Milliseconds(),
+ TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(),
})
require.NoError(t, err)
require.EqualValues(t, 2, atomic.LoadInt64(&setCalled))
require.Equal(t, failureTTL.Milliseconds(), got.FailureTTLMillis)
- require.Equal(t, inactivityTTL.Milliseconds(), got.InactivityTTLMillis)
- require.Equal(t, lockedTTL.Milliseconds(), got.LockedTTLMillis)
+ require.Equal(t, inactivityTTL.Milliseconds(), got.TimeTilDormantMillis)
+ require.Equal(t, timeTilDormantAutoDelete.Milliseconds(), got.TimeTilDormantAutoDeleteMillis)
})
t.Run("IgnoredUnlicensed", func(t *testing.T) {
@@ -760,29 +760,29 @@ func TestPatchTemplateMeta(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
- ctr.InactivityTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
- ctr.LockedTTLMillis = ptr.Ref(0 * time.Hour.Milliseconds())
+ ctr.TimeTilDormantMillis = ptr.Ref(0 * time.Hour.Milliseconds())
+ ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref(0 * time.Hour.Milliseconds())
})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
- Name: template.Name,
- DisplayName: template.DisplayName,
- Description: template.Description,
- Icon: template.Icon,
- DefaultTTLMillis: template.DefaultTTLMillis,
- RestartRequirement: &template.RestartRequirement,
- AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
- FailureTTLMillis: failureTTL.Milliseconds(),
- InactivityTTLMillis: inactivityTTL.Milliseconds(),
- LockedTTLMillis: lockedTTL.Milliseconds(),
+ Name: template.Name,
+ DisplayName: template.DisplayName,
+ Description: template.Description,
+ Icon: template.Icon,
+ DefaultTTLMillis: template.DefaultTTLMillis,
+ RestartRequirement: &template.RestartRequirement,
+ AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ FailureTTLMillis: failureTTL.Milliseconds(),
+ TimeTilDormantMillis: inactivityTTL.Milliseconds(),
+ TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(),
})
require.NoError(t, err)
require.Zero(t, got.FailureTTLMillis)
- require.Zero(t, got.InactivityTTLMillis)
- require.Zero(t, got.LockedTTLMillis)
+ require.Zero(t, got.TimeTilDormantMillis)
+ require.Zero(t, got.TimeTilDormantAutoDeleteMillis)
})
})
@@ -989,8 +989,8 @@ func TestPatchTemplateMeta(t *testing.T) {
RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek),
RestartRequirementWeeks: options.RestartRequirement.Weeks,
FailureTTL: int64(options.FailureTTL),
- InactivityTTL: int64(options.InactivityTTL),
- LockedTTL: int64(options.LockedTTL),
+ TimeTilDormant: int64(options.TimeTilDormant),
+ TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete),
})
if !assert.NoError(t, err) {
return database.Template{}, err
@@ -1058,8 +1058,8 @@ func TestPatchTemplateMeta(t *testing.T) {
RestartRequirementDaysOfWeek: int16(options.RestartRequirement.DaysOfWeek),
RestartRequirementWeeks: options.RestartRequirement.Weeks,
FailureTTL: int64(options.FailureTTL),
- InactivityTTL: int64(options.InactivityTTL),
- LockedTTL: int64(options.LockedTTL),
+ TimeTilDormant: int64(options.TimeTilDormant),
+ TimeTilDormantAutoDelete: int64(options.TimeTilDormantAutoDelete),
})
if !assert.NoError(t, err) {
return database.Template{}, err
@@ -1203,7 +1203,7 @@ func TestTemplateMetrics(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
diff --git a/coderd/templateversions.go b/coderd/templateversions.go
index 80ad85a3ecc12..551a807418c9e 100644
--- a/coderd/templateversions.go
+++ b/coderd/templateversions.go
@@ -18,18 +18,18 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/parameter"
- "github.com/coder/coder/coderd/provisionerdserver"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/examples"
- sdkproto "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/parameter"
+ "github.com/coder/coder/v2/coderd/provisionerdserver"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/examples"
+ sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
)
// @Summary Get template version by ID
diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go
index 5826d31b06286..2ca934e2d4085 100644
--- a/coderd/templateversions_test.go
+++ b/coderd/templateversions_test.go
@@ -13,17 +13,17 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/provisionerdserver"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/examples"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/provisionerdserver"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/examples"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func TestTemplateVersion(t *testing.T) {
@@ -136,8 +136,8 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
data, err := echo.Tar(&echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: echo.ProvisionComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionApply: echo.ApplyComplete,
+ ProvisionPlan: echo.PlanComplete,
})
require.NoError(t, err)
@@ -245,8 +245,8 @@ func TestPatchCancelTemplateVersion(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
@@ -284,8 +284,8 @@ func TestPatchCancelTemplateVersion(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
@@ -308,11 +308,13 @@ func TestPatchCancelTemplateVersion(t *testing.T) {
require.Eventually(t, func() bool {
var err error
version, err = client.TemplateVersion(ctx, version.ID)
+ // job gets marked Failed when there is an Error; in practice we never get to Status = Canceled
+ // because provisioners report an Error when canceled. We check the Error string to ensure we don't mask
+ // other errors in this test.
+ t.Logf("got version %s | %s", version.Job.Error, version.Job.Status)
return assert.NoError(t, err) &&
- // The job will never actually cancel successfully because it will never send a
- // provision complete response.
- assert.Empty(t, version.Job.Error) &&
- version.Job.Status == codersdk.ProvisionerJobCanceling
+ strings.HasSuffix(version.Job.Error, "canceled") &&
+ version.Job.Status == codersdk.ProvisionerJobFailed
}, testutil.WaitShort, testutil.IntervalFast)
})
}
@@ -346,9 +348,9 @@ func TestTemplateVersionsGitAuth(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: []*proto.Response{{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
GitAuthProviders: []string{"github"},
},
},
@@ -400,9 +402,9 @@ func TestTemplateVersionResources(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -439,17 +441,17 @@ func TestTemplateVersionLogs(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "example",
},
},
}, {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -610,15 +612,15 @@ func TestTemplateVersionDryRun(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{
+ ProvisionApply: []*proto.Response{
{
- Type: &proto.Provision_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{},
},
},
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{resource},
},
},
@@ -677,8 +679,8 @@ func TestTemplateVersionDryRun(t *testing.T) {
// This import job will never finish
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
@@ -705,15 +707,15 @@ func TestTemplateVersionDryRun(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{
+ ProvisionApply: []*proto.Response{
{
- Type: &proto.Provision_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{},
},
},
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
},
},
},
@@ -776,15 +778,15 @@ func TestTemplateVersionDryRun(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{
+ ProvisionApply: []*proto.Response{
{
- Type: &proto.Provision_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{},
},
},
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
},
},
},
@@ -1040,21 +1042,17 @@ func TestTemplateVersionVariables(t *testing.T) {
createEchoResponses := func(templateVariables []*proto.TemplateVariable) *echo.Responses {
return &echo.Responses{
- Parse: []*proto.Parse_Response{
+ Parse: []*proto.Response{
{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
+ Type: &proto.Response_Parse{
+ Parse: &proto.ParseComplete{
TemplateVariables: templateVariables,
},
},
},
},
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- }},
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
}
}
@@ -1418,10 +1416,10 @@ func TestTemplateVersionParameters_Order(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
+ ProvisionPlan: []*proto.Response{
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{
Name: firstParameterName,
@@ -1453,11 +1451,7 @@ func TestTemplateVersionParameters_Order(t *testing.T) {
},
},
},
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- }},
+ ProvisionApply: echo.ApplyComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_all_templates.json.golden b/coderd/testdata/insights/multiple_users_and_workspaces_week_all_templates.json.golden
new file mode 100644
index 0000000000000..664e2fed8f250
--- /dev/null
+++ b/coderd/testdata/insights/multiple_users_and_workspaces_week_all_templates.json.golden
@@ -0,0 +1,159 @@
+{
+ "report": {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "active_users": 3,
+ "apps_usage": [
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "Visual Studio Code",
+ "slug": "vscode",
+ "icon": "/icon/code.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "JetBrains",
+ "slug": "jetbrains",
+ "icon": "/icon/intellij.svg",
+ "seconds": 120
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "Web Terminal",
+ "slug": "reconnecting-pty",
+ "icon": "/icon/terminal.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "SSH",
+ "slug": "ssh",
+ "icon": "/icon/terminal.svg",
+ "seconds": 11520
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "type": "app",
+ "display_name": "app1",
+ "slug": "app1",
+ "icon": "/icon1.png",
+ "seconds": 25380
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "app",
+ "display_name": "app3",
+ "slug": "app3",
+ "icon": "/icon2.png",
+ "seconds": 720
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "app",
+ "display_name": "otherapp1",
+ "slug": "otherapp1",
+ "icon": "/icon1.png",
+ "seconds": 300
+ }
+ ],
+ "parameters_usage": []
+ },
+ "interval_reports": [
+ {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-16T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 3
+ },
+ {
+ "start_time": "2023-08-16T00:00:00Z",
+ "end_time": "2023-08-17T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-17T00:00:00Z",
+ "end_time": "2023-08-18T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 2
+ },
+ {
+ "start_time": "2023-08-18T00:00:00Z",
+ "end_time": "2023-08-19T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-19T00:00:00Z",
+ "end_time": "2023-08-20T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-20T00:00:00Z",
+ "end_time": "2023-08-21T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-21T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ }
+ ]
+}
diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_deployment_wide.json.golden b/coderd/testdata/insights/multiple_users_and_workspaces_week_deployment_wide.json.golden
new file mode 100644
index 0000000000000..664e2fed8f250
--- /dev/null
+++ b/coderd/testdata/insights/multiple_users_and_workspaces_week_deployment_wide.json.golden
@@ -0,0 +1,159 @@
+{
+ "report": {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "active_users": 3,
+ "apps_usage": [
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "Visual Studio Code",
+ "slug": "vscode",
+ "icon": "/icon/code.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "JetBrains",
+ "slug": "jetbrains",
+ "icon": "/icon/intellij.svg",
+ "seconds": 120
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "Web Terminal",
+ "slug": "reconnecting-pty",
+ "icon": "/icon/terminal.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "SSH",
+ "slug": "ssh",
+ "icon": "/icon/terminal.svg",
+ "seconds": 11520
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "type": "app",
+ "display_name": "app1",
+ "slug": "app1",
+ "icon": "/icon1.png",
+ "seconds": 25380
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "app",
+ "display_name": "app3",
+ "slug": "app3",
+ "icon": "/icon2.png",
+ "seconds": 720
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "app",
+ "display_name": "otherapp1",
+ "slug": "otherapp1",
+ "icon": "/icon1.png",
+ "seconds": 300
+ }
+ ],
+ "parameters_usage": []
+ },
+ "interval_reports": [
+ {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-16T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 3
+ },
+ {
+ "start_time": "2023-08-16T00:00:00Z",
+ "end_time": "2023-08-17T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-17T00:00:00Z",
+ "end_time": "2023-08-18T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 2
+ },
+ {
+ "start_time": "2023-08-18T00:00:00Z",
+ "end_time": "2023-08-19T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-19T00:00:00Z",
+ "end_time": "2023-08-20T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-20T00:00:00Z",
+ "end_time": "2023-08-21T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-21T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ }
+ ]
+}
diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_first_template.json.golden b/coderd/testdata/insights/multiple_users_and_workspaces_week_first_template.json.golden
new file mode 100644
index 0000000000000..d96469dc5c724
--- /dev/null
+++ b/coderd/testdata/insights/multiple_users_and_workspaces_week_first_template.json.golden
@@ -0,0 +1,132 @@
+{
+ "report": {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "active_users": 2,
+ "apps_usage": [
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "builtin",
+ "display_name": "Visual Studio Code",
+ "slug": "vscode",
+ "icon": "/icon/code.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "builtin",
+ "display_name": "JetBrains",
+ "slug": "jetbrains",
+ "icon": "/icon/intellij.svg",
+ "seconds": 120
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "builtin",
+ "display_name": "Web Terminal",
+ "slug": "reconnecting-pty",
+ "icon": "/icon/terminal.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "builtin",
+ "display_name": "SSH",
+ "slug": "ssh",
+ "icon": "/icon/terminal.svg",
+ "seconds": 7920
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "app",
+ "display_name": "app1",
+ "slug": "app1",
+ "icon": "/icon1.png",
+ "seconds": 3780
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "app",
+ "display_name": "app3",
+ "slug": "app3",
+ "icon": "/icon2.png",
+ "seconds": 720
+ }
+ ],
+ "parameters_usage": []
+ },
+ "interval_reports": [
+ {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-16T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 2
+ },
+ {
+ "start_time": "2023-08-16T00:00:00Z",
+ "end_time": "2023-08-17T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-17T00:00:00Z",
+ "end_time": "2023-08-18T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-18T00:00:00Z",
+ "end_time": "2023-08-19T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-19T00:00:00Z",
+ "end_time": "2023-08-20T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-20T00:00:00Z",
+ "end_time": "2023-08-21T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-21T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ }
+ ]
+}
diff --git "a/coderd/testdata/insights/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" "b/coderd/testdata/insights/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden"
new file mode 100644
index 0000000000000..8f447e4112dd0
--- /dev/null
+++ "b/coderd/testdata/insights/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden"
@@ -0,0 +1,150 @@
+{
+ "report": {
+ "start_time": "2023-08-15T00:00:00-03:00",
+ "end_time": "2023-08-22T00:00:00-03:00",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "active_users": 3,
+ "apps_usage": [
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "builtin",
+ "display_name": "Visual Studio Code",
+ "slug": "vscode",
+ "icon": "/icon/code.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "builtin",
+ "display_name": "JetBrains",
+ "slug": "jetbrains",
+ "icon": "/icon/intellij.svg",
+ "seconds": 120
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "builtin",
+ "display_name": "Web Terminal",
+ "slug": "reconnecting-pty",
+ "icon": "/icon/terminal.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "builtin",
+ "display_name": "SSH",
+ "slug": "ssh",
+ "icon": "/icon/terminal.svg",
+ "seconds": 4320
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "type": "app",
+ "display_name": "app1",
+ "slug": "app1",
+ "icon": "/icon1.png",
+ "seconds": 21720
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "type": "app",
+ "display_name": "app3",
+ "slug": "app3",
+ "icon": "/icon2.png",
+ "seconds": 4320
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "app",
+ "display_name": "otherapp1",
+ "slug": "otherapp1",
+ "icon": "/icon1.png",
+ "seconds": 300
+ }
+ ],
+ "parameters_usage": []
+ },
+ "interval_reports": [
+ {
+ "start_time": "2023-08-15T00:00:00-03:00",
+ "end_time": "2023-08-16T00:00:00-03:00",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-16T00:00:00-03:00",
+ "end_time": "2023-08-17T00:00:00-03:00",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 2
+ },
+ {
+ "start_time": "2023-08-17T00:00:00-03:00",
+ "end_time": "2023-08-18T00:00:00-03:00",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000002",
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 2
+ },
+ {
+ "start_time": "2023-08-18T00:00:00-03:00",
+ "end_time": "2023-08-19T00:00:00-03:00",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-19T00:00:00-03:00",
+ "end_time": "2023-08-20T00:00:00-03:00",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-20T00:00:00-03:00",
+ "end_time": "2023-08-21T00:00:00-03:00",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-21T00:00:00-03:00",
+ "end_time": "2023-08-22T00:00:00-03:00",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "interval": "day",
+ "active_users": 1
+ }
+ ]
+}
diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_second_template.json.golden b/coderd/testdata/insights/multiple_users_and_workspaces_week_second_template.json.golden
new file mode 100644
index 0000000000000..b15cba10a8520
--- /dev/null
+++ b/coderd/testdata/insights/multiple_users_and_workspaces_week_second_template.json.golden
@@ -0,0 +1,118 @@
+{
+ "report": {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "active_users": 1,
+ "apps_usage": [
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "type": "builtin",
+ "display_name": "Visual Studio Code",
+ "slug": "vscode",
+ "icon": "/icon/code.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "type": "builtin",
+ "display_name": "JetBrains",
+ "slug": "jetbrains",
+ "icon": "/icon/intellij.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "type": "builtin",
+ "display_name": "Web Terminal",
+ "slug": "reconnecting-pty",
+ "icon": "/icon/terminal.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "type": "builtin",
+ "display_name": "SSH",
+ "slug": "ssh",
+ "icon": "/icon/terminal.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "type": "app",
+ "display_name": "app1",
+ "slug": "app1",
+ "icon": "/icon1.png",
+ "seconds": 21600
+ }
+ ],
+ "parameters_usage": []
+ },
+ "interval_reports": [
+ {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-16T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-16T00:00:00Z",
+ "end_time": "2023-08-17T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-17T00:00:00Z",
+ "end_time": "2023-08-18T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-18T00:00:00Z",
+ "end_time": "2023-08-19T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-19T00:00:00Z",
+ "end_time": "2023-08-20T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-20T00:00:00Z",
+ "end_time": "2023-08-21T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-21T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ }
+ ]
+}
diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_third_template.json.golden b/coderd/testdata/insights/multiple_users_and_workspaces_week_third_template.json.golden
new file mode 100644
index 0000000000000..ea4002e09f152
--- /dev/null
+++ b/coderd/testdata/insights/multiple_users_and_workspaces_week_third_template.json.golden
@@ -0,0 +1,120 @@
+{
+ "report": {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "active_users": 1,
+ "apps_usage": [
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "Visual Studio Code",
+ "slug": "vscode",
+ "icon": "/icon/code.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "JetBrains",
+ "slug": "jetbrains",
+ "icon": "/icon/intellij.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "Web Terminal",
+ "slug": "reconnecting-pty",
+ "icon": "/icon/terminal.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "builtin",
+ "display_name": "SSH",
+ "slug": "ssh",
+ "icon": "/icon/terminal.svg",
+ "seconds": 3600
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "type": "app",
+ "display_name": "otherapp1",
+ "slug": "otherapp1",
+ "icon": "/icon1.png",
+ "seconds": 300
+ }
+ ],
+ "parameters_usage": []
+ },
+ "interval_reports": [
+ {
+ "start_time": "2023-08-15T00:00:00Z",
+ "end_time": "2023-08-16T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-16T00:00:00Z",
+ "end_time": "2023-08-17T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-17T00:00:00Z",
+ "end_time": "2023-08-18T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-18T00:00:00Z",
+ "end_time": "2023-08-19T00:00:00Z",
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "interval": "day",
+ "active_users": 1
+ },
+ {
+ "start_time": "2023-08-19T00:00:00Z",
+ "end_time": "2023-08-20T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-20T00:00:00Z",
+ "end_time": "2023-08-21T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ },
+ {
+ "start_time": "2023-08-21T00:00:00Z",
+ "end_time": "2023-08-22T00:00:00Z",
+ "template_ids": [],
+ "interval": "day",
+ "active_users": 0
+ }
+ ]
+}
diff --git a/coderd/testdata/insights/parameters_two_days_ago,_no_data.json.golden b/coderd/testdata/insights/parameters_two_days_ago,_no_data.json.golden
new file mode 100644
index 0000000000000..e3875b2a34b38
--- /dev/null
+++ b/coderd/testdata/insights/parameters_two_days_ago,_no_data.json.golden
@@ -0,0 +1,44 @@
+{
+ "report": {
+ "start_time": "0001-01-01T00:00:00Z",
+ "end_time": "0001-01-01T00:00:00Z",
+ "template_ids": [],
+ "active_users": 0,
+ "apps_usage": [
+ {
+ "template_ids": [],
+ "type": "builtin",
+ "display_name": "Visual Studio Code",
+ "slug": "vscode",
+ "icon": "/icon/code.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [],
+ "type": "builtin",
+ "display_name": "JetBrains",
+ "slug": "jetbrains",
+ "icon": "/icon/intellij.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [],
+ "type": "builtin",
+ "display_name": "Web Terminal",
+ "slug": "reconnecting-pty",
+ "icon": "/icon/terminal.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [],
+ "type": "builtin",
+ "display_name": "SSH",
+ "slug": "ssh",
+ "icon": "/icon/terminal.svg",
+ "seconds": 0
+ }
+ ],
+ "parameters_usage": []
+ },
+ "interval_reports": []
+}
diff --git a/coderd/testdata/insights/parameters_yesterday_and_today_deployment_wide.json.golden b/coderd/testdata/insights/parameters_yesterday_and_today_deployment_wide.json.golden
new file mode 100644
index 0000000000000..fc7ccd8a50ec4
--- /dev/null
+++ b/coderd/testdata/insights/parameters_yesterday_and_today_deployment_wide.json.golden
@@ -0,0 +1,165 @@
+{
+ "report": {
+ "start_time": "0001-01-01T00:00:00Z",
+ "end_time": "0001-01-01T00:00:00Z",
+ "template_ids": [],
+ "active_users": 0,
+ "apps_usage": [
+ {
+ "template_ids": [],
+ "type": "builtin",
+ "display_name": "Visual Studio Code",
+ "slug": "vscode",
+ "icon": "/icon/code.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [],
+ "type": "builtin",
+ "display_name": "JetBrains",
+ "slug": "jetbrains",
+ "icon": "/icon/intellij.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [],
+ "type": "builtin",
+ "display_name": "Web Terminal",
+ "slug": "reconnecting-pty",
+ "icon": "/icon/terminal.svg",
+ "seconds": 0
+ },
+ {
+ "template_ids": [],
+ "type": "builtin",
+ "display_name": "SSH",
+ "slug": "ssh",
+ "icon": "/icon/terminal.svg",
+ "seconds": 0
+ }
+ ],
+ "parameters_usage": [
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000003"
+ ],
+ "display_name": "otherparam1",
+ "name": "otherparam1",
+ "type": "string",
+ "description": "This is another parameter",
+ "values": [
+ {
+ "value": "",
+ "count": 1
+ },
+ {
+ "value": "xyz",
+ "count": 1
+ }
+ ]
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "display_name": "param1",
+ "name": "param1",
+ "type": "string",
+ "description": "This is first parameter",
+ "values": [
+ {
+ "value": "",
+ "count": 1
+ },
+ {
+ "value": "ABC",
+ "count": 1
+ },
+ {
+ "value": "abc",
+ "count": 2
+ }
+ ]
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "display_name": "param2",
+ "name": "param2",
+ "type": "string",
+ "description": "This is second parameter",
+ "values": [
+ {
+ "value": "",
+ "count": 1
+ },
+ {
+ "value": "123",
+ "count": 3
+ }
+ ]
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001",
+ "00000000-0000-0000-0000-000000000002"
+ ],
+ "display_name": "param3",
+ "name": "param3",
+ "type": "string",
+ "description": "This is third parameter",
+ "values": [
+ {
+ "value": "",
+ "count": 1
+ },
+ {
+ "value": "BBB",
+ "count": 2
+ },
+ {
+ "value": "bbb",
+ "count": 1
+ }
+ ]
+ },
+ {
+ "template_ids": [
+ "00000000-0000-0000-0000-000000000001"
+ ],
+ "display_name": "param4",
+ "name": "param4",
+ "type": "string",
+ "description": "This is fourth parameter",
+ "options": [
+ {
+ "name": "option1",
+ "description": "",
+ "value": "option1",
+ "icon": ""
+ },
+ {
+ "name": "option2",
+ "description": "",
+ "value": "option2",
+ "icon": ""
+ }
+ ],
+ "values": [
+ {
+ "value": "option1",
+ "count": 2
+ },
+ {
+ "value": "option2",
+ "count": 1
+ }
+ ]
+ }
+ ]
+ },
+ "interval_reports": []
+}
diff --git a/coderd/tracing/httpmw.go b/coderd/tracing/httpmw.go
index 308fbd88a4c26..653a74386245b 100644
--- a/coderd/tracing/httpmw.go
+++ b/coderd/tracing/httpmw.go
@@ -13,7 +13,7 @@ import (
"go.opentelemetry.io/otel/semconv/v1.14.0/netconv"
"go.opentelemetry.io/otel/trace"
- "github.com/coder/coder/coderd/httpmw/patternmatcher"
+ "github.com/coder/coder/v2/coderd/httpmw/patternmatcher"
)
// Middleware adds tracing to http routes.
diff --git a/coderd/tracing/httpmw_test.go b/coderd/tracing/httpmw_test.go
index 052544fdc7ed9..e866acd513ec3 100644
--- a/coderd/tracing/httpmw_test.go
+++ b/coderd/tracing/httpmw_test.go
@@ -13,8 +13,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/testutil"
)
type fakeTracer struct {
diff --git a/coderd/tracing/slog_test.go b/coderd/tracing/slog_test.go
index 2db0013d20792..5dae380e07c42 100644
--- a/coderd/tracing/slog_test.go
+++ b/coderd/tracing/slog_test.go
@@ -13,7 +13,7 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/tracing"
)
type stringer string
diff --git a/coderd/tracing/status_writer.go b/coderd/tracing/status_writer.go
index 9409c3adf5e69..e9337c20e022f 100644
--- a/coderd/tracing/status_writer.go
+++ b/coderd/tracing/status_writer.go
@@ -12,7 +12,7 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/buildinfo"
+ "github.com/coder/coder/v2/buildinfo"
)
var (
diff --git a/coderd/tracing/status_writer_test.go b/coderd/tracing/status_writer_test.go
index 4cc3e5507d600..ba19cd29a915c 100644
--- a/coderd/tracing/status_writer_test.go
+++ b/coderd/tracing/status_writer_test.go
@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/tracing"
)
func TestStatusWriter(t *testing.T) {
diff --git a/coderd/tracing/util_test.go b/coderd/tracing/util_test.go
index 1835d5325c415..708218328f01f 100644
--- a/coderd/tracing/util_test.go
+++ b/coderd/tracing/util_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/tracing"
)
// t.Parallel affects the result of these tests.
diff --git a/coderd/unhanger/detector.go b/coderd/unhanger/detector.go
index a2dcb56689fb3..975b0a5f53d17 100644
--- a/coderd/unhanger/detector.go
+++ b/coderd/unhanger/detector.go
@@ -13,11 +13,11 @@ import (
"github.com/google/uuid"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/provisionersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/provisionersdk"
)
const (
diff --git a/coderd/unhanger/detector_test.go b/coderd/unhanger/detector_test.go
index 41d2dd26f2a90..45e52cafdcb55 100644
--- a/coderd/unhanger/detector_test.go
+++ b/coderd/unhanger/detector_test.go
@@ -14,12 +14,12 @@ import (
"go.uber.org/goleak"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/unhanger"
- "github.com/coder/coder/provisionersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/unhanger"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
diff --git a/coderd/updatecheck.go b/coderd/updatecheck.go
index 882a5e6f4b5a2..4e4b07683ecf1 100644
--- a/coderd/updatecheck.go
+++ b/coderd/updatecheck.go
@@ -8,9 +8,9 @@ import (
"golang.org/x/mod/semver"
"golang.org/x/xerrors"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Update check
diff --git a/coderd/updatecheck/updatecheck.go b/coderd/updatecheck/updatecheck.go
index 5f180dd196de9..de14071a903b6 100644
--- a/coderd/updatecheck/updatecheck.go
+++ b/coderd/updatecheck/updatecheck.go
@@ -19,8 +19,8 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
)
const (
diff --git a/coderd/updatecheck/updatecheck_test.go b/coderd/updatecheck/updatecheck_test.go
index d414d8c72af10..103064eb7e6de 100644
--- a/coderd/updatecheck/updatecheck_test.go
+++ b/coderd/updatecheck/updatecheck_test.go
@@ -14,9 +14,9 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/updatecheck"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/updatecheck"
+ "github.com/coder/coder/v2/testutil"
)
func TestChecker_Notify(t *testing.T) {
diff --git a/coderd/updatecheck_test.go b/coderd/updatecheck_test.go
index 24dd8eba59ab3..c81dc0821a152 100644
--- a/coderd/updatecheck_test.go
+++ b/coderd/updatecheck_test.go
@@ -10,11 +10,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/updatecheck"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/updatecheck"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestUpdateCheck_NewVersion(t *testing.T) {
diff --git a/coderd/userauth.go b/coderd/userauth.go
index 9b6ba7992bad5..80c40b7e4c5d8 100644
--- a/coderd/userauth.go
+++ b/coderd/userauth.go
@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"net/mail"
+ "regexp"
"sort"
"strconv"
"strings"
@@ -22,17 +23,17 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/apikey"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/userpassword"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/site"
+ "github.com/coder/coder/v2/coderd/apikey"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/userpassword"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/site"
)
const (
@@ -183,7 +184,9 @@ func (api *API) postConvertLoginType(rw http.ResponseWriter, r *http.Request) {
Expires: claims.ExpiresAt.Time,
Secure: api.SecureAuthCookie,
HttpOnly: true,
- SameSite: http.SameSiteStrictMode,
+ // Must be SameSite to work on the redirected auth flow from the
+ // oauth provider.
+ SameSite: http.SameSiteLaxMode,
})
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.OAuthConversionResponse{
StateString: stateString,
@@ -688,6 +691,13 @@ type OIDCConfig struct {
// groups. If the group field is the empty string, then no group updates
// will ever come from the OIDC provider.
GroupField string
+ // CreateMissingGroups controls whether groups returned by the OIDC provider
+ // are automatically created in Coder if they are missing.
+ CreateMissingGroups bool
+ // GroupFilter is a regular expression that filters the groups returned by
+ // the OIDC provider. Any group not matched by this regex will be ignored.
+ // If the group filter is nil, then no group filtering will occur.
+ GroupFilter *regexp.Regexp
// GroupMapping controls how groups returned by the OIDC provider get mapped
// to groups within Coder.
// map[oidcGroupName]coderGroupName
@@ -1029,19 +1039,21 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
}
params := (&oauthLoginParams{
- User: user,
- Link: link,
- State: state,
- LinkedID: oidcLinkedID(idToken),
- LoginType: database.LoginTypeOIDC,
- AllowSignups: api.OIDCConfig.AllowSignups,
- Email: email,
- Username: username,
- AvatarURL: picture,
- UsingGroups: usingGroups,
- UsingRoles: api.OIDCConfig.RoleSyncEnabled(),
- Roles: roles,
- Groups: groups,
+ User: user,
+ Link: link,
+ State: state,
+ LinkedID: oidcLinkedID(idToken),
+ LoginType: database.LoginTypeOIDC,
+ AllowSignups: api.OIDCConfig.AllowSignups,
+ Email: email,
+ Username: username,
+ AvatarURL: picture,
+ UsingGroups: usingGroups,
+ UsingRoles: api.OIDCConfig.RoleSyncEnabled(),
+ Roles: roles,
+ Groups: groups,
+ CreateMissingGroups: api.OIDCConfig.CreateMissingGroups,
+ GroupFilter: api.OIDCConfig.GroupFilter,
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
return audit.InitRequest[database.User](rw, params)
})
@@ -1125,8 +1137,10 @@ type oauthLoginParams struct {
AvatarURL string
// Is UsingGroups is true, then the user will be assigned
// to the Groups provided.
- UsingGroups bool
- Groups []string
+ UsingGroups bool
+ CreateMissingGroups bool
+ Groups []string
+ GroupFilter *regexp.Regexp
// Is UsingRoles is true, then the user will be assigned
// the roles provided.
UsingRoles bool
@@ -1342,8 +1356,18 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
// Ensure groups are correct.
if params.UsingGroups {
+ filtered := params.Groups
+ if params.GroupFilter != nil {
+ filtered = make([]string, 0, len(params.Groups))
+ for _, group := range params.Groups {
+ if params.GroupFilter.MatchString(group) {
+ filtered = append(filtered, group)
+ }
+ }
+ }
+
//nolint:gocritic
- err := api.Options.SetUserGroups(dbauthz.AsSystemRestricted(ctx), tx, user.ID, params.Groups)
+ err := api.Options.SetUserGroups(dbauthz.AsSystemRestricted(ctx), logger, tx, user.ID, filtered, params.CreateMissingGroups)
if err != nil {
return xerrors.Errorf("set user groups: %w", err)
}
@@ -1362,7 +1386,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
}
//nolint:gocritic
- err := api.Options.SetUserSiteRoles(dbauthz.AsSystemRestricted(ctx), tx, user.ID, filtered)
+ err := api.Options.SetUserSiteRoles(dbauthz.AsSystemRestricted(ctx), logger, tx, user.ID, filtered)
if err != nil {
return httpError{
code: http.StatusBadRequest,
@@ -1427,7 +1451,8 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
}
var key database.APIKey
- if oldKey, ok := httpmw.APIKeyOptional(r); ok && isConvertLoginType {
+ oldKey, _, ok := httpmw.APIKeyFromRequest(ctx, api.Database, nil, r)
+ if ok && oldKey != nil && isConvertLoginType {
// If this is a convert login type, and it succeeds, then delete the old
// session. Force the user to log back in.
err := api.Database.DeleteAPIKeyByID(r.Context(), oldKey.ID)
@@ -1447,7 +1472,9 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
Secure: api.SecureAuthCookie,
HttpOnly: true,
})
- key = oldKey
+ // This is intentional setting the key to the deleted old key,
+ // as the user needs to be forced to log back in.
+ key = *oldKey
} else {
//nolint:gocritic
cookie, newKey, err := api.createAPIKey(dbauthz.AsSystemRestricted(ctx), apikey.CreateParams{
@@ -1648,7 +1675,7 @@ func clearOAuthConvertCookie() *http.Cookie {
func wrongLoginTypeHTTPError(user database.LoginType, params database.LoginType) httpError {
addedMsg := ""
if user == database.LoginTypePassword {
- addedMsg = " Try logging in with your password."
+ addedMsg = " You can convert your account to use this login type by visiting your account settings."
}
return httpError{
code: http.StatusForbidden,
diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go
index 6f49222ff8764..1f37a0721a1e7 100644
--- a/coderd/userauth_test.go
+++ b/coderd/userauth_test.go
@@ -4,32 +4,72 @@ import (
"context"
"crypto"
"fmt"
- "io"
"net/http"
"net/http/cookiejar"
+ "net/url"
"strings"
"testing"
"github.com/coreos/go-oidc/v3/oidc"
- "github.com/golang-jwt/jwt"
+ "github.com/golang-jwt/jwt/v4"
"github.com/google/go-github/v43/github"
"github.com/google/uuid"
- "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "golang.org/x/oauth2"
"golang.org/x/xerrors"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/coderdtest/oidctest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
+// This test specifically tests logging in with OIDC when an expired
+// OIDC session token exists.
+// The token refreshing should not happen since we are reauthenticating.
+// nolint:bodyclose
+func TestOIDCOauthLoginWithExisting(t *testing.T) {
+ t.Parallel()
+
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithRefreshHook(func(_ string) error {
+ return xerrors.New("refreshing token should never occur")
+ }),
+ oidctest.WithServing(),
+ )
+
+ cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.IgnoreUserInfo = true
+ })
+
+ client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
+ OIDCConfig: cfg,
+ })
+
+ const username = "alice"
+ claims := jwt.MapClaims{
+ "email": "alice@coder.com",
+ "email_verified": true,
+ "preferred_username": username,
+ }
+
+ helper := oidctest.NewLoginHelper(client, fake)
+ // Signup alice
+ userClient, _ := helper.Login(t, claims)
+
+ // Expire the link. This will force the client to refresh the token.
+ helper.ExpireOauthToken(t, api.Database, userClient)
+
+ // Instead of refreshing, just log in again.
+ helper.Login(t, claims)
+}
+
func TestUserLogin(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
@@ -59,7 +99,7 @@ func TestUserLogin(t *testing.T) {
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
})
// Password auth should fail if the user is made without password login.
- t.Run("LoginTypeNone", func(t *testing.T) {
+ t.Run("DisableLoginDeprecatedField", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
@@ -74,6 +114,22 @@ func TestUserLogin(t *testing.T) {
})
require.Error(t, err)
})
+
+ t.Run("LoginTypeNone", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ user := coderdtest.CreateFirstUser(t, client)
+ anotherClient, anotherUser := coderdtest.CreateAnotherUserMutators(t, client, user.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
+ r.Password = ""
+ r.UserLoginType = codersdk.LoginTypeNone
+ })
+
+ _, err := anotherClient.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
+ Email: anotherUser.Email,
+ Password: "SomeSecurePassword!",
+ })
+ require.Error(t, err)
+ })
}
func TestUserAuthMethods(t *testing.T) {
@@ -558,7 +614,7 @@ func TestUserOIDC(t *testing.T) {
"email": "kyle@kwc.io",
},
AllowSignups: true,
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
Username: "kyle",
}, {
Name: "EmailNotVerified",
@@ -583,7 +639,7 @@ func TestUserOIDC(t *testing.T) {
"email_verified": false,
},
AllowSignups: true,
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
Username: "kyle",
IgnoreEmailVerified: true,
}, {
@@ -607,7 +663,7 @@ func TestUserOIDC(t *testing.T) {
EmailDomain: []string{
"kwc.io",
},
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}, {
Name: "EmptyClaims",
IDTokenClaims: jwt.MapClaims{},
@@ -628,7 +684,7 @@ func TestUserOIDC(t *testing.T) {
},
Username: "kyle",
AllowSignups: true,
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}, {
Name: "UsernameFromClaims",
IDTokenClaims: jwt.MapClaims{
@@ -638,7 +694,7 @@ func TestUserOIDC(t *testing.T) {
},
Username: "hotdog",
AllowSignups: true,
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}, {
// Services like Okta return the email as the username:
// https://developer.okta.com/docs/reference/api/oidc/#base-claims-always-present
@@ -650,7 +706,7 @@ func TestUserOIDC(t *testing.T) {
},
Username: "kyle",
AllowSignups: true,
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}, {
// See: https://github.com/coder/coder/issues/4472
Name: "UsernameIsEmail",
@@ -659,7 +715,7 @@ func TestUserOIDC(t *testing.T) {
},
Username: "kyle",
AllowSignups: true,
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}, {
Name: "WithPicture",
IDTokenClaims: jwt.MapClaims{
@@ -671,7 +727,7 @@ func TestUserOIDC(t *testing.T) {
Username: "kyle",
AllowSignups: true,
AvatarURL: "/example.png",
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}, {
Name: "WithUserInfoClaims",
IDTokenClaims: jwt.MapClaims{
@@ -685,7 +741,7 @@ func TestUserOIDC(t *testing.T) {
Username: "potato",
AllowSignups: true,
AvatarURL: "/example.png",
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}, {
Name: "GroupsDoesNothing",
IDTokenClaims: jwt.MapClaims{
@@ -693,7 +749,7 @@ func TestUserOIDC(t *testing.T) {
"groups": []string{"pingpong"},
},
AllowSignups: true,
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}, {
Name: "UserInfoOverridesIDTokenClaims",
IDTokenClaims: jwt.MapClaims{
@@ -708,7 +764,7 @@ func TestUserOIDC(t *testing.T) {
Username: "user",
AllowSignups: true,
IgnoreEmailVerified: false,
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}, {
Name: "InvalidUserInfo",
IDTokenClaims: jwt.MapClaims{
@@ -735,36 +791,41 @@ func TestUserOIDC(t *testing.T) {
Username: "user",
IgnoreUserInfo: true,
AllowSignups: true,
- StatusCode: http.StatusTemporaryRedirect,
+ StatusCode: http.StatusOK,
}} {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
- auditor := audit.NewMock()
- conf := coderdtest.NewOIDCConfig(t, "")
-
- config := conf.OIDCConfig(t, tc.UserInfoClaims)
- config.AllowSignups = tc.AllowSignups
- config.EmailDomain = tc.EmailDomain
- config.IgnoreEmailVerified = tc.IgnoreEmailVerified
- config.IgnoreUserInfo = tc.IgnoreUserInfo
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithRefreshHook(func(_ string) error {
+ return xerrors.New("refreshing token should never occur")
+ }),
+ oidctest.WithServing(),
+ oidctest.WithStaticUserInfo(tc.UserInfoClaims),
+ )
+ cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = tc.AllowSignups
+ cfg.EmailDomain = tc.EmailDomain
+ cfg.IgnoreEmailVerified = tc.IgnoreEmailVerified
+ cfg.IgnoreUserInfo = tc.IgnoreUserInfo
+ })
+ auditor := audit.NewMock()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
- client := coderdtest.New(t, &coderdtest.Options{
+ owner := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
- OIDCConfig: config,
+ OIDCConfig: cfg,
Logger: &logger,
})
numLogs := len(auditor.AuditLogs())
- resp := oidcCallback(t, client, conf.EncodeClaims(t, tc.IDTokenClaims))
+ client, resp := fake.AttemptLogin(t, owner, tc.IDTokenClaims)
numLogs++ // add an audit log for login
- assert.Equal(t, tc.StatusCode, resp.StatusCode)
+ require.Equal(t, tc.StatusCode, resp.StatusCode)
ctx := testutil.Context(t, testutil.WaitLong)
if tc.Username != "" {
- client.SetSessionToken(authCookieValue(resp.Cookies()))
user, err := client.User(ctx, "me")
require.NoError(t, err)
require.Equal(t, tc.Username, user.Username)
@@ -775,7 +836,6 @@ func TestUserOIDC(t *testing.T) {
}
if tc.AvatarURL != "" {
- client.SetSessionToken(authCookieValue(resp.Cookies()))
user, err := client.User(ctx, "me")
require.NoError(t, err)
require.Equal(t, tc.AvatarURL, user.AvatarURL)
@@ -788,26 +848,29 @@ func TestUserOIDC(t *testing.T) {
t.Run("OIDCConvert", func(t *testing.T) {
t.Parallel()
- auditor := audit.NewMock()
- conf := coderdtest.NewOIDCConfig(t, "")
- config := conf.OIDCConfig(t, nil)
- config.AllowSignups = true
+ auditor := audit.NewMock()
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithRefreshHook(func(_ string) error {
+ return xerrors.New("refreshing token should never occur")
+ }),
+ oidctest.WithServing(),
+ )
+ cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ })
- cfg := coderdtest.DeploymentValues(t)
client := coderdtest.New(t, &coderdtest.Options{
- Auditor: auditor,
- OIDCConfig: config,
- DeploymentValues: cfg,
+ Auditor: auditor,
+ OIDCConfig: cfg,
})
- owner := coderdtest.CreateFirstUser(t, client)
+ owner := coderdtest.CreateFirstUser(t, client)
user, userData := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
- code := conf.EncodeClaims(t, jwt.MapClaims{
+ claims := jwt.MapClaims{
"email": userData.Email,
- })
-
+ }
var err error
user.HTTPClient.Jar, err = cookiejar.New(nil)
require.NoError(t, err)
@@ -819,52 +882,58 @@ func TestUserOIDC(t *testing.T) {
})
require.NoError(t, err)
- resp := oidcCallbackWithState(t, user, code, convertResponse.StateString)
- require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
+ fake.LoginWithClient(t, user, claims, func(r *http.Request) {
+ r.URL.RawQuery = url.Values{
+ "oidc_merge_state": {convertResponse.StateString},
+ }.Encode()
+ r.Header.Set(codersdk.SessionTokenHeader, user.SessionToken())
+ cookies := user.HTTPClient.Jar.Cookies(r.URL)
+ for _, cookie := range cookies {
+ r.AddCookie(cookie)
+ }
+ })
})
t.Run("AlternateUsername", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
- conf := coderdtest.NewOIDCConfig(t, "")
-
- config := conf.OIDCConfig(t, nil)
- config.AllowSignups = true
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithRefreshHook(func(_ string) error {
+ return xerrors.New("refreshing token should never occur")
+ }),
+ oidctest.WithServing(),
+ )
+ cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ })
client := coderdtest.New(t, &coderdtest.Options{
Auditor: auditor,
- OIDCConfig: config,
+ OIDCConfig: cfg,
})
- numLogs := len(auditor.AuditLogs())
- code := conf.EncodeClaims(t, jwt.MapClaims{
+ numLogs := len(auditor.AuditLogs())
+ claims := jwt.MapClaims{
"email": "jon@coder.com",
- })
- resp := oidcCallback(t, client, code)
- numLogs++ // add an audit log for login
+ }
- assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
+ userClient, _ := fake.Login(t, client, claims)
+ numLogs++ // add an audit log for login
ctx := testutil.Context(t, testutil.WaitLong)
-
- client.SetSessionToken(authCookieValue(resp.Cookies()))
- user, err := client.User(ctx, "me")
+ user, err := userClient.User(ctx, "me")
require.NoError(t, err)
require.Equal(t, "jon", user.Username)
// Pass a different subject field so that we prompt creating a
- // new user.
- code = conf.EncodeClaims(t, jwt.MapClaims{
+ // new user
+ userClient, _ = fake.Login(t, client, jwt.MapClaims{
"email": "jon@example2.com",
"sub": "diff",
})
- resp = oidcCallback(t, client, code)
numLogs++ // add an audit log for login
- assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
-
- client.SetSessionToken(authCookieValue(resp.Cookies()))
- user, err = client.User(ctx, "me")
+ user, err = userClient.User(ctx, "me")
require.NoError(t, err)
require.True(t, strings.HasPrefix(user.Username, "jon-"), "username %q should have prefix %q", user.Username, "jon-")
@@ -875,45 +944,62 @@ func TestUserOIDC(t *testing.T) {
t.Run("Disabled", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
- resp := oidcCallback(t, client, "asdf")
+ oauthURL, err := client.URL.Parse("/api/v2/users/oidc/callback")
+ require.NoError(t, err)
+
+ req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
+ require.NoError(t, err)
+ resp, err := client.HTTPClient.Do(req)
+ require.NoError(t, err)
+ resp.Body.Close()
+
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("NoIDToken", func(t *testing.T) {
t.Parallel()
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithRefreshHook(func(_ string) error {
+ return xerrors.New("refreshing token should never occur")
+ }),
+ oidctest.WithServing(),
+ )
+ cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ })
+
client := coderdtest.New(t, &coderdtest.Options{
- OIDCConfig: &coderd.OIDCConfig{
- OAuth2Config: &testutil.OAuth2Config{},
- },
+ OIDCConfig: cfg,
})
- resp := oidcCallback(t, client, "asdf")
+ _, resp := fake.AttemptLogin(t, client, jwt.MapClaims{})
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("BadVerify", func(t *testing.T) {
t.Parallel()
- verifier := oidc.NewVerifier("", &oidc.StaticKeySet{
+ badVerifier := oidc.NewVerifier("", &oidc.StaticKeySet{
PublicKeys: []crypto.PublicKey{},
}, &oidc.Config{})
- provider := &oidc.Provider{}
+ badProvider := &oidc.Provider{}
+
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithRefreshHook(func(_ string) error {
+ return xerrors.New("refreshing token should never occur")
+ }),
+ oidctest.WithServing(),
+ )
+ cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.Provider = badProvider
+ cfg.Verifier = badVerifier
+ })
client := coderdtest.New(t, &coderdtest.Options{
- OIDCConfig: &coderd.OIDCConfig{
- OAuth2Config: &testutil.OAuth2Config{
- Token: (&oauth2.Token{
- AccessToken: "token",
- }).WithExtra(map[string]interface{}{
- "id_token": "invalid",
- }),
- },
- Provider: provider,
- Verifier: verifier,
- },
+ OIDCConfig: cfg,
})
- resp := oidcCallback(t, client, "asdf")
-
+ _, resp := fake.AttemptLogin(t, client, jwt.MapClaims{})
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
@@ -1044,33 +1130,6 @@ func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response {
return res
}
-func oidcCallback(t *testing.T, client *codersdk.Client, code string) *http.Response {
- return oidcCallbackWithState(t, client, code, "somestate")
-}
-
-func oidcCallbackWithState(t *testing.T, client *codersdk.Client, code, state string) *http.Response {
- t.Helper()
-
- client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
- return http.ErrUseLastResponse
- }
- oauthURL, err := client.URL.Parse(fmt.Sprintf("/api/v2/users/oidc/callback?code=%s&state=%s", code, state))
- require.NoError(t, err)
- req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
- require.NoError(t, err)
- req.AddCookie(&http.Cookie{
- Name: codersdk.OAuth2StateCookie,
- Value: state,
- })
- res, err := client.HTTPClient.Do(req)
- require.NoError(t, err)
- defer res.Body.Close()
- data, err := io.ReadAll(res.Body)
- require.NoError(t, err)
- t.Log(string(data))
- return res
-}
-
func i64ptr(i int64) *int64 {
return &i
}
diff --git a/coderd/userpassword/hashing_bench_test.go b/coderd/userpassword/hashing_bench_test.go
index 109a1724cbf06..7b1d8cb7e9449 100644
--- a/coderd/userpassword/hashing_bench_test.go
+++ b/coderd/userpassword/hashing_bench_test.go
@@ -7,7 +7,7 @@ import (
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/cryptorand"
)
var (
diff --git a/coderd/userpassword/userpassword_test.go b/coderd/userpassword/userpassword_test.go
index 6976a94e1c0a6..1617748d5ada1 100644
--- a/coderd/userpassword/userpassword_test.go
+++ b/coderd/userpassword/userpassword_test.go
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/userpassword"
+ "github.com/coder/coder/v2/coderd/userpassword"
)
func TestUserPassword(t *testing.T) {
diff --git a/coderd/users.go b/coderd/users.go
index 017e20d408586..ef9c9cd5679f4 100644
--- a/coderd/users.go
+++ b/coderd/users.go
@@ -12,19 +12,19 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/gitsshkey"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/searchquery"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/coderd/userpassword"
- "github.com/coder/coder/coderd/util/slice"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/gitsshkey"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/searchquery"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/coderd/userpassword"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/codersdk"
)
// Returns whether the initial user has been created or not.
@@ -287,11 +287,27 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
+ if req.UserLoginType == "" && req.DisableLogin {
+ // Handle the deprecated field
+ req.UserLoginType = codersdk.LoginTypeNone
+ }
+ if req.UserLoginType == "" {
+ // Default to password auth
+ req.UserLoginType = codersdk.LoginTypePassword
+ }
+
+ if req.UserLoginType != codersdk.LoginTypePassword && req.Password != "" {
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: fmt.Sprintf("Password cannot be set for non-password (%q) authentication.", req.UserLoginType),
+ })
+ return
+ }
+
// If password auth is disabled, don't allow new users to be
// created with a password!
- if api.DeploymentValues.DisablePasswordAuth {
+ if api.DeploymentValues.DisablePasswordAuth && req.UserLoginType == codersdk.LoginTypePassword {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
- Message: "You cannot manually provision new users with password authentication disabled!",
+ Message: "Password based authentication is disabled! Unable to provision new users with password authentication.",
})
return
}
@@ -353,17 +369,11 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
}
}
- if req.DisableLogin && req.Password != "" {
- httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: "Cannot set password when disabling login.",
- })
- return
- }
-
var loginType database.LoginType
- if req.DisableLogin {
+ switch req.UserLoginType {
+ case codersdk.LoginTypeNone:
loginType = database.LoginTypeNone
- } else {
+ case codersdk.LoginTypePassword:
err = userpassword.Validate(req.Password)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -376,6 +386,14 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}
loginType = database.LoginTypePassword
+ case codersdk.LoginTypeOIDC:
+ loginType = database.LoginTypeOIDC
+ case codersdk.LoginTypeGithub:
+ loginType = database.LoginTypeGithub
+ default:
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: fmt.Sprintf("Unsupported login type %q for manually creating new users.", req.UserLoginType),
+ })
}
user, _, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
@@ -733,6 +751,13 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
return
}
+ if user.LoginType != database.LoginTypePassword {
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: "Users without password login type cannot change their password.",
+ })
+ return
+ }
+
err := userpassword.Validate(params.Password)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -1070,7 +1095,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
_, err = tx.InsertAllUsersGroup(ctx, organization.ID)
if err != nil {
- return xerrors.Errorf("create %q group: %w", database.AllUsersGroup, err)
+ return xerrors.Errorf("create %q group: %w", database.EveryoneGroup, err)
}
}
diff --git a/coderd/users_test.go b/coderd/users_test.go
index eff3174ad83a2..60e6ddb82aecf 100644
--- a/coderd/users_test.go
+++ b/coderd/users_test.go
@@ -4,24 +4,29 @@ import (
"context"
"fmt"
"net/http"
- "sort"
"strings"
"testing"
"time"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/coderdtest/oidctest"
+
+ "github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestFirstUser(t *testing.T) {
@@ -401,6 +406,7 @@ func TestPostLogout(t *testing.T) {
})
}
+// nolint:bodyclose
func TestPostUsers(t *testing.T) {
t.Parallel()
t.Run("NoAuth", func(t *testing.T) {
@@ -565,6 +571,65 @@ func TestPostUsers(t *testing.T) {
}
}
})
+
+ t.Run("CreateNoneLoginType", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, nil)
+ first := coderdtest.CreateFirstUser(t, client)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ user, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
+ OrganizationID: first.OrganizationID,
+ Email: "another@user.org",
+ Username: "someone-else",
+ Password: "",
+ UserLoginType: codersdk.LoginTypeNone,
+ })
+ require.NoError(t, err)
+
+ found, err := client.User(ctx, user.ID.String())
+ require.NoError(t, err)
+ require.Equal(t, found.LoginType, codersdk.LoginTypeNone)
+ })
+
+ t.Run("CreateOIDCLoginType", func(t *testing.T) {
+ t.Parallel()
+ email := "another@user.org"
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithServing(),
+ )
+ cfg := fake.OIDCConfig(t, nil, func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ })
+
+ client := coderdtest.New(t, &coderdtest.Options{
+ OIDCConfig: cfg,
+ })
+ first := coderdtest.CreateFirstUser(t, client)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ _, err := client.CreateUser(ctx, codersdk.CreateUserRequest{
+ OrganizationID: first.OrganizationID,
+ Email: email,
+ Username: "someone-else",
+ Password: "",
+ UserLoginType: codersdk.LoginTypeOIDC,
+ })
+ require.NoError(t, err)
+
+ // Try to log in with OIDC.
+ userClient, _ := fake.Login(t, client, jwt.MapClaims{
+ "email": email,
+ })
+
+ found, err := userClient.User(ctx, "me")
+ require.NoError(t, err)
+ require.Equal(t, found.LoginType, codersdk.LoginTypeOIDC)
+ })
}
func TestUpdateUserProfile(t *testing.T) {
@@ -1804,8 +1869,8 @@ func assertPagination(ctx context.Context, t *testing.T, client *codersdk.Client
// sortUsers sorts by (created_at, id)
func sortUsers(users []codersdk.User) {
- sort.Slice(users, func(i, j int) bool {
- return strings.ToLower(users[i].Username) < strings.ToLower(users[j].Username)
+ slices.SortFunc(users, func(a, b codersdk.User) int {
+ return slice.Ascending(strings.ToLower(a.Username), strings.ToLower(b.Username))
})
}
diff --git a/coderd/util/ptr/ptr_test.go b/coderd/util/ptr/ptr_test.go
index 2dee346c8f5e4..355b32fc5cd68 100644
--- a/coderd/util/ptr/ptr_test.go
+++ b/coderd/util/ptr/ptr_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/coder/coder/coderd/util/ptr"
+ "github.com/coder/coder/v2/coderd/util/ptr"
)
func Test_Ref_Deref(t *testing.T) {
diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go
index 9909fe2b72c21..c366b04f91d8d 100644
--- a/coderd/util/slice/slice.go
+++ b/coderd/util/slice/slice.go
@@ -1,5 +1,9 @@
package slice
+import (
+ "golang.org/x/exp/constraints"
+)
+
// SameElements returns true if the 2 lists have the same elements in any
// order.
func SameElements[T comparable](a []T, b []T) bool {
@@ -38,17 +42,19 @@ func Overlap[T comparable](a []T, b []T) bool {
}
// Unique returns a new slice with all duplicate elements removed.
-// This is a slow function on large lists.
-// TODO: Sort elements and implement a faster search algorithm if we
-// really start to use this.
func Unique[T comparable](a []T) []T {
cpy := make([]T, 0, len(a))
+ seen := make(map[T]struct{}, len(a))
+
for _, v := range a {
- v := v
- if !Contains(cpy, v) {
- cpy = append(cpy, v)
+ if _, ok := seen[v]; ok {
+ continue
}
+
+ seen[v] = struct{}{}
+ cpy = append(cpy, v)
}
+
return cpy
}
@@ -67,3 +73,17 @@ func OverlapCompare[T any](a []T, b []T, equal func(a, b T) bool) bool {
func New[T any](items ...T) []T {
return items
}
+
+func Ascending[T constraints.Ordered](a, b T) int {
+ if a < b {
+ return -1
+ } else if a == b {
+ return 0
+ } else {
+ return 1
+ }
+}
+
+func Descending[T constraints.Ordered](a, b T) int {
+ return -Ascending[T](a, b)
+}
diff --git a/coderd/util/slice/slice_test.go b/coderd/util/slice/slice_test.go
index b21e0cc0b52a5..cf686f3de4a48 100644
--- a/coderd/util/slice/slice_test.go
+++ b/coderd/util/slice/slice_test.go
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/util/slice"
)
func TestSameElements(t *testing.T) {
@@ -107,3 +107,19 @@ func assertSetContains[T comparable](t *testing.T, set []T, in []T, out []T) {
require.False(t, slice.Contains(set, e), "expect element in set")
}
}
+
+func TestAscending(t *testing.T) {
+ t.Parallel()
+
+ assert.Equal(t, -1, slice.Ascending(1, 2))
+ assert.Equal(t, 0, slice.Ascending(1, 1))
+ assert.Equal(t, 1, slice.Ascending(2, 1))
+}
+
+func TestDescending(t *testing.T) {
+ t.Parallel()
+
+ assert.Equal(t, 1, slice.Descending(1, 2))
+ assert.Equal(t, 0, slice.Descending(1, 1))
+ assert.Equal(t, -1, slice.Descending(2, 1))
+}
diff --git a/coderd/util/strings/strings_test.go b/coderd/util/strings/strings_test.go
index a5db8ebbf8734..a107a7754fc7f 100644
--- a/coderd/util/strings/strings_test.go
+++ b/coderd/util/strings/strings_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/util/strings"
+ "github.com/coder/coder/v2/coderd/util/strings"
)
func TestJoinWithConjunction(t *testing.T) {
diff --git a/coderd/util/syncmap/map.go b/coderd/util/syncmap/map.go
new file mode 100644
index 0000000000000..d245973efa844
--- /dev/null
+++ b/coderd/util/syncmap/map.go
@@ -0,0 +1,77 @@
+package syncmap
+
+import "sync"
+
+// Map is a type safe sync.Map
+type Map[K, V any] struct {
+ m sync.Map
+}
+
+func New[K, V any]() *Map[K, V] {
+ return &Map[K, V]{
+ m: sync.Map{},
+ }
+}
+
+func (m *Map[K, V]) Store(k K, v V) {
+ m.m.Store(k, v)
+}
+
+//nolint:forcetypeassert
+func (m *Map[K, V]) Load(key K) (value V, ok bool) {
+ v, ok := m.m.Load(key)
+ if !ok {
+ var empty V
+ return empty, false
+ }
+ return v.(V), ok
+}
+
+func (m *Map[K, V]) Delete(key K) {
+ m.m.Delete(key)
+}
+
+//nolint:forcetypeassert
+func (m *Map[K, V]) LoadAndDelete(key K) (actual V, loaded bool) {
+ act, loaded := m.m.LoadAndDelete(key)
+ if !loaded {
+ var empty V
+ return empty, loaded
+ }
+ return act.(V), loaded
+}
+
+//nolint:forcetypeassert
+func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
+ act, loaded := m.m.LoadOrStore(key, value)
+ if !loaded {
+ var empty V
+ return empty, loaded
+ }
+ return act.(V), loaded
+}
+
+func (m *Map[K, V]) CompareAndSwap(key K, old V, new V) bool {
+ return m.m.CompareAndSwap(key, old, new)
+}
+
+func (m *Map[K, V]) CompareAndDelete(key K, old V) (deleted bool) {
+ return m.m.CompareAndDelete(key, old)
+}
+
+//nolint:forcetypeassert
+func (m *Map[K, V]) Swap(key K, value V) (previous any, loaded bool) {
+ previous, loaded = m.m.Swap(key, value)
+ if !loaded {
+ var empty V
+ return empty, loaded
+ }
+ return previous.(V), loaded
+}
+
+//nolint:forcetypeassert
+func (m *Map[K, V]) Range(f func(key K, value V) bool) {
+ m.m.Range(func(key, value interface{}) bool {
+ return f(key.(K), value.(V))
+ })
+}
diff --git a/coderd/util/tz/tz_test.go b/coderd/util/tz/tz_test.go
index f70046837064f..a0e7971bd7492 100644
--- a/coderd/util/tz/tz_test.go
+++ b/coderd/util/tz/tz_test.go
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/util/tz"
+ "github.com/coder/coder/v2/coderd/util/tz"
)
//nolint:paralleltest // Environment variables
diff --git a/coderd/util/xio/limitwriter_test.go b/coderd/util/xio/limitwriter_test.go
index 52d6075fbb7f3..f14c873e96422 100644
--- a/coderd/util/xio/limitwriter_test.go
+++ b/coderd/util/xio/limitwriter_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/util/xio"
+ "github.com/coder/coder/v2/coderd/util/xio"
)
func TestLimitWriter(t *testing.T) {
diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go
index 8567ff1d895b3..7a3b25eee96fc 100644
--- a/coderd/workspaceagents.go
+++ b/coderd/workspaceagents.go
@@ -6,7 +6,6 @@ import (
"database/sql"
"encoding/json"
"errors"
- "flag"
"fmt"
"io"
"net"
@@ -14,15 +13,16 @@ import (
"net/netip"
"net/url"
"runtime/pprof"
+ "sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
- "github.com/bep/debounce"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
+ "golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/mod/semver"
"golang.org/x/sync/errgroup"
@@ -31,16 +31,16 @@ import (
"tailscale.com/tailcfg"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/gitauth"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/gitauth"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/tailnet"
)
// @Summary Get workspace agent by ID
@@ -165,6 +165,7 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request)
AgentID: apiAgent.ID,
Apps: convertApps(dbApps),
DERPMap: api.DERPMap(),
+ DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
GitAuthConfigs: len(api.GitAuthConfigs),
EnvironmentVariables: apiAgent.EnvironmentVariables,
StartupScript: apiAgent.StartupScript,
@@ -209,7 +210,13 @@ func (api *API) postWorkspaceAgentStartup(rw http.ResponseWriter, r *http.Reques
return
}
- api.Logger.Info(ctx, "post workspace agent version", slog.F("agent_id", apiAgent.ID), slog.F("agent_version", req.Version))
+ api.Logger.Debug(
+ ctx,
+ "post workspace agent version",
+ slog.F("agent_id", apiAgent.ID),
+ slog.F("agent_version", req.Version),
+ slog.F("remote_addr", r.RemoteAddr),
+ )
if !semver.IsValid(req.Version) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -219,11 +226,31 @@ func (api *API) postWorkspaceAgentStartup(rw http.ResponseWriter, r *http.Reques
return
}
+ // Validate subsystems.
+ seen := make(map[codersdk.AgentSubsystem]bool)
+ for _, s := range req.Subsystems {
+ if !s.Valid() {
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: "Invalid workspace agent subsystem provided.",
+ Detail: fmt.Sprintf("invalid subsystem: %q", s),
+ })
+ return
+ }
+ if seen[s] {
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: "Invalid workspace agent subsystem provided.",
+ Detail: fmt.Sprintf("duplicate subsystem: %q", s),
+ })
+ return
+ }
+ seen[s] = true
+ }
+
if err := api.Database.UpdateWorkspaceAgentStartupByID(ctx, database.UpdateWorkspaceAgentStartupByIDParams{
ID: apiAgent.ID,
Version: req.Version,
ExpandedDirectory: req.ExpandedDirectory,
- Subsystem: convertWorkspaceAgentSubsystem(req.Subsystem),
+ Subsystems: convertWorkspaceAgentSubsystems(req.Subsystems),
}); err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Error setting agent version",
@@ -455,6 +482,15 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) {
return
}
+ workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID)
+ if err != nil {
+ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Internal error fetching workspace by agent id.",
+ Detail: err.Error(),
+ })
+ return
+ }
+
api.WebsocketWaitMutex.Lock()
api.WebsocketWaitGroup.Add(1)
api.WebsocketWaitMutex.Unlock()
@@ -530,7 +566,8 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) {
go func() {
defer close(bufferedLogs)
- for {
+ keepGoing := true
+ for keepGoing {
select {
case <-ctx.Done():
return
@@ -539,6 +576,18 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) {
t.Reset(recheckInterval)
}
+ agents, err := api.Database.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, workspace.ID)
+ if err != nil {
+ if xerrors.Is(err, context.Canceled) {
+ return
+ }
+ logger.Warn(ctx, "failed to get workspace agents in latest build", slog.Error(err))
+ continue
+ }
+ // If the agent is no longer in the latest build, we can stop after
+ // checking once.
+ keepGoing = slices.ContainsFunc(agents, func(agent database.WorkspaceAgent) bool { return agent.ID == workspaceAgent.ID })
+
logs, err := api.Database.GetWorkspaceAgentLogsAfter(ctx, database.GetWorkspaceAgentLogsAfterParams{
AgentID: workspaceAgent.ID,
CreatedAfter: lastSentLogID,
@@ -705,10 +754,11 @@ func (api *API) _dialWorkspaceAgentTailnet(agentID uuid.UUID) (*codersdk.Workspa
derpMap := api.DERPMap()
conn, err := tailnet.NewConn(&tailnet.Options{
- Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
- DERPMap: api.DERPMap(),
- Logger: api.Logger.Named("net.tailnet"),
- BlockEndpoints: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
+ Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
+ DERPMap: api.DERPMap(),
+ DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
+ Logger: api.Logger.Named("net.tailnet"),
+ BlockEndpoints: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
})
if err != nil {
_ = clientConn.Close()
@@ -803,6 +853,7 @@ func (api *API) workspaceAgentConnection(rw http.ResponseWriter, r *http.Request
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentConnectionInfo{
DERPMap: api.DERPMap(),
+ DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
})
}
@@ -823,6 +874,7 @@ func (api *API) workspaceAgentConnectionGeneric(rw http.ResponseWriter, r *http.
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentConnectionInfo{
DERPMap: api.DERPMap(),
+ DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(),
})
}
@@ -849,13 +901,15 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
})
return
}
- nconn := websocket.NetConn(ctx, ws, websocket.MessageBinary)
+ ctx, nconn := websocketNetConn(ctx, ws, websocket.MessageBinary)
defer nconn.Close()
// Slurp all packets from the connection into io.Discard so pongs get sent
- // by the websocket package.
+ // by the websocket package. We don't do any reads ourselves so this is
+ // necessary.
go func() {
_, _ = io.Copy(io.Discard, nconn)
+ _ = nconn.Close()
}()
go func(ctx context.Context) {
@@ -870,13 +924,11 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
return
}
- // We don't need a context that times out here because the ping will
- // eventually go through. If the context times out, then other
- // websocket read operations will receive an error, obfuscating the
- // actual problem.
+ ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
err := ws.Ping(ctx)
+ cancel()
if err != nil {
- _ = ws.Close(websocket.StatusInternalError, err.Error())
+ _ = nconn.Close()
return
}
}
@@ -891,7 +943,7 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
if lastDERPMap == nil || !tailnet.CompareDERPMaps(lastDERPMap, derpMap) {
err := json.NewEncoder(nconn).Encode(derpMap)
if err != nil {
- _ = ws.Close(websocket.StatusInternalError, err.Error())
+ _ = nconn.Close()
return
}
lastDERPMap = derpMap
@@ -1277,6 +1329,11 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
if dbAgent.TroubleshootingURL != "" {
troubleshootingURL = dbAgent.TroubleshootingURL
}
+ subsystems := make([]codersdk.AgentSubsystem, len(dbAgent.Subsystems))
+ for i, subsystem := range dbAgent.Subsystems {
+ subsystems[i] = codersdk.AgentSubsystem(subsystem)
+ }
+
workspaceAgent := codersdk.WorkspaceAgent{
ID: dbAgent.ID,
CreatedAt: dbAgent.CreatedAt,
@@ -1302,7 +1359,7 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin
LoginBeforeReady: dbAgent.StartupScriptBehavior != database.StartupScriptBehaviorBlocking,
ShutdownScript: dbAgent.ShutdownScript.String,
ShutdownScriptTimeoutSeconds: dbAgent.ShutdownScriptTimeoutSeconds,
- Subsystem: codersdk.AgentSubsystem(dbAgent.Subsystem),
+ Subsystems: subsystems,
}
node := coordinator.Node(dbAgent.ID)
if node != nil {
@@ -1410,36 +1467,12 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
activityBumpWorkspace(ctx, api.Logger.Named("activity_bump"), api.Database, workspace.ID)
}
- payload, err := json.Marshal(req.ConnectionsByProto)
- if err != nil {
- api.Logger.Error(ctx, "marshal agent connections by proto", slog.F("workspace_agent_id", workspaceAgent.ID), slog.Error(err))
- payload = json.RawMessage("{}")
- }
-
now := database.Now()
var errGroup errgroup.Group
errGroup.Go(func() error {
- _, err = api.Database.InsertWorkspaceAgentStat(ctx, database.InsertWorkspaceAgentStatParams{
- ID: uuid.New(),
- CreatedAt: now,
- AgentID: workspaceAgent.ID,
- WorkspaceID: workspace.ID,
- UserID: workspace.OwnerID,
- TemplateID: workspace.TemplateID,
- ConnectionsByProto: payload,
- ConnectionCount: req.ConnectionCount,
- RxPackets: req.RxPackets,
- RxBytes: req.RxBytes,
- TxPackets: req.TxPackets,
- TxBytes: req.TxBytes,
- SessionCountVSCode: req.SessionCountVSCode,
- SessionCountJetBrains: req.SessionCountJetBrains,
- SessionCountReconnectingPTY: req.SessionCountReconnectingPTY,
- SessionCountSSH: req.SessionCountSSH,
- ConnectionMedianLatencyMS: req.ConnectionMedianLatencyMS,
- })
- if err != nil {
+ if err := api.statsBatcher.Add(time.Now(), workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, req); err != nil {
+ api.Logger.Error(ctx, "failed to add stats to batcher", slog.Error(err))
return xerrors.Errorf("can't insert workspace agent stat: %w", err)
}
return nil
@@ -1476,6 +1509,13 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
})
}
+func ellipse(v string, n int) string {
+ if len(v) > n {
+ return v[:n] + "..."
+ }
+ return v
+}
+
// @Summary Submit workspace agent metadata
// @ID submit-workspace-agent-metadata
// @Security CoderSessionToken
@@ -1508,7 +1548,11 @@ func (api *API) workspaceAgentPostMetadata(rw http.ResponseWriter, r *http.Reque
key := chi.URLParam(r, "key")
const (
- maxValueLen = 32 << 10
+ // maxValueLen is set to 2048 to stay under the 8000 byte Postgres
+ // NOTIFY limit. Since both value and error can be set, the real
+ // payload limit is 2 * 2048 * 4/3 = 5461 bytes + a few hundred bytes for JSON
+ // syntax, key names, and metadata.
+ maxValueLen = 2048
maxErrorLen = maxValueLen
)
@@ -1548,9 +1592,16 @@ func (api *API) workspaceAgentPostMetadata(rw http.ResponseWriter, r *http.Reque
slog.F("workspace_id", workspace.ID),
slog.F("collected_at", datum.CollectedAt),
slog.F("key", datum.Key),
+ slog.F("value", ellipse(datum.Value, 16)),
)
- err = api.Pubsub.Publish(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), []byte(datum.Key))
+ datumJSON, err := json.Marshal(datum)
+ if err != nil {
+ httpapi.InternalServerError(rw, err)
+ return
+ }
+
+ err = api.Pubsub.Publish(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), datumJSON)
if err != nil {
httpapi.InternalServerError(rw, err)
return
@@ -1571,9 +1622,47 @@ func (api *API) watchWorkspaceAgentMetadata(rw http.ResponseWriter, r *http.Requ
var (
ctx = r.Context()
workspaceAgent = httpmw.WorkspaceAgentParam(r)
+ log = api.Logger.Named("workspace_metadata_watcher").With(
+ slog.F("workspace_agent_id", workspaceAgent.ID),
+ )
+ )
+
+ // We avoid channel-based synchronization here to avoid backpressure problems.
+ var (
+ metadataMapMu sync.Mutex
+ metadataMap = make(map[string]database.WorkspaceAgentMetadatum)
+ // pendingChanges must only be mutated when metadataMapMu is held.
+ pendingChanges atomic.Bool
)
- sendEvent, senderClosed, err := httpapi.ServerSentEventSender(rw, r)
+ // Send metadata on updates, we must ensure subscription before sending
+ // initial metadata to guarantee that events in-between are not missed.
+ cancelSub, err := api.Pubsub.Subscribe(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), func(_ context.Context, byt []byte) {
+ var update database.UpdateWorkspaceAgentMetadataParams
+ err := json.Unmarshal(byt, &update)
+ if err != nil {
+ api.Logger.Error(ctx, "failed to unmarshal pubsub message", slog.Error(err))
+ return
+ }
+
+ log.Debug(ctx, "received metadata update", "key", update.Key)
+
+ metadataMapMu.Lock()
+ defer metadataMapMu.Unlock()
+ md := metadataMap[update.Key]
+ md.Value = update.Value
+ md.Error = update.Error
+ md.CollectedAt = update.CollectedAt
+ metadataMap[update.Key] = md
+ pendingChanges.Store(true)
+ })
+ if err != nil {
+ httpapi.InternalServerError(rw, err)
+ return
+ }
+ defer cancelSub()
+
+ sseSendEvent, sseSenderClosed, err := httpapi.ServerSentEventSender(rw, r)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error setting up server-sent events.",
@@ -1583,87 +1672,61 @@ func (api *API) watchWorkspaceAgentMetadata(rw http.ResponseWriter, r *http.Requ
}
// Prevent handler from returning until the sender is closed.
defer func() {
- <-senderClosed
+ <-sseSenderClosed
}()
- const refreshInterval = time.Second * 5
- refreshTicker := time.NewTicker(refreshInterval)
- defer refreshTicker.Stop()
+ // We send updates exactly every second.
+ const sendInterval = time.Second * 1
+ sendTicker := time.NewTicker(sendInterval)
+ defer sendTicker.Stop()
- var (
- lastDBMetaMu sync.Mutex
- lastDBMeta []database.WorkspaceAgentMetadatum
- )
+ // We always use the original Request context because it contains
+ // the RBAC actor.
+ md, err := api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID)
+ if err != nil {
+ // If we can't successfully pull the initial metadata, pubsub
+ // updates will be no-op so we may as well terminate the
+ // connection early.
+ httpapi.InternalServerError(rw, err)
+ return
+ }
- sendMetadata := func(pull bool) {
- lastDBMetaMu.Lock()
- defer lastDBMetaMu.Unlock()
+ metadataMapMu.Lock()
+ for _, datum := range md {
+ metadataMap[datum.Key] = datum
+ }
+ metadataMapMu.Unlock()
- var err error
- if pull {
- // We always use the original Request context because it contains
- // the RBAC actor.
- lastDBMeta, err = api.Database.GetWorkspaceAgentMetadata(ctx, workspaceAgent.ID)
- if err != nil {
- _ = sendEvent(ctx, codersdk.ServerSentEvent{
- Type: codersdk.ServerSentEventTypeError,
- Data: codersdk.Response{
- Message: "Internal error getting metadata.",
- Detail: err.Error(),
- },
- })
- return
- }
- slices.SortFunc(lastDBMeta, func(i, j database.WorkspaceAgentMetadatum) bool {
- return i.Key < j.Key
- })
+ // Send initial metadata.
- // Avoid sending refresh if the client is about to get a
- // fresh update.
- refreshTicker.Reset(refreshInterval)
- }
+ var lastSend time.Time
+ sendMetadata := func() {
+ metadataMapMu.Lock()
+ values := maps.Values(metadataMap)
+ pendingChanges.Store(false)
+ metadataMapMu.Unlock()
- _ = sendEvent(ctx, codersdk.ServerSentEvent{
+ lastSend = time.Now()
+ _ = sseSendEvent(ctx, codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeData,
- Data: convertWorkspaceAgentMetadata(lastDBMeta),
+ Data: convertWorkspaceAgentMetadata(values),
})
}
- // We debounce metadata updates to avoid overloading the frontend when
- // an agent is sending a lot of updates.
- pubsubDebounce := debounce.New(time.Second)
- if flag.Lookup("test.v") != nil {
- pubsubDebounce = debounce.New(time.Millisecond * 100)
- }
-
- // Send metadata on updates, we must ensure subscription before sending
- // initial metadata to guarantee that events in-between are not missed.
- cancelSub, err := api.Pubsub.Subscribe(watchWorkspaceAgentMetadataChannel(workspaceAgent.ID), func(_ context.Context, _ []byte) {
- pubsubDebounce(func() {
- sendMetadata(true)
- })
- })
- if err != nil {
- httpapi.InternalServerError(rw, err)
- return
- }
- defer cancelSub()
-
- // Send initial metadata.
- sendMetadata(true)
+ sendMetadata()
for {
select {
- case <-senderClosed:
+ case <-sendTicker.C:
+ // We send an update even if there's no change every 5 seconds
+ // to ensure that the frontend always has an accurate "Result.Age".
+ if !pendingChanges.Load() && time.Since(lastSend) < time.Second*5 {
+ continue
+ }
+ sendMetadata()
+ case <-sseSenderClosed:
return
- case <-refreshTicker.C:
}
-
- // Avoid spamming the DB with reads we know there are no updates. We want
- // to continue sending updates to the frontend so that "Result.Age"
- // is always accurate. This way, the frontend doesn't need to
- // sync its own clock with the backend.
- sendMetadata(false)
}
}
@@ -1687,6 +1750,10 @@ func convertWorkspaceAgentMetadata(db []database.WorkspaceAgentMetadatum) []code
},
})
}
+ // Sorting prevents the metadata from jumping around in the frontend.
+ sort.Slice(result, func(i, j int) bool {
+ return result[i].Description.Key < result[j].Description.Key
+ })
return result
}
@@ -2138,11 +2205,23 @@ func convertWorkspaceAgentLog(logEntry database.WorkspaceAgentLog) codersdk.Work
}
}
-func convertWorkspaceAgentSubsystem(ss codersdk.AgentSubsystem) database.WorkspaceAgentSubsystem {
- switch ss {
- case codersdk.AgentSubsystemEnvbox:
- return database.WorkspaceAgentSubsystemEnvbox
- default:
- return database.WorkspaceAgentSubsystemNone
+func convertWorkspaceAgentSubsystems(ss []codersdk.AgentSubsystem) []database.WorkspaceAgentSubsystem {
+ out := make([]database.WorkspaceAgentSubsystem, 0, len(ss))
+ for _, s := range ss {
+ switch s {
+ case codersdk.AgentSubsystemEnvbox:
+ out = append(out, database.WorkspaceAgentSubsystemEnvbox)
+ case codersdk.AgentSubsystemEnvbuilder:
+ out = append(out, database.WorkspaceAgentSubsystemEnvbuilder)
+ case codersdk.AgentSubsystemExectrace:
+ out = append(out, database.WorkspaceAgentSubsystemExectrace)
+ default:
+ // Invalid, drop it.
+ }
}
+
+ sort.Slice(out, func(i, j int) bool {
+ return out[i] < out[j]
+ })
+ return out
}
diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go
index 12cb19efac012..2e51687afafa6 100644
--- a/coderd/workspaceagents_test.go
+++ b/coderd/workspaceagents_test.go
@@ -20,15 +20,15 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/tailnet/tailnettest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/tailnet/tailnettest"
+ "github.com/coder/coder/v2/testutil"
)
func TestWorkspaceAgent(t *testing.T) {
@@ -43,10 +43,10 @@ func TestWorkspaceAgent(t *testing.T) {
tmpDir := t.TempDir()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -87,10 +87,10 @@ func TestWorkspaceAgent(t *testing.T) {
tmpDir := t.TempDir()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -132,10 +132,10 @@ func TestWorkspaceAgent(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -188,10 +188,10 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -242,6 +242,91 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) {
require.Equal(t, "testing", logChunk[0].Output)
require.Equal(t, "testing2", logChunk[1].Output)
})
+ t.Run("Close logs on outdated build", func(t *testing.T) {
+ t.Parallel()
+ ctx := testutil.Context(t, testutil.WaitMedium)
+ client := coderdtest.New(t, &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ })
+ user := coderdtest.CreateFirstUser(t, client)
+ authToken := uuid.NewString()
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
+ Resources: []*proto.Resource{{
+ Name: "example",
+ Type: "aws_instance",
+ Agents: []*proto.Agent{{
+ Id: uuid.NewString(),
+ Auth: &proto.Agent_Token{
+ Token: authToken,
+ },
+ }},
+ }},
+ },
+ },
+ }},
+ })
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
+
+ agentClient := agentsdk.New(client.URL)
+ agentClient.SetSessionToken(authToken)
+ err := agentClient.PatchLogs(ctx, agentsdk.PatchLogs{
+ Logs: []agentsdk.Log{
+ {
+ CreatedAt: database.Now(),
+ Output: "testing",
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ logs, closer, err := client.WorkspaceAgentLogsAfter(ctx, build.Resources[0].Agents[0].ID, 0, true)
+ require.NoError(t, err)
+ defer func() {
+ _ = closer.Close()
+ }()
+
+ first := make(chan struct{})
+ go func() {
+ select {
+ case <-ctx.Done():
+ assert.Fail(t, "context done while waiting in goroutine")
+ case <-logs:
+ close(first)
+ }
+ }()
+ select {
+ case <-ctx.Done():
+ require.FailNow(t, "context done while waiting for first log")
+ case <-first:
+ }
+
+ _ = coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
+
+ // Send a new log message to trigger a re-check.
+ err = agentClient.PatchLogs(ctx, agentsdk.PatchLogs{
+ Logs: []agentsdk.Log{
+ {
+ CreatedAt: database.Now(),
+ Output: "testing2",
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ select {
+ case <-ctx.Done():
+ require.FailNow(t, "context done while waiting for logs close")
+ case <-logs:
+ }
+ })
t.Run("PublishesOnOverflow", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
@@ -252,10 +337,10 @@ func TestWorkspaceAgentStartupLogs(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -319,7 +404,7 @@ func TestWorkspaceAgentListen(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -360,7 +445,7 @@ func TestWorkspaceAgentListen(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
@@ -371,10 +456,10 @@ func TestWorkspaceAgentListen(t *testing.T) {
version = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -406,7 +491,9 @@ func TestWorkspaceAgentListen(t *testing.T) {
_, err = agentClient.Listen(ctx)
require.Error(t, err)
- require.ErrorContains(t, err, "build is outdated")
+ var sdkErr *codersdk.Error
+ require.ErrorAs(t, err, &sdkErr)
+ require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
})
}
@@ -417,7 +504,7 @@ func TestWorkspaceAgentTailnet(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -469,7 +556,7 @@ func TestWorkspaceAgentTailnetDirectDisabled(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -546,10 +633,10 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -804,9 +891,9 @@ func TestWorkspaceAgentAppHealth(t *testing.T) {
}
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -891,7 +978,7 @@ func TestWorkspaceAgentReportStats(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -940,7 +1027,7 @@ func TestWorkspaceAgent_LifecycleState(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -1012,10 +1099,10 @@ func TestWorkspaceAgent_Metadata(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -1151,7 +1238,7 @@ func TestWorkspaceAgent_Metadata(t *testing.T) {
require.Len(t, update, 3)
check(wantMetadata1, update[0], true)
- const maxValueLen = 32 << 10
+ const maxValueLen = 2048
tooLongValueMetadata := wantMetadata1
tooLongValueMetadata.Value = strings.Repeat("a", maxValueLen*2)
tooLongValueMetadata.Error = ""
@@ -1182,7 +1269,7 @@ func TestWorkspaceAgent_Startup(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -1195,16 +1282,23 @@ func TestWorkspaceAgent_Startup(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium)
- const (
- expectedVersion = "v1.2.3"
- expectedDir = "/home/coder"
- expectedSubsystem = codersdk.AgentSubsystemEnvbox
+ var (
+ expectedVersion = "v1.2.3"
+ expectedDir = "/home/coder"
+ expectedSubsystems = []codersdk.AgentSubsystem{
+ codersdk.AgentSubsystemEnvbox,
+ codersdk.AgentSubsystemExectrace,
+ }
)
err := agentClient.PostStartup(ctx, agentsdk.PostStartupRequest{
Version: expectedVersion,
ExpandedDirectory: expectedDir,
- Subsystem: expectedSubsystem,
+ Subsystems: []codersdk.AgentSubsystem{
+ // Not sorted.
+ expectedSubsystems[1],
+ expectedSubsystems[0],
+ },
})
require.NoError(t, err)
@@ -1215,7 +1309,8 @@ func TestWorkspaceAgent_Startup(t *testing.T) {
require.NoError(t, err)
require.Equal(t, expectedVersion, wsagent.Version)
require.Equal(t, expectedDir, wsagent.ExpandedDirectory)
- require.Equal(t, expectedSubsystem, wsagent.Subsystem)
+ // Sorted
+ require.Equal(t, expectedSubsystems, wsagent.Subsystems)
})
t.Run("InvalidSemver", func(t *testing.T) {
@@ -1228,7 +1323,7 @@ func TestWorkspaceAgent_Startup(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -1283,7 +1378,7 @@ func TestWorkspaceAgent_UpdatedDERP(t *testing.T) {
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(agentToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go
index df33889467b80..56567fb633ee4 100644
--- a/coderd/workspaceapps.go
+++ b/coderd/workspaceapps.go
@@ -11,14 +11,14 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/apikey"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/apikey"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Get applications host
diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go
index f64ba7c30bf31..7e15f188e70bc 100644
--- a/coderd/workspaceapps/apptest/apptest.go
+++ b/coderd/workspaceapps/apptest/apptest.go
@@ -15,6 +15,7 @@ import (
"runtime"
"strconv"
"strings"
+ "sync"
"testing"
"time"
@@ -23,11 +24,11 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
// Run runs the entire workspace app test suite against deployments minted
@@ -51,23 +52,8 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
t.Skip("ConPTY appears to be inconsistent on Windows.")
}
- expectLine := func(t *testing.T, r *bufio.Reader, matcher func(string) bool) {
- for {
- line, err := r.ReadString('\n')
- require.NoError(t, err)
- if matcher(line) {
- break
- }
- }
- }
- matchEchoCommand := func(line string) bool {
- return strings.Contains(line, "echo test")
- }
- matchEchoOutput := func(line string) bool {
- return strings.Contains(line, "test") && !strings.Contains(line, "echo")
- }
-
t.Run("OK", func(t *testing.T) {
+ t.Parallel()
appDetails := setupProxyTest(t, nil)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
@@ -76,40 +62,13 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
// Run the test against the path app hostname since that's where the
// reconnecting-pty proxy server we want to test is mounted.
client := appDetails.AppClient(t)
- conn, err := client.WorkspaceAgentReconnectingPTY(ctx, codersdk.WorkspaceAgentReconnectingPTYOpts{
+ testReconnectingPTY(ctx, t, client, codersdk.WorkspaceAgentReconnectingPTYOpts{
AgentID: appDetails.Agent.ID,
Reconnect: uuid.New(),
- Height: 80,
- Width: 80,
- Command: "/bin/bash",
+ Height: 100,
+ Width: 100,
+ Command: "bash",
})
- require.NoError(t, err)
- defer conn.Close()
-
- // First attempt to resize the TTY.
- // The websocket will close if it fails!
- data, err := json.Marshal(codersdk.ReconnectingPTYRequest{
- Height: 250,
- Width: 250,
- })
- require.NoError(t, err)
- _, err = conn.Write(data)
- require.NoError(t, err)
- bufRead := bufio.NewReader(conn)
-
- // Brief pause to reduce the likelihood that we send keystrokes while
- // the shell is simultaneously sending a prompt.
- time.Sleep(100 * time.Millisecond)
-
- data, err = json.Marshal(codersdk.ReconnectingPTYRequest{
- Data: "echo test\r\n",
- })
- require.NoError(t, err)
- _, err = conn.Write(data)
- require.NoError(t, err)
-
- expectLine(t, bufRead, matchEchoCommand)
- expectLine(t, bufRead, matchEchoOutput)
})
t.Run("SignedTokenQueryParameter", func(t *testing.T) {
@@ -137,41 +96,14 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
// Make an unauthenticated client.
unauthedAppClient := codersdk.New(appDetails.AppClient(t).URL)
- conn, err := unauthedAppClient.WorkspaceAgentReconnectingPTY(ctx, codersdk.WorkspaceAgentReconnectingPTYOpts{
+ testReconnectingPTY(ctx, t, unauthedAppClient, codersdk.WorkspaceAgentReconnectingPTYOpts{
AgentID: appDetails.Agent.ID,
Reconnect: uuid.New(),
- Height: 80,
- Width: 80,
- Command: "/bin/bash",
+ Height: 100,
+ Width: 100,
+ Command: "bash",
SignedToken: issueRes.SignedToken,
})
- require.NoError(t, err)
- defer conn.Close()
-
- // First attempt to resize the TTY.
- // The websocket will close if it fails!
- data, err := json.Marshal(codersdk.ReconnectingPTYRequest{
- Height: 250,
- Width: 250,
- })
- require.NoError(t, err)
- _, err = conn.Write(data)
- require.NoError(t, err)
- bufRead := bufio.NewReader(conn)
-
- // Brief pause to reduce the likelihood that we send keystrokes while
- // the shell is simultaneously sending a prompt.
- time.Sleep(100 * time.Millisecond)
-
- data, err = json.Marshal(codersdk.ReconnectingPTYRequest{
- Data: "echo test\r\n",
- })
- require.NoError(t, err)
- _, err = conn.Write(data)
- require.NoError(t, err)
-
- expectLine(t, bufRead, matchEchoCommand)
- expectLine(t, bufRead, matchEchoOutput)
})
})
@@ -1406,4 +1338,123 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, []string{"Origin", "X-Foobar"}, deduped)
require.Equal(t, []string{"baz"}, resp.Header.Values("X-Foobar"))
})
+
+ t.Run("ReportStats", func(t *testing.T) {
+ t.Parallel()
+
+ flush := make(chan chan<- struct{}, 1)
+
+ reporter := &fakeStatsReporter{}
+ appDetails := setupProxyTest(t, &DeploymentOptions{
+ StatsCollectorOptions: workspaceapps.StatsCollectorOptions{
+ Reporter: reporter,
+ ReportInterval: time.Hour,
+ RollupWindow: time.Minute,
+
+ Flush: flush,
+ },
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ u := appDetails.PathAppURL(appDetails.Apps.Owner)
+ resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+ _, err = io.Copy(io.Discard, resp.Body)
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var stats []workspaceapps.StatsReport
+ require.Eventually(t, func() bool {
+ // Keep flushing until we get a non-empty stats report.
+ flushDone := make(chan struct{}, 1)
+ flush <- flushDone
+ <-flushDone
+
+ stats = reporter.stats()
+ return len(stats) > 0
+ }, testutil.WaitLong, testutil.IntervalFast, "stats not reported")
+
+ assert.Equal(t, workspaceapps.AccessMethodPath, stats[0].AccessMethod)
+ assert.Equal(t, "test-app-owner", stats[0].SlugOrPort)
+ assert.Equal(t, 1, stats[0].Requests)
+ })
+}
+
+type fakeStatsReporter struct {
+ mu sync.Mutex
+ s []workspaceapps.StatsReport
+}
+
+func (r *fakeStatsReporter) stats() []workspaceapps.StatsReport {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ return r.s
+}
+
+func (r *fakeStatsReporter) Report(_ context.Context, stats []workspaceapps.StatsReport) error {
+ r.mu.Lock()
+ r.s = append(r.s, stats...)
+ r.mu.Unlock()
+ return nil
+}
+
+func testReconnectingPTY(ctx context.Context, t *testing.T, client *codersdk.Client, opts codersdk.WorkspaceAgentReconnectingPTYOpts) {
+ matchEchoCommand := func(line string) bool {
+ return strings.Contains(line, "echo test")
+ }
+ matchEchoOutput := func(line string) bool {
+ return strings.Contains(line, "test") && !strings.Contains(line, "echo")
+ }
+ matchExitCommand := func(line string) bool {
+ return strings.Contains(line, "exit")
+ }
+ matchExitOutput := func(line string) bool {
+ return strings.Contains(line, "exit") || strings.Contains(line, "logout")
+ }
+
+ conn, err := client.WorkspaceAgentReconnectingPTY(ctx, opts)
+ require.NoError(t, err)
+ defer conn.Close()
+
+ // First attempt to resize the TTY.
+ // The websocket will close if it fails!
+ data, err := json.Marshal(codersdk.ReconnectingPTYRequest{
+ Height: 80,
+ Width: 80,
+ })
+ require.NoError(t, err)
+ _, err = conn.Write(data)
+ require.NoError(t, err)
+
+ // Brief pause to reduce the likelihood that we send keystrokes while
+ // the shell is simultaneously sending a prompt.
+ time.Sleep(100 * time.Millisecond)
+
+ data, err = json.Marshal(codersdk.ReconnectingPTYRequest{
+ Data: "echo test\r\n",
+ })
+ require.NoError(t, err)
+ _, err = conn.Write(data)
+ require.NoError(t, err)
+
+ require.NoError(t, testutil.ReadUntil(ctx, t, conn, matchEchoCommand), "find echo command")
+ require.NoError(t, testutil.ReadUntil(ctx, t, conn, matchEchoOutput), "find echo output")
+
+ // Exit should cause the connection to close.
+ data, err = json.Marshal(codersdk.ReconnectingPTYRequest{
+ Data: "exit\r\n",
+ })
+ require.NoError(t, err)
+ _, err = conn.Write(data)
+ require.NoError(t, err)
+
+ // Once for the input and again for the output.
+ require.NoError(t, testutil.ReadUntil(ctx, t, conn, matchExitCommand), "find exit command")
+ require.NoError(t, testutil.ReadUntil(ctx, t, conn, matchExitOutput), "find exit output")
+
+ // Ensure the connection closes.
+ require.ErrorIs(t, testutil.ReadUntil(ctx, t, conn, nil), io.EOF)
}
diff --git a/coderd/workspaceapps/apptest/setup.go b/coderd/workspaceapps/apptest/setup.go
index 4245204268391..6ab541f078072 100644
--- a/coderd/workspaceapps/apptest/setup.go
+++ b/coderd/workspaceapps/apptest/setup.go
@@ -19,13 +19,14 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
const (
@@ -51,6 +52,8 @@ type DeploymentOptions struct {
DangerousAllowPathAppSiteOwnerAccess bool
ServeHTTPS bool
+ StatsCollectorOptions workspaceapps.StatsCollectorOptions
+
// The following fields are only used by setupProxyTestWithFactory.
noWorkspace bool
port uint16
@@ -285,10 +288,10 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
appURL := fmt.Sprintf("%s://127.0.0.1:%d?%s", scheme, port, proxyTestAppQuery)
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
diff --git a/coderd/workspaceapps/db.go b/coderd/workspaceapps/db.go
index 34851fb1559e1..5f15b52d0e6f3 100644
--- a/coderd/workspaceapps/db.go
+++ b/coderd/workspaceapps/db.go
@@ -13,12 +13,12 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
// DBTokenProvider provides authentication and authorization for workspace apps
diff --git a/coderd/workspaceapps/db_test.go b/coderd/workspaceapps/db_test.go
index bab2d8ae3b9dd..163247f6d4e4f 100644
--- a/coderd/workspaceapps/db_test.go
+++ b/coderd/workspaceapps/db_test.go
@@ -18,15 +18,15 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func Test_ResolveRequest(t *testing.T) {
@@ -94,10 +94,10 @@ func Test_ResolveRequest(t *testing.T) {
agentAuthToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
diff --git a/coderd/workspaceapps/errors.go b/coderd/workspaceapps/errors.go
index b9724c6f79f69..0dd8cc270d3cb 100644
--- a/coderd/workspaceapps/errors.go
+++ b/coderd/workspaceapps/errors.go
@@ -5,7 +5,7 @@ import (
"net/url"
"cdr.dev/slog"
- "github.com/coder/coder/site"
+ "github.com/coder/coder/v2/site"
)
// WriteWorkspaceApp404 writes a HTML 404 error page for a workspace app. If
diff --git a/coderd/workspaceapps/provider.go b/coderd/workspaceapps/provider.go
index 62b6da02a6050..25c18effd5f7a 100644
--- a/coderd/workspaceapps/provider.go
+++ b/coderd/workspaceapps/provider.go
@@ -7,8 +7,8 @@ import (
"time"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
const (
diff --git a/coderd/workspaceapps/proxy.go b/coderd/workspaceapps/proxy.go
index 9b2d9c4bfa297..5e9babcb85048 100644
--- a/coderd/workspaceapps/proxy.go
+++ b/coderd/workspaceapps/proxy.go
@@ -18,13 +18,14 @@ import (
"nhooyr.io/websocket"
"cdr.dev/slog"
- "github.com/coder/coder/agent/agentssh"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/coderd/util/slice"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/site"
+ "github.com/coder/coder/v2/agent/agentssh"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/site"
)
const (
@@ -109,7 +110,8 @@ type Server struct {
DisablePathApps bool
SecureAuthCookie bool
- AgentProvider AgentProvider
+ AgentProvider AgentProvider
+ StatsCollector *StatsCollector
websocketWaitMutex sync.Mutex
websocketWaitGroup sync.WaitGroup
@@ -122,6 +124,10 @@ func (s *Server) Close() error {
s.websocketWaitGroup.Wait()
s.websocketWaitMutex.Unlock()
+ if s.StatsCollector != nil {
+ _ = s.StatsCollector.Close()
+ }
+
// The caller must close the SignedTokenProvider and the AgentProvider (if
// necessary).
@@ -586,6 +592,14 @@ func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appT
// end span so we don't get long lived trace data
tracing.EndHTTPSpan(r, http.StatusOK, trace.SpanFromContext(ctx))
+ report := newStatsReportFromSignedToken(appToken)
+ s.collectStats(report)
+ defer func() {
+ // We must use defer here because ServeHTTP may panic.
+ report.SessionEndedAt = database.Now()
+ s.collectStats(report)
+ }()
+
proxy.ServeHTTP(rw, r)
}
@@ -669,7 +683,6 @@ func (s *Server) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
return
}
defer release()
- defer agentConn.Close()
log.Debug(ctx, "dialed workspace agent")
ptNetConn, err := agentConn.ReconnectingPTY(ctx, reconnect, uint16(height), uint16(width), r.URL.Query().Get("command"))
if err != nil {
@@ -679,10 +692,24 @@ func (s *Server) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
}
defer ptNetConn.Close()
log.Debug(ctx, "obtained PTY")
+
+ report := newStatsReportFromSignedToken(*appToken)
+ s.collectStats(report)
+ defer func() {
+ report.SessionEndedAt = database.Now()
+ s.collectStats(report)
+ }()
+
agentssh.Bicopy(ctx, wsNetConn, ptNetConn)
log.Debug(ctx, "pty Bicopy finished")
}
+func (s *Server) collectStats(stats StatsReport) {
+ if s.StatsCollector != nil {
+ s.StatsCollector.Collect(stats)
+ }
+}
+
// wsNetConn wraps net.Conn created by websocket.NetConn(). Cancel func
// is called if a read or write error is encountered.
type wsNetConn struct {
diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go
index e9d0ff9ffcc3a..fbfa010aeda40 100644
--- a/coderd/workspaceapps/request.go
+++ b/coderd/workspaceapps/request.go
@@ -12,8 +12,8 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
)
type AccessMethod string
diff --git a/coderd/workspaceapps/request_test.go b/coderd/workspaceapps/request_test.go
index e6fb0279d3ecd..03ecccd4d7dc1 100644
--- a/coderd/workspaceapps/request_test.go
+++ b/coderd/workspaceapps/request_test.go
@@ -6,7 +6,7 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/workspaceapps"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
)
func Test_RequestValidate(t *testing.T) {
diff --git a/coderd/workspaceapps/stats.go b/coderd/workspaceapps/stats.go
new file mode 100644
index 0000000000000..c9b11117126c2
--- /dev/null
+++ b/coderd/workspaceapps/stats.go
@@ -0,0 +1,403 @@
+package workspaceapps
+
+import (
+ "context"
+ "sync"
+ "time"
+
+ "github.com/google/uuid"
+ "golang.org/x/xerrors"
+
+ "cdr.dev/slog"
+
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+)
+
+const (
+ DefaultStatsCollectorReportInterval = 30 * time.Second
+ DefaultStatsCollectorRollupWindow = 1 * time.Minute
+ DefaultStatsDBReporterBatchSize = 1024
+)
+
+// StatsReport is a report of a workspace app session.
+type StatsReport struct {
+ UserID uuid.UUID `json:"user_id"`
+ WorkspaceID uuid.UUID `json:"workspace_id"`
+ AgentID uuid.UUID `json:"agent_id"`
+ AccessMethod AccessMethod `json:"access_method"`
+ SlugOrPort string `json:"slug_or_port"`
+ SessionID uuid.UUID `json:"session_id"`
+ SessionStartedAt time.Time `json:"session_started_at"`
+ SessionEndedAt time.Time `json:"session_ended_at"` // Updated periodically while app is in use active and when the last connection is closed.
+ Requests int `json:"requests"`
+
+ rolledUp bool // Indicates if this report has been rolled up.
+}
+
+func newStatsReportFromSignedToken(token SignedToken) StatsReport {
+ return StatsReport{
+ UserID: token.UserID,
+ WorkspaceID: token.WorkspaceID,
+ AgentID: token.AgentID,
+ AccessMethod: token.AccessMethod,
+ SlugOrPort: token.AppSlugOrPort,
+ SessionID: uuid.New(),
+ SessionStartedAt: database.Now(),
+ Requests: 1,
+ }
+}
+
+// StatsReporter reports workspace app StatsReports.
+type StatsReporter interface {
+ Report(context.Context, []StatsReport) error
+}
+
+var _ StatsReporter = (*StatsDBReporter)(nil)
+
+// StatsDBReporter writes workspace app StatsReports to the database.
+type StatsDBReporter struct {
+ db database.Store
+ batchSize int
+}
+
+// NewStatsDBReporter returns a new StatsDBReporter.
+func NewStatsDBReporter(db database.Store, batchSize int) *StatsDBReporter {
+ return &StatsDBReporter{
+ db: db,
+ batchSize: batchSize,
+ }
+}
+
+// Report writes the given StatsReports to the database.
+func (r *StatsDBReporter) Report(ctx context.Context, stats []StatsReport) error {
+ err := r.db.InTx(func(tx database.Store) error {
+ maxBatchSize := r.batchSize
+ if len(stats) < maxBatchSize {
+ maxBatchSize = len(stats)
+ }
+ batch := database.InsertWorkspaceAppStatsParams{
+ UserID: make([]uuid.UUID, 0, maxBatchSize),
+ WorkspaceID: make([]uuid.UUID, 0, maxBatchSize),
+ AgentID: make([]uuid.UUID, 0, maxBatchSize),
+ AccessMethod: make([]string, 0, maxBatchSize),
+ SlugOrPort: make([]string, 0, maxBatchSize),
+ SessionID: make([]uuid.UUID, 0, maxBatchSize),
+ SessionStartedAt: make([]time.Time, 0, maxBatchSize),
+ SessionEndedAt: make([]time.Time, 0, maxBatchSize),
+ Requests: make([]int32, 0, maxBatchSize),
+ }
+ for _, stat := range stats {
+ batch.UserID = append(batch.UserID, stat.UserID)
+ batch.WorkspaceID = append(batch.WorkspaceID, stat.WorkspaceID)
+ batch.AgentID = append(batch.AgentID, stat.AgentID)
+ batch.AccessMethod = append(batch.AccessMethod, string(stat.AccessMethod))
+ batch.SlugOrPort = append(batch.SlugOrPort, stat.SlugOrPort)
+ batch.SessionID = append(batch.SessionID, stat.SessionID)
+ batch.SessionStartedAt = append(batch.SessionStartedAt, stat.SessionStartedAt)
+ batch.SessionEndedAt = append(batch.SessionEndedAt, stat.SessionEndedAt)
+ batch.Requests = append(batch.Requests, int32(stat.Requests))
+
+ if len(batch.UserID) >= r.batchSize {
+ err := tx.InsertWorkspaceAppStats(ctx, batch)
+ if err != nil {
+ return err
+ }
+
+ // Reset batch.
+ batch.UserID = batch.UserID[:0]
+ batch.WorkspaceID = batch.WorkspaceID[:0]
+ batch.AgentID = batch.AgentID[:0]
+ batch.AccessMethod = batch.AccessMethod[:0]
+ batch.SlugOrPort = batch.SlugOrPort[:0]
+ batch.SessionID = batch.SessionID[:0]
+ batch.SessionStartedAt = batch.SessionStartedAt[:0]
+ batch.SessionEndedAt = batch.SessionEndedAt[:0]
+ batch.Requests = batch.Requests[:0]
+ }
+ }
+ if len(batch.UserID) > 0 {
+ err := tx.InsertWorkspaceAppStats(ctx, batch)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }, nil)
+ if err != nil {
+ return xerrors.Errorf("insert workspace app stats failed: %w", err)
+ }
+
+ return nil
+}
+
+// This should match the database unique constraint.
+type statsGroupKey struct {
+ StartTimeTrunc time.Time
+ UserID uuid.UUID
+ WorkspaceID uuid.UUID
+ AgentID uuid.UUID
+ AccessMethod AccessMethod
+ SlugOrPort string
+}
+
+func (s StatsReport) groupKey(windowSize time.Duration) statsGroupKey {
+ return statsGroupKey{
+ StartTimeTrunc: s.SessionStartedAt.Truncate(windowSize),
+ UserID: s.UserID,
+ WorkspaceID: s.WorkspaceID,
+ AgentID: s.AgentID,
+ AccessMethod: s.AccessMethod,
+ SlugOrPort: s.SlugOrPort,
+ }
+}
+
+// StatsCollector collects workspace app StatsReports and reports them
+// in batches, stats compaction is performed for short-lived sessions.
+type StatsCollector struct {
+ opts StatsCollectorOptions
+
+ ctx context.Context
+ cancel context.CancelFunc
+ done chan struct{}
+
+ mu sync.Mutex // Protects following.
+ statsBySessionID map[uuid.UUID]*StatsReport // Track unique sessions.
+ groupedStats map[statsGroupKey][]*StatsReport // Rolled up stats for sessions in close proximity.
+ backlog []StatsReport // Stats that have not been reported yet (due to error).
+}
+
+type StatsCollectorOptions struct {
+ Logger *slog.Logger
+ Reporter StatsReporter
+ // ReportInterval is the interval at which stats are reported, both partial
+ // and fully formed stats.
+ ReportInterval time.Duration
+ // RollupWindow is the window size for rolling up stats, session shorter
+ // than this will be rolled up and longer than this will be tracked
+ // individually.
+ RollupWindow time.Duration
+
+ // Options for tests.
+ Flush <-chan chan<- struct{}
+ Now func() time.Time
+}
+
+func NewStatsCollector(opts StatsCollectorOptions) *StatsCollector {
+ if opts.Logger == nil {
+ opts.Logger = &slog.Logger{}
+ }
+ if opts.ReportInterval == 0 {
+ opts.ReportInterval = DefaultStatsCollectorReportInterval
+ }
+ if opts.RollupWindow == 0 {
+ opts.RollupWindow = DefaultStatsCollectorRollupWindow
+ }
+ if opts.Now == nil {
+ opts.Now = time.Now
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ sc := &StatsCollector{
+ ctx: ctx,
+ cancel: cancel,
+ done: make(chan struct{}),
+ opts: opts,
+
+ statsBySessionID: make(map[uuid.UUID]*StatsReport),
+ groupedStats: make(map[statsGroupKey][]*StatsReport),
+ }
+
+ go sc.start()
+ return sc
+}
+
+// Collect the given StatsReport for later reporting (non-blocking).
+func (sc *StatsCollector) Collect(report StatsReport) {
+ sc.mu.Lock()
+ defer sc.mu.Unlock()
+
+ r := &report
+ if _, ok := sc.statsBySessionID[report.SessionID]; !ok {
+ groupKey := r.groupKey(sc.opts.RollupWindow)
+ sc.groupedStats[groupKey] = append(sc.groupedStats[groupKey], r)
+ }
+
+ if r.SessionEndedAt.IsZero() {
+ sc.statsBySessionID[report.SessionID] = r
+ } else {
+ if stat, ok := sc.statsBySessionID[report.SessionID]; ok {
+ // Update in-place.
+ *stat = *r
+ }
+ delete(sc.statsBySessionID, report.SessionID)
+ }
+}
+
+// rollup performs stats rollup for sessions that fall within the
+// configured rollup window. For sessions longer than the window,
+// we report them individually.
+func (sc *StatsCollector) rollup(now time.Time) []StatsReport {
+ sc.mu.Lock()
+ defer sc.mu.Unlock()
+
+ var report []StatsReport
+
+ for g, group := range sc.groupedStats {
+ if len(group) == 0 {
+ // Safety check, this should not happen.
+ sc.opts.Logger.Error(sc.ctx, "empty stats group", "group", g)
+ delete(sc.groupedStats, g)
+ continue
+ }
+
+ var rolledUp *StatsReport
+ if group[0].rolledUp {
+ rolledUp = group[0]
+ group = group[1:]
+ } else {
+ rolledUp = &StatsReport{
+ UserID: g.UserID,
+ WorkspaceID: g.WorkspaceID,
+ AgentID: g.AgentID,
+ AccessMethod: g.AccessMethod,
+ SlugOrPort: g.SlugOrPort,
+ SessionStartedAt: g.StartTimeTrunc,
+ SessionEndedAt: g.StartTimeTrunc.Add(sc.opts.RollupWindow),
+ Requests: 0,
+ rolledUp: true,
+ }
+ }
+ rollupChanged := false
+ newGroup := []*StatsReport{rolledUp} // Must be first in slice for future iterations (see group[0] above).
+ for _, stat := range group {
+ if !stat.SessionEndedAt.IsZero() && stat.SessionEndedAt.Sub(stat.SessionStartedAt) <= sc.opts.RollupWindow {
+ // This is a short-lived session, roll it up.
+ if rolledUp.SessionID == uuid.Nil {
+ rolledUp.SessionID = stat.SessionID // Borrow the first session ID, useful in tests.
+ }
+ rolledUp.Requests += stat.Requests
+ rollupChanged = true
+ continue
+ }
+ if stat.SessionEndedAt.IsZero() && now.Sub(stat.SessionStartedAt) <= sc.opts.RollupWindow {
+ // This is an incomplete session, wait and see if it'll be rolled up or not.
+ newGroup = append(newGroup, stat)
+ continue
+ }
+
+ // This is a long-lived session, report it individually.
+ // Make a copy of stat for reporting.
+ r := *stat
+ if r.SessionEndedAt.IsZero() {
+ // Report an end time for incomplete sessions, it will
+ // be updated later. This ensures that data in the DB
+ // will have an end time even if the service is stopped.
+ r.SessionEndedAt = now.UTC() // Use UTC like database.Now().
+ }
+ report = append(report, r) // Report it (ended or incomplete).
+ if stat.SessionEndedAt.IsZero() {
+ newGroup = append(newGroup, stat) // Keep it for future updates.
+ }
+ }
+ if rollupChanged {
+ report = append(report, *rolledUp)
+ }
+
+ // Future rollups should only consider the compacted group.
+ sc.groupedStats[g] = newGroup
+
+ // Keep the group around until the next rollup window has passed
+ // in case data was collected late.
+ if len(newGroup) == 1 && rolledUp.SessionEndedAt.Add(sc.opts.RollupWindow).Before(now) {
+ delete(sc.groupedStats, g)
+ }
+ }
+
+ return report
+}
+
+func (sc *StatsCollector) flush(ctx context.Context) (err error) {
+ sc.opts.Logger.Debug(ctx, "flushing workspace app stats")
+ defer func() {
+ if err != nil {
+ sc.opts.Logger.Error(ctx, "failed to flush workspace app stats", "error", err)
+ } else {
+ sc.opts.Logger.Debug(ctx, "flushed workspace app stats")
+ }
+ }()
+
+ // We keep the backlog as a simple slice so that we don't need to
+ // attempt to merge it with the stats we're about to report. This
+ // is because the rollup is a one-way operation and the backlog may
+ // contain stats that are still in the statsBySessionID map and will
+ // be reported again in the future. It is possible to merge the
+ // backlog and the stats we're about to report, but it's not worth
+ // the complexity.
+ if len(sc.backlog) > 0 {
+ err = sc.opts.Reporter.Report(ctx, sc.backlog)
+ if err != nil {
+ return xerrors.Errorf("report workspace app stats from backlog failed: %w", err)
+ }
+ sc.backlog = nil
+ }
+
+ now := sc.opts.Now()
+ stats := sc.rollup(now)
+ if len(stats) == 0 {
+ return nil
+ }
+
+ err = sc.opts.Reporter.Report(ctx, stats)
+ if err != nil {
+ sc.backlog = stats
+ return xerrors.Errorf("report workspace app stats failed: %w", err)
+ }
+
+ return nil
+}
+
+func (sc *StatsCollector) Close() error {
+ sc.cancel()
+ <-sc.done
+ return nil
+}
+
+func (sc *StatsCollector) start() {
+ defer func() {
+ close(sc.done)
+ sc.opts.Logger.Debug(sc.ctx, "workspace app stats collector stopped")
+ }()
+ sc.opts.Logger.Debug(sc.ctx, "workspace app stats collector started")
+
+ t := time.NewTimer(sc.opts.ReportInterval)
+ defer t.Stop()
+
+ var reportFlushDone chan<- struct{}
+ done := false
+ for !done {
+ select {
+ case <-sc.ctx.Done():
+ t.Stop()
+ done = true
+ case <-t.C:
+ case reportFlushDone = <-sc.opts.Flush:
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ //nolint:gocritic // Inserting app stats is a system function.
+ _ = sc.flush(dbauthz.AsSystemRestricted(ctx))
+ cancel()
+
+ if !done {
+ t.Reset(sc.opts.ReportInterval)
+ }
+
+ // For tests.
+ if reportFlushDone != nil {
+ reportFlushDone <- struct{}{}
+ reportFlushDone = nil
+ }
+ }
+}
diff --git a/coderd/workspaceapps/stats_test.go b/coderd/workspaceapps/stats_test.go
new file mode 100644
index 0000000000000..bf8444c04f62e
--- /dev/null
+++ b/coderd/workspaceapps/stats_test.go
@@ -0,0 +1,426 @@
+package workspaceapps_test
+
+import (
+ "context"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/exp/slices"
+ "golang.org/x/xerrors"
+
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/testutil"
+)
+
+type fakeReporter struct {
+ mu sync.Mutex
+ s []workspaceapps.StatsReport
+ err error
+ errN int
+}
+
+func (r *fakeReporter) stats() []workspaceapps.StatsReport {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ return r.s
+}
+
+func (r *fakeReporter) errors() int {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ return r.errN
+}
+
+func (r *fakeReporter) setError(err error) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.err = err
+}
+
+func (r *fakeReporter) Report(_ context.Context, stats []workspaceapps.StatsReport) error {
+ r.mu.Lock()
+ if r.err != nil {
+ r.errN++
+ r.mu.Unlock()
+ return r.err
+ }
+ r.s = append(r.s, stats...)
+ r.mu.Unlock()
+ return nil
+}
+
+func TestStatsCollector(t *testing.T) {
+ t.Parallel()
+
+ rollupUUID := uuid.New()
+ rollupUUID2 := uuid.New()
+ someUUID := uuid.New()
+
+ rollupWindow := time.Minute
+ start := database.Now().Truncate(time.Minute).UTC()
+ end := start.Add(10 * time.Second)
+
+ tests := []struct {
+ name string
+ flushIncrement time.Duration
+ flushCount int
+ stats []workspaceapps.StatsReport
+ want []workspaceapps.StatsReport
+ }{
+ {
+ name: "Single stat rolled up and reported once",
+ flushIncrement: 2*rollupWindow + time.Second,
+ flushCount: 10, // Only reported once.
+ stats: []workspaceapps.StatsReport{
+ {
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: end,
+ Requests: 1,
+ },
+ },
+ want: []workspaceapps.StatsReport{
+ {
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow),
+ Requests: 1,
+ },
+ },
+ },
+ {
+ name: "Two unique stat rolled up",
+ flushIncrement: 2*rollupWindow + time.Second,
+ flushCount: 10, // Only reported once.
+ stats: []workspaceapps.StatsReport{
+ {
+ AccessMethod: workspaceapps.AccessMethodPath,
+ SlugOrPort: "code-server",
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: end,
+ Requests: 1,
+ },
+ {
+ AccessMethod: workspaceapps.AccessMethodTerminal,
+ SessionID: rollupUUID2,
+ SessionStartedAt: start,
+ SessionEndedAt: end,
+ Requests: 1,
+ },
+ },
+ want: []workspaceapps.StatsReport{
+ {
+ AccessMethod: workspaceapps.AccessMethodPath,
+ SlugOrPort: "code-server",
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow),
+ Requests: 1,
+ },
+ {
+ AccessMethod: workspaceapps.AccessMethodTerminal,
+ SessionID: rollupUUID2,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow),
+ Requests: 1,
+ },
+ },
+ },
+ {
+ name: "Multiple stats rolled up",
+ flushIncrement: 2*rollupWindow + time.Second,
+ flushCount: 2,
+ stats: []workspaceapps.StatsReport{
+ {
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: end,
+ Requests: 1,
+ },
+ {
+ SessionID: uuid.New(),
+ SessionStartedAt: start,
+ SessionEndedAt: end,
+ Requests: 1,
+ },
+ },
+ want: []workspaceapps.StatsReport{
+ {
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow),
+ Requests: 2,
+ },
+ },
+ },
+ {
+ name: "Long sessions not rolled up but reported multiple times",
+ flushIncrement: rollupWindow + time.Second,
+ flushCount: 4,
+ stats: []workspaceapps.StatsReport{
+ {
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ Requests: 1,
+ },
+ },
+ want: []workspaceapps.StatsReport{
+ {
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow + time.Second),
+ Requests: 1,
+ },
+ {
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(2 * (rollupWindow + time.Second)),
+ Requests: 1,
+ },
+ {
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(3 * (rollupWindow + time.Second)),
+ Requests: 1,
+ },
+ {
+ SessionID: rollupUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(4 * (rollupWindow + time.Second)),
+ Requests: 1,
+ },
+ },
+ },
+ {
+ name: "Incomplete stats not reported until it exceeds rollup window",
+ flushIncrement: rollupWindow / 4,
+ flushCount: 6,
+ stats: []workspaceapps.StatsReport{
+ {
+ SessionID: someUUID,
+ SessionStartedAt: start,
+ Requests: 1,
+ },
+ },
+ want: []workspaceapps.StatsReport{
+ {
+ SessionID: someUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow / 4 * 5),
+ Requests: 1,
+ },
+ {
+ SessionID: someUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow / 4 * 6),
+ Requests: 1,
+ },
+ },
+ },
+ {
+ name: "Same stat reported without and with end time and rolled up",
+ flushIncrement: rollupWindow + time.Second,
+ flushCount: 1,
+ stats: []workspaceapps.StatsReport{
+ {
+ SessionID: someUUID,
+ SessionStartedAt: start,
+ Requests: 1,
+ },
+ {
+ SessionID: someUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(10 * time.Second),
+ Requests: 1,
+ },
+ },
+ want: []workspaceapps.StatsReport{
+ {
+ SessionID: someUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow),
+ Requests: 1,
+ },
+ },
+ },
+ {
+ name: "Same non-rolled up stat reported without and with end time",
+ flushIncrement: rollupWindow * 2,
+ flushCount: 1,
+ stats: []workspaceapps.StatsReport{
+ {
+ SessionID: someUUID,
+ SessionStartedAt: start,
+ Requests: 1,
+ },
+ {
+ SessionID: someUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow * 2),
+ Requests: 1,
+ },
+ },
+ want: []workspaceapps.StatsReport{
+ {
+ SessionID: someUUID,
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(rollupWindow * 2),
+ Requests: 1,
+ },
+ },
+ },
+ }
+
+ // Run tests.
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ flush := make(chan chan<- struct{}, 1)
+ var now atomic.Pointer[time.Time]
+ now.Store(&start)
+
+ reporter := &fakeReporter{}
+ collector := workspaceapps.NewStatsCollector(workspaceapps.StatsCollectorOptions{
+ Reporter: reporter,
+ ReportInterval: time.Hour,
+ RollupWindow: rollupWindow,
+
+ Flush: flush,
+ Now: func() time.Time { return *now.Load() },
+ })
+
+ // Collect reports.
+ for _, report := range tt.stats {
+ collector.Collect(report)
+ }
+
+ // Advance time.
+ flushTime := start.Add(tt.flushIncrement)
+ for i := 0; i < tt.flushCount; i++ {
+ now.Store(&flushTime)
+ flushDone := make(chan struct{}, 1)
+ flush <- flushDone
+ <-flushDone
+ flushTime = flushTime.Add(tt.flushIncrement)
+ }
+
+ var gotStats []workspaceapps.StatsReport
+ require.Eventually(t, func() bool {
+ gotStats = reporter.stats()
+ return len(gotStats) == len(tt.want)
+ }, testutil.WaitMedium, testutil.IntervalFast)
+
+ // Order is not guaranteed.
+ sortBySessionID := func(a, b workspaceapps.StatsReport) int {
+ if a.SessionID == b.SessionID {
+ return int(a.SessionEndedAt.Sub(b.SessionEndedAt))
+ }
+ if a.SessionID.String() < b.SessionID.String() {
+ return -1
+ }
+ return 1
+ }
+ slices.SortFunc(tt.want, sortBySessionID)
+ slices.SortFunc(gotStats, sortBySessionID)
+
+ // Verify reported stats.
+ for i, got := range gotStats {
+ want := tt.want[i]
+
+ assert.Equal(t, want.SessionID, got.SessionID, "session ID; i = %d", i)
+ assert.Equal(t, want.SessionStartedAt, got.SessionStartedAt, "session started at; i = %d", i)
+ assert.Equal(t, want.SessionEndedAt, got.SessionEndedAt, "session ended at; i = %d", i)
+ assert.Equal(t, want.Requests, got.Requests, "requests; i = %d", i)
+ }
+ })
+ }
+}
+
+func TestStatsCollector_backlog(t *testing.T) {
+ t.Parallel()
+
+ rollupWindow := time.Minute
+ flush := make(chan chan<- struct{}, 1)
+
+ start := database.Now().Truncate(time.Minute).UTC()
+ var now atomic.Pointer[time.Time]
+ now.Store(&start)
+
+ reporter := &fakeReporter{}
+ collector := workspaceapps.NewStatsCollector(workspaceapps.StatsCollectorOptions{
+ Reporter: reporter,
+ ReportInterval: time.Hour,
+ RollupWindow: rollupWindow,
+
+ Flush: flush,
+ Now: func() time.Time { return *now.Load() },
+ })
+
+ reporter.setError(xerrors.New("some error"))
+
+ // The first collected stat is "rolled up" and moved into the
+ // backlog during the first flush. On the second flush nothing is
+ // rolled up due to being unable to report the backlog.
+ for i := 0; i < 2; i++ {
+ collector.Collect(workspaceapps.StatsReport{
+ SessionID: uuid.New(),
+ SessionStartedAt: start,
+ SessionEndedAt: start.Add(10 * time.Second),
+ Requests: 1,
+ })
+ start = start.Add(time.Minute)
+ now.Store(&start)
+
+ flushDone := make(chan struct{}, 1)
+ flush <- flushDone
+ <-flushDone
+ }
+
+ // Flush was performed 2 times, 2 reports should have failed.
+ wantErrors := 2
+ assert.Equal(t, wantErrors, reporter.errors())
+ assert.Empty(t, reporter.stats())
+
+ reporter.setError(nil)
+
+ // Flush again, this time the backlog should be reported in addition
+ // to the second collected stat being rolled up and reported.
+ flushDone := make(chan struct{}, 1)
+ flush <- flushDone
+ <-flushDone
+
+ assert.Equal(t, wantErrors, reporter.errors())
+ assert.Len(t, reporter.stats(), 2)
+}
+
+func TestStatsCollector_Close(t *testing.T) {
+ t.Parallel()
+
+ reporter := &fakeReporter{}
+ collector := workspaceapps.NewStatsCollector(workspaceapps.StatsCollectorOptions{
+ Reporter: reporter,
+ ReportInterval: time.Hour,
+ RollupWindow: time.Minute,
+ })
+
+ collector.Collect(workspaceapps.StatsReport{
+ SessionID: uuid.New(),
+ SessionStartedAt: database.Now(),
+ SessionEndedAt: database.Now(),
+ Requests: 1,
+ })
+
+ collector.Close()
+
+ // Verify that stats are reported after close.
+ assert.NotEmpty(t, reporter.stats())
+}
diff --git a/coderd/workspaceapps/token.go b/coderd/workspaceapps/token.go
index 3811602a2314c..96181340fc147 100644
--- a/coderd/workspaceapps/token.go
+++ b/coderd/workspaceapps/token.go
@@ -11,8 +11,8 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
)
const (
diff --git a/coderd/workspaceapps/token_test.go b/coderd/workspaceapps/token_test.go
index 8b20180cabaf0..e674fedb9cbf7 100644
--- a/coderd/workspaceapps/token_test.go
+++ b/coderd/workspaceapps/token_test.go
@@ -9,10 +9,10 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/cryptorand"
)
func Test_TokenMatchesRequest(t *testing.T) {
diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go
index 8f31bed4d27d2..2018e1d8dde4e 100644
--- a/coderd/workspaceapps_test.go
+++ b/coderd/workspaceapps_test.go
@@ -9,16 +9,16 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/coderd/workspaceapps/apptest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/coderd/workspaceapps/apptest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestGetAppHost(t *testing.T) {
@@ -275,6 +275,7 @@ func TestWorkspaceApps(t *testing.T) {
"CF-Connecting-IP",
},
},
+ WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions,
})
user := coderdtest.CreateFirstUser(t, client)
diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go
index c9156506b30c1..6f03d63dff785 100644
--- a/coderd/workspacebuilds.go
+++ b/coderd/workspacebuilds.go
@@ -16,14 +16,14 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/wsbuilder"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/wsbuilder"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Get workspace build
@@ -303,7 +303,6 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
// @Param request body codersdk.CreateWorkspaceBuildRequest true "Create workspace build request"
// @Success 200 {object} codersdk.WorkspaceBuild
// @Router /workspaces/{workspace}/builds [post]
-// nolint:gocyclo
func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go
index b838e39e3b251..2c32f9ac39b7a 100644
--- a/coderd/workspacebuilds_test.go
+++ b/coderd/workspacebuilds_test.go
@@ -14,14 +14,16 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "cdr.dev/slog"
+ "cdr.dev/slog/sloggers/slogtest"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func TestWorkspaceBuild(t *testing.T) {
@@ -376,12 +378,12 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -401,26 +403,30 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
require.Eventually(t, func() bool {
var err error
build, err = client.WorkspaceBuild(ctx, build.ID)
+ // job gets marked Failed when there is an Error; in practice we never get to Status = Canceled
+ // because provisioners report an Error when canceled. We check the Error string to ensure we don't mask
+ // other errors in this test.
return assert.NoError(t, err) &&
- // The job will never actually cancel successfully because it will never send a
- // provision complete response.
- assert.Empty(t, build.Job.Error) &&
- build.Job.Status == codersdk.ProvisionerJobCanceling
+ build.Job.Error == "canceled" &&
+ build.Job.Status == codersdk.ProvisionerJobFailed
}, testutil.WaitShort, testutil.IntervalFast)
})
t.Run("User is not allowed to cancel", func(t *testing.T) {
t.Parallel()
- client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ // need to include our own logger because the provisioner (rightly) drops error logs when we shut down the
+ // test with a build in progress.
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Logger: &logger})
owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
@@ -452,9 +458,9 @@ func TestWorkspaceBuildResources(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -494,16 +500,16 @@ func TestWorkspaceBuildLogs(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "example",
},
},
}, {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -548,10 +554,10 @@ func TestWorkspaceBuildState(t *testing.T) {
wantState := []byte("some kinda state")
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
State: wantState,
},
},
@@ -764,31 +770,31 @@ func TestWorkspaceBuildDebugMode(t *testing.T) {
// Interact as template admin
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_DEBUG,
Output: "want-it",
},
},
}, {
- Type: &proto.Provision_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_TRACE,
Output: "dont-want-it",
},
},
}, {
- Type: &proto.Provision_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_DEBUG,
Output: "done",
},
},
}, {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
},
}},
}
@@ -831,7 +837,10 @@ func TestWorkspaceBuildDebugMode(t *testing.T) {
if !ok {
break processingLogs
}
-
+ t.Logf("got log: %s -- %s | %s | %s", log.Level, log.Stage, log.Source, log.Output)
+ if log.Source != "provisioner" {
+ continue
+ }
logsProcessed++
require.NotEqual(t, "dont-want-it", log.Output, "unexpected log message", "%s log message shouldn't be logged: %s")
@@ -841,7 +850,6 @@ func TestWorkspaceBuildDebugMode(t *testing.T) {
}
}
}
-
- require.Len(t, echoResponses.ProvisionApply, logsProcessed)
+ require.Equal(t, 2, logsProcessed)
})
}
diff --git a/coderd/workspaceproxies.go b/coderd/workspaceproxies.go
index f861233507cf6..fca096819575f 100644
--- a/coderd/workspaceproxies.go
+++ b/coderd/workspaceproxies.go
@@ -8,10 +8,10 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
// PrimaryRegion exposes the user facing values of a workspace proxy to
diff --git a/coderd/workspaceproxies_test.go b/coderd/workspaceproxies_test.go
index d11ab8fbdd975..60718f8a22277 100644
--- a/coderd/workspaceproxies_test.go
+++ b/coderd/workspaceproxies_test.go
@@ -6,10 +6,10 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestRegions(t *testing.T) {
diff --git a/coderd/workspaceresourceauth.go b/coderd/workspaceresourceauth.go
index b43434c1d6c0f..3642822b18d77 100644
--- a/coderd/workspaceresourceauth.go
+++ b/coderd/workspaceresourceauth.go
@@ -5,14 +5,14 @@ import (
"fmt"
"net/http"
- "github.com/coder/coder/coderd/awsidentity"
- "github.com/coder/coder/coderd/azureidentity"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/provisionerdserver"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
+ "github.com/coder/coder/v2/coderd/awsidentity"
+ "github.com/coder/coder/v2/coderd/azureidentity"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/provisionerdserver"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/mitchellh/mapstructure"
)
diff --git a/coderd/workspaceresourceauth_test.go b/coderd/workspaceresourceauth_test.go
index d6c088de58507..fdf1bd2335034 100644
--- a/coderd/workspaceresourceauth_test.go
+++ b/coderd/workspaceresourceauth_test.go
@@ -7,12 +7,12 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) {
@@ -26,9 +26,9 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
@@ -71,9 +71,9 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
@@ -157,9 +157,9 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
diff --git a/coderd/workspaces.go b/coderd/workspaces.go
index 0aa1cb0675155..9384d2b7ecb00 100644
--- a/coderd/workspaces.go
+++ b/coderd/workspaces.go
@@ -15,19 +15,19 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/searchquery"
- "github.com/coder/coder/coderd/telemetry"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/coderd/wsbuilder"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/searchquery"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/coderd/wsbuilder"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
)
var (
@@ -86,6 +86,11 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
return
}
+ if len(data.templates) == 0 {
+ httpapi.Forbidden(rw)
+ return
+ }
+
httpapi.Write(ctx, rw, http.StatusOK, convertWorkspace(
workspace,
data.builds[0],
@@ -760,46 +765,43 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent)
}
-// @Summary Update workspace lock by id.
-// @ID update-workspace-lock-by-id
+// @Summary Update workspace dormancy status by id.
+// @ID update-workspace-dormancy-status-by-id
// @Security CoderSessionToken
// @Accept json
// @Produce json
// @Tags Workspaces
// @Param workspace path string true "Workspace ID" format(uuid)
-// @Param request body codersdk.UpdateWorkspaceLock true "Lock or unlock a workspace"
-// @Success 200 {object} codersdk.Response
-// @Router /workspaces/{workspace}/lock [put]
-func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) {
+// @Param request body codersdk.UpdateWorkspaceDormancy true "Make a workspace dormant or active"
+// @Success 200 {object} codersdk.Workspace
+// @Router /workspaces/{workspace}/dormant [put]
+func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
workspace := httpmw.WorkspaceParam(r)
- var req codersdk.UpdateWorkspaceLock
+ var req codersdk.UpdateWorkspaceDormancy
if !httpapi.Read(ctx, rw, r, &req) {
return
}
- code := http.StatusOK
- resp := codersdk.Response{}
-
// If the workspace is already in the desired state do nothing!
- if workspace.LockedAt.Valid == req.Lock {
+ if workspace.DormantAt.Valid == req.Dormant {
httpapi.Write(ctx, rw, http.StatusNotModified, codersdk.Response{
Message: "Nothing to do!",
})
return
}
- lockedAt := sql.NullTime{
- Valid: req.Lock,
+ dormantAt := sql.NullTime{
+ Valid: req.Dormant,
}
- if req.Lock {
- lockedAt.Time = database.Now()
+ if req.Dormant {
+ dormantAt.Time = database.Now()
}
- err := api.Database.UpdateWorkspaceLockedDeletingAt(ctx, database.UpdateWorkspaceLockedDeletingAtParams{
- ID: workspace.ID,
- LockedAt: lockedAt,
+ workspace, err := api.Database.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
+ ID: workspace.ID,
+ DormantAt: dormantAt,
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -809,10 +811,26 @@ func (api *API) putWorkspaceLock(rw http.ResponseWriter, r *http.Request) {
return
}
- // TODO should we kick off a build to stop the workspace if it's started
- // from this endpoint? I'm leaning no to keep things simple and kick
- // the responsibility back to the client.
- httpapi.Write(ctx, rw, code, resp)
+ data, err := api.workspaceData(ctx, []database.Workspace{workspace})
+ if err != nil {
+ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Internal error fetching workspace resources.",
+ Detail: err.Error(),
+ })
+ return
+ }
+
+ if len(data.templates) == 0 {
+ httpapi.Forbidden(rw)
+ return
+ }
+
+ httpapi.Write(ctx, rw, http.StatusOK, convertWorkspace(
+ workspace,
+ data.builds[0],
+ data.templates[0],
+ findUser(workspace.OwnerID, data.users),
+ ))
}
// @Summary Extend workspace deadline by ID
@@ -956,6 +974,16 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
})
return
}
+ if len(data.templates) == 0 {
+ _ = sendEvent(ctx, codersdk.ServerSentEvent{
+ Type: codersdk.ServerSentEventTypeError,
+ Data: codersdk.Response{
+ Message: "Forbidden reading template of selected workspace.",
+ Detail: err.Error(),
+ },
+ })
+ return
+ }
_ = sendEvent(ctx, codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeData,
@@ -1000,6 +1028,9 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
_ = sendEvent(ctx, codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypePing,
})
+ // Send updated workspace info after connection is established. This avoids
+ // missing updates if the client connects after an update.
+ sendUpdate(ctx, nil)
for {
select {
@@ -1017,6 +1048,10 @@ type workspaceData struct {
users []database.User
}
+// workspacesData only returns the data the caller can access. If the caller
+// does not have the correct perms to read a given template, the template will
+// not be returned.
+// So the caller must check the templates & users exist before using them.
func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspace) (workspaceData, error) {
workspaceIDs := make([]uuid.UUID, 0, len(workspaces))
templateIDs := make([]uuid.UUID, 0, len(workspaces))
@@ -1082,17 +1117,22 @@ func convertWorkspaces(workspaces []database.Workspace, data workspaceData) ([]c
apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
+ // If any data is missing from the workspace, just skip returning
+ // this workspace. This is not ideal, but the user cannot read
+ // all the workspace's data, so do not show them.
+ // Ideally we could just return some sort of "unknown" for the missing
+ // fields?
build, exists := buildByWorkspaceID[workspace.ID]
if !exists {
- return nil, xerrors.Errorf("build not found for workspace %q", workspace.Name)
+ continue
}
template, exists := templateByID[workspace.TemplateID]
if !exists {
- return nil, xerrors.Errorf("template not found for workspace %q", workspace.Name)
+ continue
}
owner, exists := userByID[workspace.OwnerID]
if !exists {
- return nil, xerrors.Errorf("owner not found for workspace: %q", workspace.Name)
+ continue
}
apiWorkspaces = append(apiWorkspaces, convertWorkspace(
@@ -1116,14 +1156,14 @@ func convertWorkspace(
autostartSchedule = &workspace.AutostartSchedule.String
}
- var lockedAt *time.Time
- if workspace.LockedAt.Valid {
- lockedAt = &workspace.LockedAt.Time
+ var dormantAt *time.Time
+ if workspace.DormantAt.Valid {
+ dormantAt = &workspace.DormantAt.Time
}
- var deletedAt *time.Time
+ var deletingAt *time.Time
if workspace.DeletingAt.Valid {
- deletedAt = &workspace.DeletingAt.Time
+ deletingAt = &workspace.DeletingAt.Time
}
failingAgents := []uuid.UUID{}
@@ -1150,13 +1190,14 @@ func convertWorkspace(
TemplateIcon: template.Icon,
TemplateDisplayName: template.DisplayName,
TemplateAllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ TemplateActiveVersionID: template.ActiveVersionID,
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),
Name: workspace.Name,
AutostartSchedule: autostartSchedule,
TTLMillis: ttlMillis,
LastUsedAt: workspace.LastUsedAt,
- DeletingAt: deletedAt,
- LockedAt: lockedAt,
+ DeletingAt: deletingAt,
+ DormantAt: dormantAt,
Health: codersdk.WorkspaceHealth{
Healthy: len(failingAgents) == 0,
FailingAgents: failingAgents,
diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go
index db5db020488b7..8da37158b1e3e 100644
--- a/coderd/workspaces_test.go
+++ b/coderd/workspaces_test.go
@@ -19,23 +19,23 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/parameter"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/parameter"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func TestWorkspace(t *testing.T) {
@@ -174,9 +174,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -214,9 +214,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -258,9 +258,9 @@ func TestWorkspace(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -1248,7 +1248,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -1276,7 +1276,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -1316,10 +1316,10 @@ func TestWorkspaceFilterManual(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -1363,9 +1363,9 @@ func TestWorkspaceFilterManual(t *testing.T) {
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
if atomic.AddInt64(&setCalled, 1) == 2 {
- assert.Equal(t, inactivityTTL, options.InactivityTTL)
+ assert.Equal(t, inactivityTTL, options.TimeTilDormant)
}
- template.InactivityTTL = int64(options.InactivityTTL)
+ template.TimeTilDormant = int64(options.TimeTilDormant)
return template, nil
},
},
@@ -1374,7 +1374,7 @@ func TestWorkspaceFilterManual(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
@@ -1385,11 +1385,11 @@ func TestWorkspaceFilterManual(t *testing.T) {
defer cancel()
template, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
- InactivityTTLMillis: inactivityTTL.Milliseconds(),
+ TimeTilDormantMillis: inactivityTTL.Milliseconds(),
})
assert.NoError(t, err)
- assert.Equal(t, inactivityTTL.Milliseconds(), template.InactivityTTLMillis)
+ assert.Equal(t, inactivityTTL.Milliseconds(), template.TimeTilDormantMillis)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
@@ -1404,9 +1404,105 @@ func TestWorkspaceFilterManual(t *testing.T) {
assert.NoError(t, err)
// we are expecting that no workspaces are returned as user is unlicensed
- // and template.InactivityTTL should be 0
+ // and template.TimeTilDormant should be 0
assert.Len(t, res.Workspaces, 0)
})
+
+ t.Run("DormantAt", func(t *testing.T) {
+ // this test has a licensed counterpart in enterprise/coderd/workspaces_test.go: FilterQueryHasDeletingByAndLicensed
+ t.Parallel()
+ client := coderdtest.New(t, &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ })
+ user := coderdtest.CreateFirstUser(t, client)
+ authToken := uuid.NewString()
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
+ })
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+
+ // update template with inactivity ttl
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ dormantWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWorkspace.LatestBuild.ID)
+
+ // Create another workspace to validate that we do not return active workspaces.
+ _ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWorkspace.LatestBuild.ID)
+
+ err := client.UpdateWorkspaceDormancy(ctx, dormantWorkspace.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: true,
+ })
+ require.NoError(t, err)
+
+ res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
+ FilterQuery: fmt.Sprintf("dormant_at:%s", time.Now().Add(-time.Minute).Format("2006-01-02")),
+ })
+ require.NoError(t, err)
+ require.Len(t, res.Workspaces, 1)
+ require.NotNil(t, res.Workspaces[0].DormantAt)
+ })
+
+ t.Run("LastUsed", func(t *testing.T) {
+ t.Parallel()
+ client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ })
+ user := coderdtest.CreateFirstUser(t, client)
+ authToken := uuid.NewString()
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
+ })
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+
+ // update template with inactivity ttl
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ now := database.Now()
+ before := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, before.LatestBuild.ID)
+
+ after := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, after.LatestBuild.ID)
+
+ //nolint:gocritic // Unit testing context
+ err := api.Database.UpdateWorkspaceLastUsedAt(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceLastUsedAtParams{
+ ID: before.ID,
+ LastUsedAt: now.UTC().Add(time.Hour * -1),
+ })
+ require.NoError(t, err)
+
+ // Unit testing context
+ //nolint:gocritic // Unit testing context
+ err = api.Database.UpdateWorkspaceLastUsedAt(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceLastUsedAtParams{
+ ID: after.ID,
+ LastUsedAt: now.UTC().Add(time.Hour * 1),
+ })
+ require.NoError(t, err)
+
+ beforeRes, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
+ FilterQuery: fmt.Sprintf("last_used_before:%q", now.Format(time.RFC3339)),
+ })
+ require.NoError(t, err)
+ require.Len(t, beforeRes.Workspaces, 1)
+ require.Equal(t, before.ID, beforeRes.Workspaces[0].ID)
+
+ afterRes, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
+ FilterQuery: fmt.Sprintf("last_used_after:%q", now.Format(time.RFC3339)),
+ })
+ require.NoError(t, err)
+ require.Len(t, afterRes.Workspaces, 1)
+ require.Equal(t, after.ID, afterRes.Workspaces[0].ID)
+ })
}
func TestOffsetLimit(t *testing.T) {
@@ -1479,7 +1575,7 @@ func TestPostWorkspaceBuild(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
- ProvisionApply: []*proto.Provision_Response{{}},
+ ProvisionApply: []*proto.Response{{}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
@@ -2042,10 +2138,10 @@ func TestWorkspaceWatcher(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -2081,9 +2177,14 @@ func TestWorkspaceWatcher(t *testing.T) {
case w, ok := <-wc:
require.True(t, ok, "watch channel closed: %s", event)
if ready == nil || ready(w) {
- logger.Info(ctx, "done waiting for event", slog.F("event", event))
+ logger.Info(ctx, "done waiting for event",
+ slog.F("event", event),
+ slog.F("workspace", w))
return
}
+ logger.Info(ctx, "skipped update for event",
+ slog.F("event", event),
+ slog.F("workspace", w))
}
}
}
@@ -2130,10 +2231,10 @@ func TestWorkspaceWatcher(t *testing.T) {
// Add a new version that will fail.
badVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Error: "test error",
},
},
@@ -2154,12 +2255,23 @@ func TestWorkspaceWatcher(t *testing.T) {
})
// We want to verify pending state here, but it's possible that we reach
// failed state fast enough that we never see pending.
+ sawFailed := false
wait("workspace build pending or failed", func(w codersdk.Workspace) bool {
- return w.LatestBuild.Status == codersdk.WorkspaceStatusPending || w.LatestBuild.Status == codersdk.WorkspaceStatusFailed
- })
- wait("workspace build failed", func(w codersdk.Workspace) bool {
- return w.LatestBuild.Status == codersdk.WorkspaceStatusFailed
+ switch w.LatestBuild.Status {
+ case codersdk.WorkspaceStatusPending:
+ return true
+ case codersdk.WorkspaceStatusFailed:
+ sawFailed = true
+ return true
+ default:
+ return false
+ }
})
+ if !sawFailed {
+ wait("workspace build failed", func(w codersdk.Workspace) bool {
+ return w.LatestBuild.Status == codersdk.WorkspaceStatusFailed
+ })
+ }
closeFunc.Close()
build := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
@@ -2187,9 +2299,9 @@ func TestWorkspaceResource(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "beta",
Type: "example",
@@ -2255,9 +2367,9 @@ func TestWorkspaceResource(t *testing.T) {
}
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -2312,9 +2424,9 @@ func TestWorkspaceResource(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
@@ -2385,10 +2497,10 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
+ ProvisionPlan: []*proto.Response{
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{
Name: firstParameterName,
@@ -2409,9 +2521,9 @@ func TestWorkspaceWithRichParameters(t *testing.T) {
},
},
},
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
},
}},
})
@@ -2478,10 +2590,10 @@ func TestWorkspaceWithOptionalRichParameters(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
+ ProvisionPlan: []*proto.Response{
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{
Name: firstParameterName,
@@ -2500,9 +2612,9 @@ func TestWorkspaceWithOptionalRichParameters(t *testing.T) {
},
},
},
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
},
}},
})
@@ -2569,10 +2681,10 @@ func TestWorkspaceWithEphemeralRichParameters(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{
+ ProvisionPlan: []*proto.Response{
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
Parameters: []*proto.RichParameter{
{
Name: firstParameterName,
@@ -2594,9 +2706,9 @@ func TestWorkspaceWithEphemeralRichParameters(t *testing.T) {
},
},
},
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
},
}},
})
@@ -2670,21 +2782,21 @@ func TestWorkspaceWithEphemeralRichParameters(t *testing.T) {
require.ElementsMatch(t, expectedBuildParameters, workspaceBuildParameters)
}
-func TestWorkspaceLock(t *testing.T) {
+func TestWorkspaceDormant(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
var (
- client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
- user = coderdtest.CreateFirstUser(t, client)
- version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
- _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
- lockedTTL = time.Minute
+ client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user = coderdtest.CreateFirstUser(t, client)
+ version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ timeTilDormantAutoDelete = time.Minute
)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
- ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds())
+ ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](timeTilDormantAutoDelete.Milliseconds())
})
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
@@ -2693,32 +2805,32 @@ func TestWorkspaceLock(t *testing.T) {
defer cancel()
lastUsedAt := workspace.LastUsedAt
- err := client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
- Lock: true,
+ err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: true,
})
require.NoError(t, err)
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
require.NoError(t, err, "fetch provisioned workspace")
- // The template doesn't have a locked_ttl set so this should be nil.
+ // The template doesn't have a time_til_dormant_autodelete set so this should be nil.
require.Nil(t, workspace.DeletingAt)
- require.NotNil(t, workspace.LockedAt)
- require.WithinRange(t, *workspace.LockedAt, time.Now().Add(-time.Second*10), time.Now())
+ require.NotNil(t, workspace.DormantAt)
+ require.WithinRange(t, *workspace.DormantAt, time.Now().Add(-time.Second*10), time.Now())
require.Equal(t, lastUsedAt, workspace.LastUsedAt)
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
lastUsedAt = workspace.LastUsedAt
- err = client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
- Lock: false,
+ err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: false,
})
require.NoError(t, err)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch provisioned workspace")
- require.Nil(t, workspace.LockedAt)
- // The template doesn't have a locked_ttl set so this should be nil.
+ require.Nil(t, workspace.DormantAt)
+ // The template doesn't have a time_til_dormant_autodelete set so this should be nil.
require.Nil(t, workspace.DeletingAt)
- // The last_used_at should get updated when we unlock the workspace.
+ // The last_used_at should get updated when we activate the workspace.
require.True(t, workspace.LastUsedAt.After(lastUsedAt))
})
@@ -2737,23 +2849,23 @@ func TestWorkspaceLock(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
- err := client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
- Lock: true,
+ err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: true,
})
require.NoError(t, err)
- // Should be able to stop a workspace while it is locked.
+ // Should be able to stop a workspace while it is dormant.
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
- // Should not be able to start a workspace while it is locked.
+ // Should not be able to start a workspace while it is dormant.
_, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransition(database.WorkspaceTransitionStart),
})
require.Error(t, err)
- err = client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
- Lock: false,
+ err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: false,
})
require.NoError(t, err)
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStop, database.WorkspaceTransitionStart)
diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go
index 06b4d8c167cf9..26090a4f1be99 100644
--- a/coderd/wsbuilder/wsbuilder.go
+++ b/coderd/wsbuilder/wsbuilder.go
@@ -14,13 +14,13 @@ import (
"github.com/sqlc-dev/pqtype"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/provisionerdserver"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/provisionerdserver"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
)
// Builder encapsulates the business logic of inserting a new workspace build into the database.
@@ -525,7 +525,7 @@ func (b *Builder) getParameters() (names, values []string, err error) {
// At this point, we've queried all the data we need from the database,
// so the only errors are problems with the request (missing data, failed
// validation, immutable parameters, etc.)
- return nil, nil, BuildError{http.StatusBadRequest, err.Error(), err}
+ return nil, nil, BuildError{http.StatusBadRequest, fmt.Sprintf("Unable to validate parameter %q", templateVersionParameter.Name), err}
}
names = append(names, templateVersionParameter.Name)
values = append(values, value)
diff --git a/coderd/wsbuilder/wsbuilder_test.go b/coderd/wsbuilder/wsbuilder_test.go
index afb7c5af75579..a8b6ef21f0285 100644
--- a/coderd/wsbuilder/wsbuilder_test.go
+++ b/coderd/wsbuilder/wsbuilder_test.go
@@ -13,11 +13,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbmock"
- "github.com/coder/coder/coderd/provisionerdserver"
- "github.com/coder/coder/coderd/wsbuilder"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbmock"
+ "github.com/coder/coder/v2/coderd/provisionerdserver"
+ "github.com/coder/coder/v2/coderd/wsbuilder"
+ "github.com/coder/coder/v2/codersdk"
)
var (
diff --git a/coderd/wsconncache/wsconncache.go b/coderd/wsconncache/wsconncache.go
index 4ff7f30e049eb..b9d362eac3163 100644
--- a/coderd/wsconncache/wsconncache.go
+++ b/coderd/wsconncache/wsconncache.go
@@ -16,8 +16,8 @@ import (
"golang.org/x/sync/singleflight"
"golang.org/x/xerrors"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/site"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/site"
)
type AgentProvider struct {
diff --git a/coderd/wsconncache/wsconncache_test.go b/coderd/wsconncache/wsconncache_test.go
index d0769cf057316..68e41b17517fa 100644
--- a/coderd/wsconncache/wsconncache_test.go
+++ b/coderd/wsconncache/wsconncache_test.go
@@ -23,13 +23,13 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/wsconncache"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/tailnet/tailnettest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/wsconncache"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/tailnet/tailnettest"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
@@ -179,9 +179,10 @@ func setupAgent(t *testing.T, manifest agentsdk.Manifest, ptyTimeout time.Durati
_ = closer.Close()
})
conn, err := tailnet.NewConn(&tailnet.Options{
- Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
- DERPMap: manifest.DERPMap,
- Logger: slogtest.Make(t, nil).Named("tailnet").Leveled(slog.LevelDebug),
+ Addresses: []netip.Prefix{netip.PrefixFrom(tailnet.IP(), 128)},
+ DERPMap: manifest.DERPMap,
+ DERPForceWebSockets: manifest.DERPForceWebSockets,
+ Logger: slogtest.Make(t, nil).Named("tailnet").Leveled(slog.LevelDebug),
})
require.NoError(t, err)
clientConn, serverConn := net.Pipe()
diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go
index e2189e1cc53d2..fb1b2f497410b 100644
--- a/codersdk/agentsdk/agentsdk.go
+++ b/codersdk/agentsdk/agentsdk.go
@@ -19,7 +19,7 @@ import (
"tailscale.com/tailcfg"
"cdr.dev/slog"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
"github.com/coder/retry"
)
@@ -89,6 +89,7 @@ type Manifest struct {
VSCodePortProxyURI string `json:"vscode_port_proxy_uri"`
Apps []codersdk.WorkspaceApp `json:"apps"`
DERPMap *tailcfg.DERPMap `json:"derpmap"`
+ DERPForceWebSockets bool `json:"derp_force_websockets"`
EnvironmentVariables map[string]string `json:"environment_variables"`
StartupScript string `json:"startup_script"`
StartupScriptTimeout time.Duration `json:"startup_script_timeout"`
@@ -612,9 +613,9 @@ func (c *Client) PostLifecycle(ctx context.Context, req PostLifecycleRequest) er
}
type PostStartupRequest struct {
- Version string `json:"version"`
- ExpandedDirectory string `json:"expanded_directory"`
- Subsystem codersdk.AgentSubsystem `json:"subsystem"`
+ Version string `json:"version"`
+ ExpandedDirectory string `json:"expanded_directory"`
+ Subsystems []codersdk.AgentSubsystem `json:"subsystems"`
}
func (c *Client) PostStartup(ctx context.Context, req PostStartupRequest) error {
diff --git a/codersdk/agentsdk/logs.go b/codersdk/agentsdk/logs.go
index 2eb7d4856f27e..52f30f9fe3989 100644
--- a/codersdk/agentsdk/logs.go
+++ b/codersdk/agentsdk/logs.go
@@ -11,7 +11,7 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
"github.com/coder/retry"
)
diff --git a/codersdk/agentsdk/logs_test.go b/codersdk/agentsdk/logs_test.go
index 1cca86242e065..7c1d7b0bf814c 100644
--- a/codersdk/agentsdk/logs_test.go
+++ b/codersdk/agentsdk/logs_test.go
@@ -13,9 +13,9 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestStartupLogsWriter_Write(t *testing.T) {
diff --git a/codersdk/apikey.go b/codersdk/apikey.go
index 514b519f5ffda..32c97cf538417 100644
--- a/codersdk/apikey.go
+++ b/codersdk/apikey.go
@@ -28,6 +28,7 @@ type APIKey struct {
type LoginType string
const (
+ LoginTypeUnknown LoginType = ""
LoginTypePassword LoginType = "password"
LoginTypeGithub LoginType = "github"
LoginTypeOIDC LoginType = "oidc"
diff --git a/codersdk/audit.go b/codersdk/audit.go
index 1ff2fb8ac97a1..5ceae81a21c42 100644
--- a/codersdk/audit.go
+++ b/codersdk/audit.go
@@ -24,6 +24,8 @@ const (
ResourceTypeGroup ResourceType = "group"
ResourceTypeLicense ResourceType = "license"
ResourceTypeConvertLogin ResourceType = "convert_login"
+ ResourceTypeWorkspaceProxy ResourceType = "workspace_proxy"
+ ResourceTypeOrganization ResourceType = "organization"
)
func (r ResourceType) FriendlyString() string {
@@ -50,6 +52,10 @@ func (r ResourceType) FriendlyString() string {
return "license"
case ResourceTypeConvertLogin:
return "login type conversion"
+ case ResourceTypeWorkspaceProxy:
+ return "workspace proxy"
+ case ResourceTypeOrganization:
+ return "organization"
default:
return "unknown"
}
diff --git a/codersdk/client.go b/codersdk/client.go
index ad9e46ccf7f4a..9c34245bcba76 100644
--- a/codersdk/client.go
+++ b/codersdk/client.go
@@ -20,7 +20,7 @@ import (
"go.opentelemetry.io/otel/semconv/v1.14.0/httpconv"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/tracing"
"cdr.dev/slog"
)
@@ -71,6 +71,9 @@ const (
// command that was invoked to produce the request. It is for internal use
// only.
CLITelemetryHeader = "Coder-CLI-Telemetry"
+
+ // ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon
+ ProvisionerDaemonPSK = "Coder-Provisioner-Daemon-PSK"
)
// loggableMimeTypes is a list of MIME types that are safe to log
diff --git a/codersdk/client_internal_test.go b/codersdk/client_internal_test.go
index 60e61c9309afb..ae86ce81ef3b7 100644
--- a/codersdk/client_internal_test.go
+++ b/codersdk/client_internal_test.go
@@ -27,7 +27,7 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/testutil"
)
const jsonCT = "application/json"
diff --git a/codersdk/deployment.go b/codersdk/deployment.go
index af861138e6d7d..a8356b6816554 100644
--- a/codersdk/deployment.go
+++ b/codersdk/deployment.go
@@ -16,8 +16,8 @@ import (
"github.com/coreos/go-oidc/v3/oidc"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/cli/clibase"
)
// Entitlement represents whether a feature is licensed.
@@ -103,6 +103,7 @@ type Entitlements struct {
HasLicense bool `json:"has_license"`
Trial bool `json:"trial"`
RequireTelemetry bool `json:"require_telemetry"`
+ RefreshedAt time.Time `json:"refreshed_at" format:"date-time"`
}
func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) {
@@ -228,9 +229,10 @@ type DERPServerConfig struct {
}
type DERPConfig struct {
- BlockDirect clibase.Bool `json:"block_direct" typescript:",notnull"`
- URL clibase.String `json:"url" typescript:",notnull"`
- Path clibase.String `json:"path" typescript:",notnull"`
+ BlockDirect clibase.Bool `json:"block_direct" typescript:",notnull"`
+ ForceWebSockets clibase.Bool `json:"force_websockets" typescript:",notnull"`
+ URL clibase.String `json:"url" typescript:",notnull"`
+ Path clibase.String `json:"path" typescript:",notnull"`
}
type PrometheusConfig struct {
@@ -260,9 +262,12 @@ type OAuth2GithubConfig struct {
}
type OIDCConfig struct {
- AllowSignups clibase.Bool `json:"allow_signups" typescript:",notnull"`
- ClientID clibase.String `json:"client_id" typescript:",notnull"`
- ClientSecret clibase.String `json:"client_secret" typescript:",notnull"`
+ AllowSignups clibase.Bool `json:"allow_signups" typescript:",notnull"`
+ ClientID clibase.String `json:"client_id" typescript:",notnull"`
+ ClientSecret clibase.String `json:"client_secret" typescript:",notnull"`
+ // ClientKeyFile & ClientCertFile are used in place of ClientSecret for PKI auth.
+ ClientKeyFile clibase.String `json:"client_key_file" typescript:",notnull"`
+ ClientCertFile clibase.String `json:"client_cert_file" typescript:",notnull"`
EmailDomain clibase.StringArray `json:"email_domain" typescript:",notnull"`
IssuerURL clibase.String `json:"issuer_url" typescript:",notnull"`
Scopes clibase.StringArray `json:"scopes" typescript:",notnull"`
@@ -271,6 +276,8 @@ type OIDCConfig struct {
EmailField clibase.String `json:"email_field" typescript:",notnull"`
AuthURLParams clibase.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"`
IgnoreUserInfo clibase.Bool `json:"ignore_user_info" typescript:",notnull"`
+ GroupAutoCreate clibase.Bool `json:"group_auto_create" typescript:",notnull"`
+ GroupRegexFilter clibase.Regexp `json:"group_regex_filter" typescript:",notnull"`
GroupField clibase.String `json:"groups_field" typescript:",notnull"`
GroupMapping clibase.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"`
UserRoleField clibase.String `json:"user_role_field" typescript:",notnull"`
@@ -328,6 +335,7 @@ type ProvisionerConfig struct {
DaemonPollInterval clibase.Duration `json:"daemon_poll_interval" typescript:",notnull"`
DaemonPollJitter clibase.Duration `json:"daemon_poll_jitter" typescript:",notnull"`
ForceCancelInterval clibase.Duration `json:"force_cancel_interval" typescript:",notnull"`
+ DaemonPSK clibase.String `json:"daemon_psk" typescript:",notnull"`
}
type RateLimitConfig struct {
@@ -730,6 +738,7 @@ when required by your organization's security policy.`,
Value: &c.DERP.Server.RegionID,
Group: &deploymentGroupNetworkingDERP,
YAML: "regionID",
+ Hidden: true,
// Does not apply to external proxies as this value is generated.
},
{
@@ -741,6 +750,7 @@ when required by your organization's security policy.`,
Value: &c.DERP.Server.RegionCode,
Group: &deploymentGroupNetworkingDERP,
YAML: "regionCode",
+ Hidden: true,
// Does not apply to external proxies as we use the proxy name.
},
{
@@ -756,10 +766,10 @@ when required by your organization's security policy.`,
},
{
Name: "DERP Server STUN Addresses",
- Description: "Addresses for STUN servers to establish P2P connections. Use special value 'disable' to turn off STUN.",
+ Description: "Addresses for STUN servers to establish P2P connections. It's recommended to have at least two STUN servers to give users the best chance of connecting P2P to workspaces. Each STUN server will get it's own DERP region, with region IDs starting at `--derp-server-region-id + 1`. Use special value 'disable' to turn off STUN completely.",
Flag: "derp-server-stun-addresses",
Env: "CODER_DERP_SERVER_STUN_ADDRESSES",
- Default: "stun.l.google.com:19302",
+ Default: "stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302",
Value: &c.DERP.Server.STUNAddresses,
Group: &deploymentGroupNetworkingDERP,
YAML: "stunAddresses",
@@ -788,6 +798,15 @@ when required by your organization's security policy.`,
Group: &deploymentGroupNetworkingDERP,
YAML: "blockDirect",
},
+ {
+ Name: "DERP Force WebSockets",
+ Description: "Force clients and agents to always use WebSocket to connect to DERP relay servers. By default, DERP uses `Upgrade: derp`, which may cause issues with some reverse proxies. Clients may automatically fallback to WebSocket if they detect an issue with `Upgrade: derp`, but this does not work in all situations.",
+ Flag: "derp-force-websockets",
+ Env: "CODER_DERP_FORCE_WEBSOCKETS",
+ Value: &c.DERP.Config.ForceWebSockets,
+ Group: &deploymentGroupNetworkingDERP,
+ YAML: "forceWebSockets",
+ },
{
Name: "DERP Config URL",
Description: "URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custom-derp-servers/.",
@@ -963,6 +982,26 @@ when required by your organization's security policy.`,
Value: &c.OIDC.ClientSecret,
Group: &deploymentGroupOIDC,
},
+ {
+ Name: "OIDC Client Key File",
+ Description: "Pem encoded RSA private key to use for oauth2 PKI/JWT authorization. " +
+ "This can be used instead of oidc-client-secret if your IDP supports it.",
+ Flag: "oidc-client-key-file",
+ Env: "CODER_OIDC_CLIENT_KEY_FILE",
+ YAML: "oidcClientKeyFile",
+ Value: &c.OIDC.ClientKeyFile,
+ Group: &deploymentGroupOIDC,
+ },
+ {
+ Name: "OIDC Client Cert File",
+ Description: "Pem encoded certificate file to use for oauth2 PKI/JWT authorization. " +
+ "The public certificate that accompanies oidc-client-key-file. A standard x509 certificate is expected.",
+ Flag: "oidc-client-cert-file",
+ Env: "CODER_OIDC_CLIENT_CERT_FILE",
+ YAML: "oidcClientCertFile",
+ Value: &c.OIDC.ClientCertFile,
+ Group: &deploymentGroupOIDC,
+ },
{
Name: "OIDC Email Domain",
Description: "Email domains that clients logging in with OIDC must match.",
@@ -1065,6 +1104,26 @@ when required by your organization's security policy.`,
Group: &deploymentGroupOIDC,
YAML: "groupMapping",
},
+ {
+ Name: "Enable OIDC Group Auto Create",
+ Description: "Automatically creates missing groups from a user's groups claim.",
+ Flag: "oidc-group-auto-create",
+ Env: "CODER_OIDC_GROUP_AUTO_CREATE",
+ Default: "false",
+ Value: &c.OIDC.GroupAutoCreate,
+ Group: &deploymentGroupOIDC,
+ YAML: "enableGroupAutoCreate",
+ },
+ {
+ Name: "OIDC Regex Group Filter",
+ Description: "If provided any group name not matching the regex is ignored. This allows for filtering out groups that are not needed. This filter is applied after the group mapping.",
+ Flag: "oidc-group-regex-filter",
+ Env: "CODER_OIDC_GROUP_REGEX_FILTER",
+ Default: ".*",
+ Value: &c.OIDC.GroupRegexFilter,
+ Group: &deploymentGroupOIDC,
+ YAML: "groupRegexFilter",
+ },
{
Name: "OIDC User Role Field",
Description: "This field must be set if using the user roles sync feature. Set this to the name of the claim used to store the user's role. The roles should be sent as an array of strings.",
@@ -1230,6 +1289,15 @@ when required by your organization's security policy.`,
Group: &deploymentGroupProvisioning,
YAML: "forceCancelInterval",
},
+ {
+ Name: "Provisioner Daemon Pre-shared Key (PSK)",
+ Description: "Pre-shared key to authenticate external provisioner daemons to Coder server.",
+ Flag: "provisioner-daemon-psk",
+ Env: "CODER_PROVISIONER_DAEMON_PSK",
+ Value: &c.Provisioner.DaemonPSK,
+ Group: &deploymentGroupProvisioning,
+ YAML: "daemonPSK",
+ },
// RateLimit settings
{
Name: "Disable All Rate Limits",
@@ -1871,6 +1939,9 @@ const (
// Deployment health page
ExperimentDeploymentHealthPage Experiment = "deployment_health_page"
+ // Workspaces batch actions
+ ExperimentWorkspacesBatchActions Experiment = "workspaces_batch_actions"
+
// Add new experiments here!
// ExperimentExample Experiment = "example"
)
@@ -1881,6 +1952,7 @@ const (
// not be included here and will be essentially hidden.
var ExperimentsAll = Experiments{
ExperimentDeploymentHealthPage,
+ ExperimentWorkspacesBatchActions,
}
// Experiments is a list of experiments that are enabled for the deployment.
diff --git a/codersdk/deployment_test.go b/codersdk/deployment_test.go
index eb4e267364c98..408aa4fd21ae5 100644
--- a/codersdk/deployment_test.go
+++ b/codersdk/deployment_test.go
@@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/codersdk"
)
type exclusion struct {
diff --git a/codersdk/groups.go b/codersdk/groups.go
index c04267e4e0eb2..2796a776a960a 100644
--- a/codersdk/groups.go
+++ b/codersdk/groups.go
@@ -10,6 +10,13 @@ import (
"golang.org/x/xerrors"
)
+type GroupSource string
+
+const (
+ GroupSourceUser GroupSource = "user"
+ GroupSourceOIDC GroupSource = "oidc"
+)
+
type CreateGroupRequest struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
@@ -18,13 +25,18 @@ type CreateGroupRequest struct {
}
type Group struct {
- ID uuid.UUID `json:"id" format:"uuid"`
- Name string `json:"name"`
- DisplayName string `json:"display_name"`
- OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
- Members []User `json:"members"`
- AvatarURL string `json:"avatar_url"`
- QuotaAllowance int `json:"quota_allowance"`
+ ID uuid.UUID `json:"id" format:"uuid"`
+ Name string `json:"name"`
+ DisplayName string `json:"display_name"`
+ OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
+ Members []User `json:"members"`
+ AvatarURL string `json:"avatar_url"`
+ QuotaAllowance int `json:"quota_allowance"`
+ Source GroupSource `json:"source"`
+}
+
+func (g Group) IsEveryone() bool {
+ return g.ID == g.OrganizationID
}
func (c *Client) CreateGroup(ctx context.Context, orgID uuid.UUID, req CreateGroupRequest) (Group, error) {
diff --git a/codersdk/insights.go b/codersdk/insights.go
index bfc51bd208dd6..6780b82d45d43 100644
--- a/codersdk/insights.go
+++ b/codersdk/insights.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "net/url"
"strings"
"time"
@@ -61,18 +62,18 @@ type UserLatencyInsightsRequest struct {
}
func (c *Client) UserLatencyInsights(ctx context.Context, req UserLatencyInsightsRequest) (UserLatencyInsightsResponse, error) {
- var qp []string
- qp = append(qp, fmt.Sprintf("start_time=%s", req.StartTime.Format(insightsTimeLayout)))
- qp = append(qp, fmt.Sprintf("end_time=%s", req.EndTime.Format(insightsTimeLayout)))
+ qp := url.Values{}
+ qp.Add("start_time", req.StartTime.Format(insightsTimeLayout))
+ qp.Add("end_time", req.EndTime.Format(insightsTimeLayout))
if len(req.TemplateIDs) > 0 {
var templateIDs []string
for _, id := range req.TemplateIDs {
templateIDs = append(templateIDs, id.String())
}
- qp = append(qp, fmt.Sprintf("template_ids=%s", strings.Join(templateIDs, ",")))
+ qp.Add("template_ids", strings.Join(templateIDs, ","))
}
- reqURL := fmt.Sprintf("/api/v2/insights/user-latency?%s", strings.Join(qp, "&"))
+ reqURL := fmt.Sprintf("/api/v2/insights/user-latency?%s", qp.Encode())
resp, err := c.Request(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return UserLatencyInsightsResponse{}, xerrors.Errorf("make request: %w", err)
@@ -118,8 +119,7 @@ type TemplateAppsType string
// TemplateAppsType enums.
const (
TemplateAppsTypeBuiltin TemplateAppsType = "builtin"
- // TODO(mafredri): To be introduced in a future pull request.
- // TemplateAppsTypeApp TemplateAppsType = "app"
+ TemplateAppsTypeApp TemplateAppsType = "app"
)
// TemplateAppUsage shows the usage of an app for one or more templates.
@@ -138,6 +138,8 @@ type TemplateParameterUsage struct {
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
+ Type string `json:"type"`
+ Description string `json:"description"`
Options []TemplateVersionParameterOption `json:"options,omitempty"`
Values []TemplateParameterValue `json:"values"`
}
@@ -157,21 +159,21 @@ type TemplateInsightsRequest struct {
}
func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsRequest) (TemplateInsightsResponse, error) {
- var qp []string
- qp = append(qp, fmt.Sprintf("start_time=%s", req.StartTime.Format(insightsTimeLayout)))
- qp = append(qp, fmt.Sprintf("end_time=%s", req.EndTime.Format(insightsTimeLayout)))
+ qp := url.Values{}
+ qp.Add("start_time", req.StartTime.Format(insightsTimeLayout))
+ qp.Add("end_time", req.EndTime.Format(insightsTimeLayout))
if len(req.TemplateIDs) > 0 {
var templateIDs []string
for _, id := range req.TemplateIDs {
templateIDs = append(templateIDs, id.String())
}
- qp = append(qp, fmt.Sprintf("template_ids=%s", strings.Join(templateIDs, ",")))
+ qp.Add("template_ids", strings.Join(templateIDs, ","))
}
if req.Interval != "" {
- qp = append(qp, fmt.Sprintf("interval=%s", req.Interval))
+ qp.Add("interval", string(req.Interval))
}
- reqURL := fmt.Sprintf("/api/v2/insights/templates?%s", strings.Join(qp, "&"))
+ reqURL := fmt.Sprintf("/api/v2/insights/templates?%s", qp.Encode())
resp, err := c.Request(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return TemplateInsightsResponse{}, xerrors.Errorf("make request: %w", err)
diff --git a/codersdk/organizations.go b/codersdk/organizations.go
index 26290fd4f4761..0b4af0e67056a 100644
--- a/codersdk/organizations.go
+++ b/codersdk/organizations.go
@@ -108,12 +108,12 @@ type CreateTemplateRequest struct {
// FailureTTLMillis allows optionally specifying the max lifetime before Coder
// stops all resources for failed workspaces created from this template.
FailureTTLMillis *int64 `json:"failure_ttl_ms,omitempty"`
- // InactivityTTLMillis allows optionally specifying the max lifetime before Coder
+ // TimeTilDormantMillis allows optionally specifying the max lifetime before Coder
// locks inactive workspaces created from this template.
- InactivityTTLMillis *int64 `json:"inactivity_ttl_ms,omitempty"`
- // LockedTTLMillis allows optionally specifying the max lifetime before Coder
- // permanently deletes locked workspaces created from this template.
- LockedTTLMillis *int64 `json:"locked_ttl_ms,omitempty"`
+ TimeTilDormantMillis *int64 `json:"dormant_ttl_ms,omitempty"`
+ // TimeTilDormantAutoDeleteMillis allows optionally specifying the max lifetime before Coder
+ // permanently deletes dormant workspaces created from this template.
+ TimeTilDormantAutoDeleteMillis *int64 `json:"delete_ttl_ms,omitempty"`
// DisableEveryoneGroupAccess allows optionally disabling the default
// behavior of granting the 'everyone' group access to use the template.
@@ -149,10 +149,11 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization,
return organization, json.NewDecoder(res.Body).Decode(&organization)
}
-// ProvisionerDaemonsByOrganization returns provisioner daemons available for an organization.
+// ProvisionerDaemons returns provisioner daemons available.
func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
res, err := c.Request(ctx, http.MethodGet,
- "/api/v2/provisionerdaemons",
+ // TODO: the organization path parameter is currently ignored.
+ "/api/v2/organizations/default/provisionerdaemons",
nil,
)
if err != nil {
diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go
index 1c9378f718b3a..4a3e280697f74 100644
--- a/codersdk/provisionerdaemons.go
+++ b/codersdk/provisionerdaemons.go
@@ -16,8 +16,8 @@ import (
"golang.org/x/xerrors"
"nhooyr.io/websocket"
- "github.com/coder/coder/provisionerd/proto"
- "github.com/coder/coder/provisionersdk"
+ "github.com/coder/coder/v2/provisionerd/proto"
+ "github.com/coder/coder/v2/provisionersdk"
)
type LogSource string
@@ -69,7 +69,6 @@ const (
type JobErrorCode string
const (
- MissingTemplateParameter JobErrorCode = "MISSING_TEMPLATE_PARAMETER"
RequiredTemplateVariables JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES"
)
@@ -81,7 +80,7 @@ type ProvisionerJob struct {
CompletedAt *time.Time `json:"completed_at,omitempty" format:"date-time"`
CanceledAt *time.Time `json:"canceled_at,omitempty" format:"date-time"`
Error string `json:"error,omitempty"`
- ErrorCode JobErrorCode `json:"error_code,omitempty" enums:"MISSING_TEMPLATE_PARAMETER,REQUIRED_TEMPLATE_VARIABLES"`
+ ErrorCode JobErrorCode `json:"error_code,omitempty" enums:"REQUIRED_TEMPLATE_VARIABLES"`
Status ProvisionerJobStatus `json:"status" enums:"pending,running,succeeded,canceling,canceled,failed"`
WorkerID *uuid.UUID `json:"worker_id,omitempty" format:"uuid"`
FileID uuid.UUID `json:"file_id" format:"uuid"`
@@ -164,38 +163,61 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
}), nil
}
-// ListenProvisionerDaemon returns the gRPC service for a provisioner daemon
+// ServeProvisionerDaemonRequest are the parameters to call ServeProvisionerDaemon with
+// @typescript-ignore ServeProvisionerDaemonRequest
+type ServeProvisionerDaemonRequest struct {
+ // Organization is the organization for the URL. At present provisioner daemons ARE NOT scoped to organizations
+ // and so the organization ID is optional.
+ Organization uuid.UUID `json:"organization" format:"uuid"`
+ // Provisioners is a list of provisioner types hosted by the provisioner daemon
+ Provisioners []ProvisionerType `json:"provisioners"`
+ // Tags is a map of key-value pairs that tag the jobs this provisioner daemon can handle
+ Tags map[string]string `json:"tags"`
+ // PreSharedKey is an authentication key to use on the API instead of the normal session token from the client.
+ PreSharedKey string `json:"pre_shared_key"`
+}
+
+// ServeProvisionerDaemon returns the gRPC service for a provisioner daemon
// implementation. The context is during dial, not during the lifetime of the
// client. Client should be closed after use.
-func (c *Client) ServeProvisionerDaemon(ctx context.Context, organization uuid.UUID, provisioners []ProvisionerType, tags map[string]string) (proto.DRPCProvisionerDaemonClient, error) {
- serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons/serve", organization))
+func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisionerDaemonRequest) (proto.DRPCProvisionerDaemonClient, error) {
+ serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/organizations/%s/provisionerdaemons/serve", req.Organization))
if err != nil {
return nil, xerrors.Errorf("parse url: %w", err)
}
query := serverURL.Query()
- for _, provisioner := range provisioners {
+ for _, provisioner := range req.Provisioners {
query.Add("provisioner", string(provisioner))
}
- for key, value := range tags {
+ for key, value := range req.Tags {
query.Add("tag", fmt.Sprintf("%s=%s", key, value))
}
serverURL.RawQuery = query.Encode()
- jar, err := cookiejar.New(nil)
- if err != nil {
- return nil, xerrors.Errorf("create cookie jar: %w", err)
- }
- jar.SetCookies(serverURL, []*http.Cookie{{
- Name: SessionTokenCookie,
- Value: c.SessionToken(),
- }})
httpClient := &http.Client{
- Jar: jar,
Transport: c.HTTPClient.Transport,
}
+ headers := http.Header{}
+
+ if req.PreSharedKey == "" {
+ // use session token if we don't have a PSK.
+ jar, err := cookiejar.New(nil)
+ if err != nil {
+ return nil, xerrors.Errorf("create cookie jar: %w", err)
+ }
+ jar.SetCookies(serverURL, []*http.Cookie{{
+ Name: SessionTokenCookie,
+ Value: c.SessionToken(),
+ }})
+ httpClient.Jar = jar
+ } else {
+ headers.Set(ProvisionerDaemonPSK, req.PreSharedKey)
+ }
+
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
HTTPClient: httpClient,
// Need to disable compression to avoid a data-race.
CompressionMode: websocket.CompressionDisabled,
+ HTTPHeader: headers,
})
if err != nil {
if res == nil {
diff --git a/codersdk/richparameters_test.go b/codersdk/richparameters_test.go
index df70c9c10e164..a7ab416b98bff 100644
--- a/codersdk/richparameters_test.go
+++ b/codersdk/richparameters_test.go
@@ -5,8 +5,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
)
func TestParameterResolver_ValidateResolve_New(t *testing.T) {
diff --git a/codersdk/serversentevents.go b/codersdk/serversentevents.go
index 56457a0c9224e..8c026524c7d92 100644
--- a/codersdk/serversentevents.go
+++ b/codersdk/serversentevents.go
@@ -9,7 +9,7 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/tracing"
)
type ServerSentEvent struct {
diff --git a/codersdk/templates.go b/codersdk/templates.go
index 2022c876db360..406933c72b75f 100644
--- a/codersdk/templates.go
+++ b/codersdk/templates.go
@@ -44,12 +44,12 @@ type Template struct {
AllowUserAutostop bool `json:"allow_user_autostop"`
AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs"`
- // FailureTTLMillis, InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their
+ // FailureTTLMillis, TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their
// values are used if your license is entitled to use the advanced
// template scheduling feature.
- FailureTTLMillis int64 `json:"failure_ttl_ms"`
- InactivityTTLMillis int64 `json:"inactivity_ttl_ms"`
- LockedTTLMillis int64 `json:"locked_ttl_ms"`
+ FailureTTLMillis int64 `json:"failure_ttl_ms"`
+ TimeTilDormantMillis int64 `json:"time_til_dormant_ms"`
+ TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms"`
}
// WeekdaysToBitmap converts a list of weekdays to a bitmap in accordance with
@@ -185,13 +185,22 @@ type UpdateTemplateMeta struct {
// RestartRequirement can only be set if your license includes the advanced
// template scheduling feature. If you attempt to set this value while
// unlicensed, it will be ignored.
- RestartRequirement *TemplateRestartRequirement `json:"restart_requirement,omitempty"`
- AllowUserAutostart bool `json:"allow_user_autostart,omitempty"`
- AllowUserAutostop bool `json:"allow_user_autostop,omitempty"`
- AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
- FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"`
- InactivityTTLMillis int64 `json:"inactivity_ttl_ms,omitempty"`
- LockedTTLMillis int64 `json:"locked_ttl_ms,omitempty"`
+ RestartRequirement *TemplateRestartRequirement `json:"restart_requirement,omitempty"`
+ AllowUserAutostart bool `json:"allow_user_autostart,omitempty"`
+ AllowUserAutostop bool `json:"allow_user_autostop,omitempty"`
+ AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
+ FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"`
+ TimeTilDormantMillis int64 `json:"time_til_dormant_ms,omitempty"`
+ TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms,omitempty"`
+ // UpdateWorkspaceLastUsedAt updates the last_used_at field of workspaces
+ // spawned from the template. This is useful for preventing workspaces being
+ // immediately locked when updating the inactivity_ttl field to a new, shorter
+ // value.
+ UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"`
+ // UpdateWorkspaceDormant updates the dormant_at field of workspaces spawned
+ // from the template. This is useful for preventing dormant workspaces being immediately
+ // deleted when updating the dormant_ttl field to a new, shorter value.
+ UpdateWorkspaceDormantAt bool `json:"update_workspace_dormant_at"`
}
type TemplateExample struct {
diff --git a/codersdk/time_test.go b/codersdk/time_test.go
index c9f5ca05839f3..a2d3b20622ba7 100644
--- a/codersdk/time_test.go
+++ b/codersdk/time_test.go
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
func TestNullTime_MarshalJSON(t *testing.T) {
diff --git a/codersdk/users.go b/codersdk/users.go
index daeefee5f12bf..c11846ebdac2b 100644
--- a/codersdk/users.go
+++ b/codersdk/users.go
@@ -78,9 +78,12 @@ type CreateFirstUserResponse struct {
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email" format:"email"`
Username string `json:"username" validate:"required,username"`
- Password string `json:"password" validate:"required_if=DisableLogin false"`
+ Password string `json:"password"`
+ // UserLoginType defaults to LoginTypePassword.
+ UserLoginType LoginType `json:"login_type"`
// DisableLogin sets the user's login type to 'none'. This prevents the user
// from being able to use a password or any other authentication method to login.
+ // Deprecated: Set UserLoginType=LoginTypeDisabled instead.
DisableLogin bool `json:"disable_login"`
OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"`
}
diff --git a/codersdk/workspaceagentconn.go b/codersdk/workspaceagentconn.go
index 6b9b6f0d33f44..e38b4f2a47f06 100644
--- a/codersdk/workspaceagentconn.go
+++ b/codersdk/workspaceagentconn.go
@@ -21,8 +21,8 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/speedtest"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/tailnet"
)
// WorkspaceAgentIP is a static IPv6 address with the Tailscale prefix that is used to route
diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go
index e9aad8421e36a..bbb6c373c6984 100644
--- a/codersdk/workspaceagents.go
+++ b/codersdk/workspaceagents.go
@@ -20,8 +20,8 @@ import (
"tailscale.com/tailcfg"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/tailnet"
"github.com/coder/retry"
)
@@ -167,7 +167,7 @@ type WorkspaceAgent struct {
LoginBeforeReady bool `json:"login_before_ready"`
ShutdownScript string `json:"shutdown_script,omitempty"`
ShutdownScriptTimeoutSeconds int32 `json:"shutdown_script_timeout_seconds"`
- Subsystem AgentSubsystem `json:"subsystem"`
+ Subsystems []AgentSubsystem `json:"subsystems"`
Health WorkspaceAgentHealth `json:"health"` // Health reports the health of the agent.
}
@@ -186,6 +186,7 @@ type DERPRegion struct {
// @typescript-ignore WorkspaceAgentConnectionInfo
type WorkspaceAgentConnectionInfo struct {
DERPMap *tailcfg.DERPMap `json:"derp_map"`
+ DERPForceWebSockets bool `json:"derp_force_websockets"`
DisableDirectConnections bool `json:"disable_direct_connections"`
}
@@ -247,11 +248,12 @@ func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, opti
header = headerTransport.Header()
}
conn, err := tailnet.NewConn(&tailnet.Options{
- Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)},
- DERPMap: connInfo.DERPMap,
- DERPHeader: &header,
- Logger: options.Logger,
- BlockEndpoints: c.DisableDirectConnections || options.BlockEndpoints,
+ Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)},
+ DERPMap: connInfo.DERPMap,
+ DERPHeader: &header,
+ DERPForceWebSockets: connInfo.DERPForceWebSockets,
+ Logger: options.Logger,
+ BlockEndpoints: c.DisableDirectConnections || options.BlockEndpoints,
})
if err != nil {
return nil, xerrors.Errorf("create tailnet: %w", err)
@@ -754,9 +756,20 @@ type WorkspaceAgentLog struct {
type AgentSubsystem string
const (
- AgentSubsystemEnvbox AgentSubsystem = "envbox"
+ AgentSubsystemEnvbox AgentSubsystem = "envbox"
+ AgentSubsystemEnvbuilder AgentSubsystem = "envbuilder"
+ AgentSubsystemExectrace AgentSubsystem = "exectrace"
)
+func (s AgentSubsystem) Valid() bool {
+ switch s {
+ case AgentSubsystemEnvbox, AgentSubsystemEnvbuilder, AgentSubsystemExectrace:
+ return true
+ default:
+ return false
+ }
+}
+
type WorkspaceAgentLogSource string
const (
diff --git a/codersdk/workspaceagents_test.go b/codersdk/workspaceagents_test.go
index 6373160c299e1..766203268c20a 100644
--- a/codersdk/workspaceagents_test.go
+++ b/codersdk/workspaceagents_test.go
@@ -15,9 +15,9 @@ import (
"tailscale.com/tailcfg"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/testutil"
)
func TestWorkspaceAgentMetadata(t *testing.T) {
diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go
index a2ef823fcb87e..05e1a156d1122 100644
--- a/codersdk/workspaces.go
+++ b/codersdk/workspaces.go
@@ -11,7 +11,7 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/tracing"
)
// Workspace is a deployment of a template. It references a specific
@@ -28,6 +28,7 @@ type Workspace struct {
TemplateDisplayName string `json:"template_display_name"`
TemplateIcon string `json:"template_icon"`
TemplateAllowUserCancelWorkspaceJobs bool `json:"template_allow_user_cancel_workspace_jobs"`
+ TemplateActiveVersionID uuid.UUID `json:"template_active_version_id" format:"uuid"`
LatestBuild WorkspaceBuild `json:"latest_build"`
Outdated bool `json:"outdated"`
Name string `json:"name"`
@@ -35,19 +36,24 @@ type Workspace struct {
TTLMillis *int64 `json:"ttl_ms,omitempty"`
LastUsedAt time.Time `json:"last_used_at" format:"date-time"`
- // DeletingAt indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil.
- // Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive.
+ // DeletingAt indicates the time at which the workspace will be permanently deleted.
+ // A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value)
+ // and a value has been specified for time_til_dormant_autodelete on its template.
DeletingAt *time.Time `json:"deleting_at" format:"date-time"`
- // LockedAt being non-nil indicates a workspace that has been locked.
- // A locked workspace is no longer accessible by a user and must be
- // unlocked by an admin. It is subject to deletion if it breaches
- // the duration of the locked_ttl field on its template.
- LockedAt *time.Time `json:"locked_at" format:"date-time"`
+ // DormantAt being non-nil indicates a workspace that is dormant.
+ // A dormant workspace is no longer accessible must be activated.
+ // It is subject to deletion if it breaches
+ // the duration of the time_til_ field on its template.
+ DormantAt *time.Time `json:"dormant_at" format:"date-time"`
// Health shows the health of the workspace and information about
// what is causing an unhealthy status.
Health WorkspaceHealth `json:"health"`
}
+func (w Workspace) FullName() string {
+ return fmt.Sprintf("%s/%s", w.OwnerName, w.Name)
+}
+
type WorkspaceHealth struct {
Healthy bool `json:"healthy" example:"false"` // Healthy is true if the workspace is healthy.
FailingAgents []uuid.UUID `json:"failing_agents" format:"uuid"` // FailingAgents lists the IDs of the agents that are failing, if any.
@@ -289,14 +295,16 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx
return nil
}
-// UpdateWorkspaceLock is a request to lock or unlock a workspace.
-type UpdateWorkspaceLock struct {
- Lock bool `json:"lock"`
+// UpdateWorkspaceDormancy is a request to activate or make a workspace dormant.
+// A value of false will activate a dormant workspace.
+type UpdateWorkspaceDormancy struct {
+ Dormant bool `json:"dormant"`
}
-// UpdateWorkspaceLock locks or unlocks a workspace.
-func (c *Client) UpdateWorkspaceLock(ctx context.Context, id uuid.UUID, req UpdateWorkspaceLock) error {
- path := fmt.Sprintf("/api/v2/workspaces/%s/lock", id.String())
+// UpdateWorkspaceDormancy sets a workspace as dormant if dormant=true and activates a dormant workspace
+// if dormant=false.
+func (c *Client) UpdateWorkspaceDormancy(ctx context.Context, id uuid.UUID, req UpdateWorkspaceDormancy) error {
+ path := fmt.Sprintf("/api/v2/workspaces/%s/dormant", id.String())
res, err := c.Request(ctx, http.MethodPut, path, req)
if err != nil {
return xerrors.Errorf("update workspace lock: %w", err)
diff --git a/cryptorand/errors_test.go b/cryptorand/errors_test.go
index 36ce2faf6beab..6abc2143875e2 100644
--- a/cryptorand/errors_test.go
+++ b/cryptorand/errors_test.go
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/cryptorand"
)
// TestRandError checks that the code handles errors when reading from
diff --git a/cryptorand/numbers_test.go b/cryptorand/numbers_test.go
index 0ffedf78b9d9e..aec9c89a7476c 100644
--- a/cryptorand/numbers_test.go
+++ b/cryptorand/numbers_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/cryptorand"
)
func TestInt63(t *testing.T) {
diff --git a/cryptorand/slices_test.go b/cryptorand/slices_test.go
index f4c7be248c0cc..1838bcf6119da 100644
--- a/cryptorand/slices_test.go
+++ b/cryptorand/slices_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/cryptorand"
)
func TestRandomElement(t *testing.T) {
diff --git a/cryptorand/strings_test.go b/cryptorand/strings_test.go
index 3f6025e0f9588..60be57ce0f400 100644
--- a/cryptorand/strings_test.go
+++ b/cryptorand/strings_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/cryptorand"
)
func TestString(t *testing.T) {
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index e794828520e81..710152a9f38bb 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -2,13 +2,17 @@
## Requirements
-We recommend using the [Nix](https://nix.dev/) package manager as it makes any pain related to maintaining dependency versions [just disappear](https://twitter.com/mitchellh/status/1491102567296040961). Once nix [has been installed](https://nixos.org/download.html) the development environment can be _manually instantiated_ through the `nix-shell` command:
+We recommend using the [Nix](https://nix.dev/) package manager as it makes any
+pain related to maintaining dependency versions
+[just disappear](https://twitter.com/mitchellh/status/1491102567296040961). Once
+nix [has been installed](https://nixos.org/download.html) the development
+environment can be _manually instantiated_ through the `nix-shell` command:
-```
-$ cd ~/code/coder
+```shell
+cd ~/code/coder
# https://nix.dev/tutorials/declarative-and-reproducible-developer-environments
-$ nix-shell
+nix-shell
...
copying path '/nix/store/3ms6cs5210n8vfb5a7jkdvzrzdagqzbp-iana-etc-20210225' from 'https://cache.nixos.org'...
@@ -17,20 +21,24 @@ copying path '/nix/store/v2gvj8whv241nj4lzha3flq8pnllcmvv-ignore-5.2.0.tgz' from
...
```
-If [direnv](https://direnv.net/) is installed and the [hooks are configured](https://direnv.net/docs/hook.html) then the development environment can be _automatically instantiated_ by creating the following `.envrc`, thus removing the need to run `nix-shell` by hand!
+If [direnv](https://direnv.net/) is installed and the
+[hooks are configured](https://direnv.net/docs/hook.html) then the development
+environment can be _automatically instantiated_ by creating the following
+`.envrc`, thus removing the need to run `nix-shell` by hand!
-```
-$ cd ~/code/coder
-$ echo "use nix" >.envrc
-$ direnv allow
+```shell
+cd ~/code/coder
+echo "use nix" >.envrc
+direnv allow
```
-Now, whenever you enter the project folder, `direnv` will prepare the environment for you:
+Now, whenever you enter the project folder,
+[`direnv`](https://direnv.net/docs/hook.html) will prepare the environment for
+you:
-```
-$ cd ~/code/coder
+```shell
+cd ~/code/coder
-# https://direnv.net/docs/hook.html
direnv: loading ~/code/coder/.envrc
direnv: using nix
direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +HOST_PATH +IN_NIX_SHELL +LD +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES +NIX_BUILD_TOP +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_INDENT_MAKE +NIX_LDFLAGS +NIX_STORE +NM +NODE_PATH +OBJCOPY +OBJDUMP +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +TEMP +TEMPDIR +TMP +TMPDIR +XDG_DATA_DIRS +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH
@@ -38,7 +46,8 @@ direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +HOST_PATH +IN_NIX_SHELL +LD +NIX_
🎉
```
-Alternatively if you do not want to use nix then you'll need to install the need the following tools by hand:
+Alternatively if you do not want to use nix then you'll need to install the need
+the following tools by hand:
- Go 1.18+
- on macOS, run `brew install go`
@@ -53,15 +62,15 @@ Alternatively if you do not want to use nix then you'll need to install the need
- [`pg_dump`](https://stackoverflow.com/a/49689589)
- on macOS, run `brew install libpq zstd`
- on Linux, install [`zstd`](https://github.com/horta/zstd.install)
-- [`pkg-config`]()
+- `pkg-config`
- on macOS, run `brew install pkg-config`
-- [`pixman`]()
+- `pixman`
- on macOS, run `brew install pixman`
-- [`cairo`]()
+- `cairo`
- on macOS, run `brew install cairo`
-- [`pango`]()
+- `pango`
- on macOS, run `brew install pango`
-- [`pandoc`]()
+- `pandoc`
- on macOS, run `brew install pandocomatic`
### Development workflow
@@ -77,44 +86,57 @@ Use the following `make` commands and scripts in development:
- Run `./scripts/develop.sh`
- Access `http://localhost:8080`
-- The default user is `admin@coder.com` and the default password is `SomeSecurePassword!`
+- The default user is `admin@coder.com` and the default password is
+ `SomeSecurePassword!`
### Deploying a PR
-You can test your changes by creating a PR deployment. There are two ways to do this:
+You can test your changes by creating a PR deployment. There are two ways to do
+this:
1. By running `./scripts/deploy-pr.sh`
-2. By manually triggering the [`pr-deploy.yaml`](https://github.com/coder/coder/actions/workflows/pr-deploy.yaml) GitHub Action workflow
- 
+2. By manually triggering the
+ [`pr-deploy.yaml`](https://github.com/coder/coder/actions/workflows/pr-deploy.yaml)
+ GitHub Action workflow 
#### Available options
-- `-s` or `--skip-build`, force prevents the build of the Docker image.(generally not needed as we are intelligently checking if the image needs to be built)
-- `-e EXPERIMENT1,EXPERIMENT2` or `--experiments EXPERIMENT1,EXPERIMENT2`, will enable the specified experiments. (defaults to `*`)
-- `-n` or `--dry-run` will display the context without deployment. e.g., branch name and PR number, etc.
+- `-d` or `--deploy`, force deploys the PR by deleting the existing deployment.
+- `-b` or `--build`, force builds the Docker image. (generally not needed as we
+ are intelligently checking if the image needs to be built)
+- `-e EXPERIMENT1,EXPERIMENT2` or `--experiments EXPERIMENT1,EXPERIMENT2`, will
+ enable the specified experiments. (defaults to `*`)
+- `-n` or `--dry-run` will display the context without deployment. e.g., branch
+ name and PR number, etc.
- `-y` or `--yes`, will skip the CLI confirmation prompt.
-> Note: PR deployment will be re-deployed automatically when the PR is updated. It will use the last values automatically for redeployment.
+> Note: PR deployment will be re-deployed automatically when the PR is updated.
+> It will use the last values automatically for redeployment.
-> You need to be a member or collaborator of the of [coder](github.com/coder) GitHub organization to be able to deploy a PR.
+> You need to be a member or collaborator of the of [coder](github.com/coder)
+> GitHub organization to be able to deploy a PR.
-Once the deployment is finished, a unique link and credentials will be posted in the [#pr-deployments](https://codercom.slack.com/archives/C05DNE982E8) Slack channel.
+Once the deployment is finished, a unique link and credentials will be posted in
+the [#pr-deployments](https://codercom.slack.com/archives/C05DNE982E8) Slack
+channel.
### Adding database migrations and fixtures
#### Database migrations
-Database migrations are managed with [`migrate`](https://github.com/golang-migrate/migrate).
+Database migrations are managed with
+[`migrate`](https://github.com/golang-migrate/migrate).
To add new migrations, use the following command:
-```
-$ ./coderd/database/migrations/create_fixture.sh my name
+```shell
+./coderd/database/migrations/create_fixture.sh my name
/home/coder/src/coder/coderd/database/migrations/000070_my_name.up.sql
/home/coder/src/coder/coderd/database/migrations/000070_my_name.down.sql
-Run "make gen" to generate models.
```
+Run "make gen" to generate models.
+
Then write queries into the generated `.up.sql` and `.down.sql` files and commit
them into the repository. The down script should make a best-effort to retain as
much data as possible.
@@ -124,11 +146,15 @@ much data as possible.
There are two types of fixtures that are used to test that migrations don't
break existing Coder deployments:
-- Partial fixtures [`migrations/testdata/fixtures`](../coderd/database/migrations/testdata/fixtures)
-- Full database dumps [`migrations/testdata/full_dumps`](../coderd/database/migrations/testdata/full_dumps)
+- Partial fixtures
+ [`migrations/testdata/fixtures`](../coderd/database/migrations/testdata/fixtures)
+- Full database dumps
+ [`migrations/testdata/full_dumps`](../coderd/database/migrations/testdata/full_dumps)
-Both types behave like database migrations (they also [`migrate`](https://github.com/golang-migrate/migrate)). Their behavior mirrors Coder migrations such that when migration
-number `000022` is applied, fixture `000022` is applied afterwards.
+Both types behave like database migrations (they also
+[`migrate`](https://github.com/golang-migrate/migrate)). Their behavior mirrors
+Coder migrations such that when migration number `000022` is applied, fixture
+`000022` is applied afterwards.
Partial fixtures are used to conveniently add data to newly created tables so
that we can ensure that this data is migrated without issue.
@@ -140,8 +166,8 @@ migration of multiple features or complex configurations.
To add a new partial fixture, run the following command:
-```
-$ ./coderd/database/migrations/create_fixture.sh my fixture
+```shell
+./coderd/database/migrations/create_fixture.sh my fixture
/home/coder/src/coder/coderd/database/migrations/testdata/fixtures/000070_my_fixture.up.sql
```
@@ -153,9 +179,9 @@ To create a full dump, run a fully fledged Coder deployment and use it to
generate data in the database. Then shut down the deployment and take a snapshot
of the database.
-```
-$ mkdir -p coderd/database/migrations/testdata/full_dumps/v0.12.2 && cd $_
-$ pg_dump "postgres://coder@localhost:..." -a --inserts >000069_dump_v0.12.2.up.sql
+```shell
+mkdir -p coderd/database/migrations/testdata/full_dumps/v0.12.2 && cd $_
+pg_dump "postgres://coder@localhost:..." -a --inserts >000069_dump_v0.12.2.up.sql
```
Make sure sensitive data in the dump is desensitized, for instance names,
@@ -164,8 +190,8 @@ emails, OAuth tokens and other secrets. Then commit the dump to the project.
To find out what the latest migration for a version of Coder is, use the
following command:
-```
-$ git ls-files v0.12.2 -- coderd/database/migrations/*.up.sql
+```shell
+git ls-files v0.12.2 -- coderd/database/migrations/*.up.sql
```
This helps in naming the dump (e.g. `000069` above).
@@ -174,19 +200,20 @@ This helps in naming the dump (e.g. `000069` above).
### Documentation
-Our style guide for authoring documentation can be found [here](./contributing/documentation.md).
+Our style guide for authoring documentation can be found
+[here](./contributing/documentation.md).
### Backend
#### Use Go style
-Contributions must adhere to the guidelines outlined in [Effective
-Go](https://go.dev/doc/effective_go). We prefer linting rules over documenting
-styles (run ours with `make lint`); humans are error-prone!
+Contributions must adhere to the guidelines outlined in
+[Effective Go](https://go.dev/doc/effective_go). We prefer linting rules over
+documenting styles (run ours with `make lint`); humans are error-prone!
-Read [Go's Code Review Comments
-Wiki](https://github.com/golang/go/wiki/CodeReviewComments) for information on
-common comments made during reviews of Go code.
+Read
+[Go's Code Review Comments Wiki](https://github.com/golang/go/wiki/CodeReviewComments)
+for information on common comments made during reviews of Go code.
#### Avoid unused packages
@@ -201,8 +228,8 @@ Our frontend guide can be found [here](./contributing/frontend.md).
## Reviews
-> The following information has been borrowed from [Go's review
-> philosophy](https://go.dev/doc/contribute#reviews).
+> The following information has been borrowed from
+> [Go's review philosophy](https://go.dev/doc/contribute#reviews).
Coder values thorough reviews. For each review comment that you receive, please
"close" it by implementing the suggestion or providing an explanation on why the
@@ -219,27 +246,45 @@ be applied selectively or to discourage anyone from contributing.
## Releases
-Coder releases are initiated via [`./scripts/release.sh`](../scripts/release.sh) and automated via GitHub Actions. Specifically, the [`release.yaml`](../.github/workflows/release.yaml) workflow. They are created based on the current [`main`](https://github.com/coder/coder/tree/main) branch.
+Coder releases are initiated via [`./scripts/release.sh`](../scripts/release.sh)
+and automated via GitHub Actions. Specifically, the
+[`release.yaml`](../.github/workflows/release.yaml) workflow. They are created
+based on the current [`main`](https://github.com/coder/coder/tree/main) branch.
-The release notes for a release are automatically generated from commit titles and metadata from PRs that are merged into `main`.
+The release notes for a release are automatically generated from commit titles
+and metadata from PRs that are merged into `main`.
### Creating a release
-The creation of a release is initiated via [`./scripts/release.sh`](../scripts/release.sh). This script will show a preview of the release that will be created, and if you choose to continue, create and push the tag which will trigger the creation of the release via GitHub Actions.
+The creation of a release is initiated via
+[`./scripts/release.sh`](../scripts/release.sh). This script will show a preview
+of the release that will be created, and if you choose to continue, create and
+push the tag which will trigger the creation of the release via GitHub Actions.
See `./scripts/release.sh --help` for more information.
### Creating a release (via workflow dispatch)
-Typically the workflow dispatch is only used to test (dry-run) a release, meaning no actual release will take place. The workflow can be dispatched manually from [Actions: Release](https://github.com/coder/coder/actions/workflows/release.yaml). Simply press "Run workflow" and choose dry-run.
+Typically the workflow dispatch is only used to test (dry-run) a release,
+meaning no actual release will take place. The workflow can be dispatched
+manually from
+[Actions: Release](https://github.com/coder/coder/actions/workflows/release.yaml).
+Simply press "Run workflow" and choose dry-run.
-If a release has failed after the tag has been created and pushed, it can be retried by again, pressing "Run workflow", changing "Use workflow from" from "Branch: main" to "Tag: vX.X.X" and not selecting dry-run.
+If a release has failed after the tag has been created and pushed, it can be
+retried by again, pressing "Run workflow", changing "Use workflow from" from
+"Branch: main" to "Tag: vX.X.X" and not selecting dry-run.
### Commit messages
-Commit messages should follow the [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) specification.
+Commit messages should follow the
+[Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/)
+specification.
-Allowed commit types (`feat`, `fix`, etc.) are listed in [conventional-commit-types](https://github.com/commitizen/conventional-commit-types/blob/c3a9be4c73e47f2e8197de775f41d981701407fb/index.json). Note that these types are also used to automatically sort and organize the release notes.
+Allowed commit types (`feat`, `fix`, etc.) are listed in
+[conventional-commit-types](https://github.com/commitizen/conventional-commit-types/blob/c3a9be4c73e47f2e8197de775f41d981701407fb/index.json).
+Note that these types are also used to automatically sort and organize the
+release notes.
A good commit message title uses the imperative, present tense and is ~50
characters long (no more than 72).
@@ -249,21 +294,34 @@ Examples:
- Good: `feat(api): add feature X`
- Bad: `feat(api): added feature X` (past tense)
-A good rule of thumb for writing good commit messages is to recite: [If applied, this commit will ...](https://reflectoring.io/meaningful-commit-messages/).
+A good rule of thumb for writing good commit messages is to recite:
+[If applied, this commit will ...](https://reflectoring.io/meaningful-commit-messages/).
-**Note:** We lint PR titles to ensure they follow the Conventional Commits specification, however, it's still possible to merge PRs on GitHub with a badly formatted title. Take care when merging single-commit PRs as GitHub may prefer to use the original commit title instead of the PR title.
+**Note:** We lint PR titles to ensure they follow the Conventional Commits
+specification, however, it's still possible to merge PRs on GitHub with a badly
+formatted title. Take care when merging single-commit PRs as GitHub may prefer
+to use the original commit title instead of the PR title.
### Breaking changes
Breaking changes can be triggered in two ways:
-- Add `!` to the commit message title, e.g. `feat(api)!: remove deprecated endpoint /test`
-- Add the [`release/breaking`](https://github.com/coder/coder/issues?q=sort%3Aupdated-desc+label%3Arelease%2Fbreaking) label to a PR that has, or will be, merged into `main`.
+- Add `!` to the commit message title, e.g.
+ `feat(api)!: remove deprecated endpoint /test`
+- Add the
+ [`release/breaking`](https://github.com/coder/coder/issues?q=sort%3Aupdated-desc+label%3Arelease%2Fbreaking)
+ label to a PR that has, or will be, merged into `main`.
### Security
-The [`security`](https://github.com/coder/coder/issues?q=sort%3Aupdated-desc+label%3Asecurity) label can be added to PRs that have, or will be, merged into `main`. Doing so will make sure the change stands out in the release notes.
+The
+[`security`](https://github.com/coder/coder/issues?q=sort%3Aupdated-desc+label%3Asecurity)
+label can be added to PRs that have, or will be, merged into `main`. Doing so
+will make sure the change stands out in the release notes.
### Experimental
-The [`release/experimental`](https://github.com/coder/coder/issues?q=sort%3Aupdated-desc+label%3Arelease%2Fexperimental) label can be used to move the note to the bottom of the release notes under a separate title.
+The
+[`release/experimental`](https://github.com/coder/coder/issues?q=sort%3Aupdated-desc+label%3Arelease%2Fexperimental)
+label can be used to move the note to the bottom of the release notes under a
+separate title.
diff --git a/docs/about/architecture.md b/docs/about/architecture.md
index 45ef36b99b891..9489ee7fc8e16 100644
--- a/docs/about/architecture.md
+++ b/docs/about/architecture.md
@@ -8,9 +8,9 @@ This document provides a high level overview of Coder's architecture.
## coderd
-coderd is the service created by running `coder server`. It is a thin
-API that connects workspaces, provisioners and users. coderd stores its state in
-Postgres and is the only service that communicates with Postgres.
+coderd is the service created by running `coder server`. It is a thin API that
+connects workspaces, provisioners and users. coderd stores its state in Postgres
+and is the only service that communicates with Postgres.
It offers:
@@ -22,16 +22,18 @@ It offers:
## provisionerd
-provisionerd is the execution context for infrastructure modifying providers.
-At the moment, the only provider is Terraform (running `terraform`).
+provisionerd is the execution context for infrastructure modifying providers. At
+the moment, the only provider is Terraform (running `terraform`).
-By default, the Coder server runs multiple provisioner daemons. [External provisioners](../admin/provisioners.md) can be added for security or scalability purposes.
+By default, the Coder server runs multiple provisioner daemons.
+[External provisioners](../admin/provisioners.md) can be added for security or
+scalability purposes.
## Agents
-An agent is the Coder service that runs within a user's remote workspace.
-It provides a consistent interface for coderd and clients to communicate
-with workspaces regardless of operating system, architecture, or cloud.
+An agent is the Coder service that runs within a user's remote workspace. It
+provides a consistent interface for coderd and clients to communicate with
+workspaces regardless of operating system, architecture, or cloud.
It offers the following services along with much more:
@@ -40,15 +42,20 @@ It offers the following services along with much more:
- Liveness checks
- `startup_script` automation
-Templates are responsible for [creating and running agents](../templates/index.md#coder-agent) within workspaces.
+Templates are responsible for
+[creating and running agents](../templates/index.md#coder-agent) within
+workspaces.
## Service Bundling
-While coderd and Postgres can be orchestrated independently,our default installation
-paths bundle them all together into one system service. It's perfectly fine to run a production deployment this way, but there are certain situations that necessitate decomposition:
+While coderd and Postgres can be orchestrated independently,our default
+installation paths bundle them all together into one system service. It's
+perfectly fine to run a production deployment this way, but there are certain
+situations that necessitate decomposition:
- Reducing global client latency (distribute coderd and centralize database)
-- Achieving greater availability and efficiency (horizontally scale individual services)
+- Achieving greater availability and efficiency (horizontally scale individual
+ services)
## Workspaces
diff --git a/docs/admin/app-logs.md b/docs/admin/app-logs.md
index 87efe05ae6061..8235fda06eda8 100644
--- a/docs/admin/app-logs.md
+++ b/docs/admin/app-logs.md
@@ -1,21 +1,28 @@
# Application Logs
-In Coderd, application logs refer to the records of events, messages, and activities generated by the application during its execution.
-These logs provide valuable information about the application's behavior, performance, and any issues that may have occurred.
+In Coderd, application logs refer to the records of events, messages, and
+activities generated by the application during its execution. These logs provide
+valuable information about the application's behavior, performance, and any
+issues that may have occurred.
-Application logs include entries that capture events on different levels of severity:
+Application logs include entries that capture events on different levels of
+severity:
- Informational messages
- Warnings
- Errors
- Debugging information
-By analyzing application logs, system administrators can gain insights into the application's behavior, identify and diagnose problems, track performance metrics, and make informed decisions to improve the application's stability and efficiency.
+By analyzing application logs, system administrators can gain insights into the
+application's behavior, identify and diagnose problems, track performance
+metrics, and make informed decisions to improve the application's stability and
+efficiency.
## Error logs
-To ensure effective monitoring and timely response to critical events in the Coder application, it is recommended to configure log alerts
-that specifically watch for the following log entries:
+To ensure effective monitoring and timely response to critical events in the
+Coder application, it is recommended to configure log alerts that specifically
+watch for the following log entries:
| Log Level | Module | Log message | Potential issues |
| --------- | ---------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------- |
diff --git a/docs/admin/appearance.md b/docs/admin/appearance.md
index 5d061b3bb1f6d..f80ffc8c1bcfe 100644
--- a/docs/admin/appearance.md
+++ b/docs/admin/appearance.md
@@ -2,12 +2,15 @@
## Support Links
-Support links let admins adjust the user dropdown menu to include links referring to internal company resources. The menu section replaces the original menu positions: documentation, report a bug to GitHub, or join the Discord server.
+Support links let admins adjust the user dropdown menu to include links
+referring to internal company resources. The menu section replaces the original
+menu positions: documentation, report a bug to GitHub, or join the Discord
+server.

-Custom links can be set in the deployment configuration using the `-c `
-flag to `coder server`.
+Custom links can be set in the deployment configuration using the
+`-c ` flag to `coder server`.
```yaml
supportLinks:
@@ -27,7 +30,8 @@ The link icons are optional, and limited to: `bug`, `chat`, and `docs`.
## Service Banners (enterprise)
-Service Banners let admins post important messages to all site users. Only Site Owners may set the service banner.
+Service Banners let admins post important messages to all site users. Only Site
+Owners may set the service banner.

diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md
index 882a7274c737f..6d7293731f6cf 100644
--- a/docs/admin/audit-logs.md
+++ b/docs/admin/audit-logs.md
@@ -1,7 +1,6 @@
# Audit Logs
-Audit Logs allows **Auditors** to monitor user operations in
-their deployment.
+Audit Logs allows **Auditors** to monitor user operations in their deployment.
## Tracked Events
@@ -9,52 +8,66 @@ We track the following resources:
-| Resource | |
-| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| APIKeylogin, logout, register, create, delete | Field Tracked created_at true expires_at true hashed_secret false id false ip_address false last_used true lifetime_seconds false login_type false scope false token_name false updated_at false user_id true
|
-| AuditOAuthConvertState | Field Tracked created_at true expires_at true from_login_type true to_login_type true user_id true
|
-| Groupcreate, write, delete | Field Tracked avatar_url true display_name true id true members true name true organization_id false quota_allowance true
|
-| GitSSHKeycreate | Field Tracked created_at false private_key true public_key true updated_at false user_id true
|
-| Licensecreate, delete | Field Tracked exp true id false jwt false uploaded_at true uuid true
|
-| Templatewrite, delete | Field Tracked active_version_id true allow_user_autostart true allow_user_autostop true allow_user_cancel_workspace_jobs true created_at false created_by true created_by_avatar_url false created_by_username false default_ttl true deleted false description true display_name true failure_ttl true group_acl true icon true id true inactivity_ttl true locked_ttl true max_ttl true name true organization_id false provisioner true restart_requirement_days_of_week true restart_requirement_weeks true updated_at false user_acl true
|
-| TemplateVersioncreate, write | Field Tracked created_at false created_by true created_by_avatar_url false created_by_username false git_auth_providers false id true job_id false message false name true organization_id false readme true template_id true updated_at false
|
-| Usercreate, write, delete | Field Tracked avatar_url false created_at false deleted true email true hashed_password true id true last_seen_at false login_type true quiet_hours_schedule true rbac_roles true status true updated_at false username true
|
-| Workspacecreate, write, delete | Field Tracked autostart_schedule true created_at false deleted false deleting_at true id true last_used_at false locked_at true name true organization_id false owner_id true template_id true ttl true updated_at false
|
-| WorkspaceBuildstart, stop | Field Tracked build_number false created_at false daily_cost false deadline false id false initiator_by_avatar_url false initiator_by_username false initiator_id false job_id false max_deadline false provisioner_state false reason false template_version_id true transition false updated_at false workspace_id false
|
-| WorkspaceProxy | Field Tracked created_at true deleted false derp_enabled true derp_only true display_name true icon true id true name true region_id true token_hashed_secret true updated_at false url true wildcard_hostname true
|
+| Resource | |
+| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| APIKeylogin, logout, register, create, delete | Field Tracked created_at true expires_at true hashed_secret false id false ip_address false last_used true lifetime_seconds false login_type false scope false token_name false updated_at false user_id true
|
+| AuditOAuthConvertState | Field Tracked created_at true expires_at true from_login_type true to_login_type true user_id true
|
+| Groupcreate, write, delete | Field Tracked avatar_url true display_name true id true members true name true organization_id false quota_allowance true source false
|
+| GitSSHKeycreate | Field Tracked created_at false private_key true public_key true updated_at false user_id true
|
+| Licensecreate, delete | Field Tracked exp true id false jwt false uploaded_at true uuid true
|
+| Templatewrite, delete | Field Tracked active_version_id true allow_user_autostart true allow_user_autostop true allow_user_cancel_workspace_jobs true created_at false created_by true created_by_avatar_url false created_by_username false default_ttl true deleted false description true display_name true failure_ttl true group_acl true icon true id true max_ttl true name true organization_id false provisioner true restart_requirement_days_of_week true restart_requirement_weeks true time_til_dormant true time_til_dormant_autodelete true updated_at false user_acl true
|
+| TemplateVersioncreate, write | Field Tracked created_at false created_by true created_by_avatar_url false created_by_username false git_auth_providers false id true job_id false message false name true organization_id false readme true template_id true updated_at false
|
+| Usercreate, write, delete | Field Tracked avatar_url false created_at false deleted true email true hashed_password true id true last_seen_at false login_type true quiet_hours_schedule true rbac_roles true status true updated_at false username true
|
+| Workspacecreate, write, delete | Field Tracked autostart_schedule true created_at false deleted false deleting_at true dormant_at true id true last_used_at false name true organization_id false owner_id true template_id true ttl true updated_at false
|
+| WorkspaceBuildstart, stop | Field Tracked build_number false created_at false daily_cost false deadline false id false initiator_by_avatar_url false initiator_by_username false initiator_id false job_id false max_deadline false provisioner_state false reason false template_version_id true transition false updated_at false workspace_id false
|
+| WorkspaceProxy | Field Tracked created_at true deleted false derp_enabled true derp_only true display_name true icon true id true name true region_id true token_hashed_secret true updated_at false url true wildcard_hostname true
|
## Filtering logs
-In the Coder UI you can filter your audit logs using the pre-defined filter or by using the Coder's filter query like the examples below:
+In the Coder UI you can filter your audit logs using the pre-defined filter or
+by using the Coder's filter query like the examples below:
- `resource_type:workspace action:delete` to find deleted workspaces
- `resource_type:template action:create` to find created templates
The supported filters are:
-- `resource_type` - The type of the resource. It can be a workspace, template, user, etc. You can [find here](https://pkg.go.dev/github.com/coder/coder/codersdk#ResourceType) all the resource types that are supported.
+- `resource_type` - The type of the resource. It can be a workspace, template,
+ user, etc. You can
+ [find here](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#ResourceType)
+ all the resource types that are supported.
- `resource_id` - The ID of the resource.
-- `resource_target` - The name of the resource. Can be used instead of `resource_id`.
-- `action`- The action applied to a resource. You can [find here](https://pkg.go.dev/github.com/coder/coder/codersdk#AuditAction) all the actions that are supported.
-- `username` - The username of the user who triggered the action. You can also use `me` as a convenient alias for the logged-in user.
+- `resource_target` - The name of the resource. Can be used instead of
+ `resource_id`.
+- `action`- The action applied to a resource. You can
+ [find here](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#AuditAction)
+ all the actions that are supported.
+- `username` - The username of the user who triggered the action. You can also
+ use `me` as a convenient alias for the logged-in user.
- `email` - The email of the user who triggered the action.
- `date_from` - The inclusive start date with format `YYYY-MM-DD`.
- `date_to` - The inclusive end date with format `YYYY-MM-DD`.
-- `build_reason` - To be used with `resource_type:workspace_build`, the [initiator](https://pkg.go.dev/github.com/coder/coder/codersdk#BuildReason) behind the build start or stop.
+- `build_reason` - To be used with `resource_type:workspace_build`, the
+ [initiator](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#BuildReason)
+ behind the build start or stop.
## Capturing/Exporting Audit Logs
-In addition to the user interface, there are multiple ways to consume or query audit trails.
+In addition to the user interface, there are multiple ways to consume or query
+audit trails.
## REST API
-Audit logs can be accessed through our REST API. You can find detailed information about this in our [endpoint documentation](../api/audit.md#get-audit-logs).
+Audit logs can be accessed through our REST API. You can find detailed
+information about this in our
+[endpoint documentation](../api/audit.md#get-audit-logs).
## Service Logs
-Audit trails are also dispatched as service logs and can be captured and categorized using any log management tool such as [Splunk](https://splunk.com).
+Audit trails are also dispatched as service logs and can be captured and
+categorized using any log management tool such as [Splunk](https://splunk.com).
Example of a [JSON formatted](../cli/server.md#--log-json) audit log entry:
@@ -93,10 +106,11 @@ Example of a [JSON formatted](../cli/server.md#--log-json) audit log entry:
Example of a [human readable](../cli/server.md#--log-human) audit log entry:
-```sh
+```console
2023-06-13 03:43:29.233 [info] coderd: audit_log ID=95f7c392-da3e-480c-a579-8909f145fbe2 Time="2023-06-13T03:43:29.230422Z" UserID=6c405053-27e3-484a-9ad7-bcb64e7bfde6 OrganizationID=00000000-0000-0000-0000-000000000000 Ip= UserAgent= ResourceType=workspace_build ResourceID=988ae133-5b73-41e3-a55e-e1e9d3ef0b66 ResourceTarget="" Action=start Diff="{}" StatusCode=200 AdditionalFields="{\"workspace_name\":\"linux-container\",\"build_number\":\"7\",\"build_reason\":\"initiator\",\"workspace_owner\":\"\"}" RequestID=9682b1b5-7b9f-4bf2-9a39-9463f8e41cd6 ResourceIcon=""
```
## Enabling this feature
-This feature is only available with an enterprise license. [Learn more](../enterprise.md)
+This feature is only available with an enterprise license.
+[Learn more](../enterprise.md)
diff --git a/docs/admin/auth.md b/docs/admin/auth.md
index 16807c159fc37..fb278cf09b058 100644
--- a/docs/admin/auth.md
+++ b/docs/admin/auth.md
@@ -1,5 +1,7 @@
# Authentication
+[OIDC with Coder Sequence Diagram](https://raw.githubusercontent.com/coder/coder/138ee55abb3635cb2f3d12661f8caef2ca9d0961/docs/images/oidc-sequence-diagram.svg).
+
By default, Coder is accessible via password authentication. Coder does not
recommend using password authentication in production, and recommends using an
authentication provider with properly configured multi-factor authentication
@@ -12,12 +14,19 @@ The following steps explain how to set up GitHub OAuth or OpenID Connect.
### Step 1: Configure the OAuth application in GitHub
-First, [register a GitHub OAuth app](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/). GitHub will ask you for the following Coder parameters:
+First,
+[register a GitHub OAuth app](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/).
+GitHub will ask you for the following Coder parameters:
-- **Homepage URL**: Set to your Coder deployments [`CODER_ACCESS_URL`](https://coder.com/docs/v2/latest/cli/server#--access-url) (e.g. `https://coder.domain.com`)
+- **Homepage URL**: Set to your Coder deployments
+ [`CODER_ACCESS_URL`](../cli/server.md#--access-url) (e.g.
+ `https://coder.domain.com`)
- **User Authorization Callback URL**: Set to `https://coder.domain.com`
-> Note: If you want to allow multiple coder deployments hosted on subdomains e.g. coder1.domain.com, coder2.domain.com, to be able to authenticate with the same GitHub OAuth app, then you can set **User Authorization Callback URL** to the `https://domain.com`
+> Note: If you want to allow multiple coder deployments hosted on subdomains
+> e.g. coder1.domain.com, coder2.domain.com, to be able to authenticate with the
+> same GitHub OAuth app, then you can set **User Authorization Callback URL** to
+> the `https://domain.com`
Note the Client ID and Client Secret generated by GitHub. You will use these
values in the next step.
@@ -27,17 +36,18 @@ values in the next step.
Navigate to your Coder host and run the following command to start up the Coder
server:
-```console
+```shell
coder server --oauth2-github-allow-signups=true --oauth2-github-allowed-orgs="your-org" --oauth2-github-client-id="8d1...e05" --oauth2-github-client-secret="57ebc9...02c24c"
```
-> For GitHub Enterprise support, specify the `--oauth2-github-enterprise-base-url` flag.
+> For GitHub Enterprise support, specify the
+> `--oauth2-github-enterprise-base-url` flag.
Alternatively, if you are running Coder as a system service, you can achieve the
same result as the command above by adding the following environment variables
to the `/etc/coder.d/coder.env` file:
-```console
+```env
CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS=true
CODER_OAUTH2_GITHUB_ALLOWED_ORGS="your-org"
CODER_OAUTH2_GITHUB_CLIENT_ID="8d1...e05"
@@ -46,7 +56,7 @@ CODER_OAUTH2_GITHUB_CLIENT_SECRET="57ebc9...02c24c"
**Note:** To allow everyone to signup using GitHub, set:
-```console
+```env
CODER_OAUTH2_GITHUB_ALLOW_EVERYONE=true
```
@@ -59,20 +69,22 @@ If deploying Coder via Helm, you can set the above environment variables in the
coder:
env:
- name: CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS
- value: true
- - name: CODER_OAUTH2_GITHUB_ALLOWED_ORGS
- value: "your-org"
+ value: "true"
- name: CODER_OAUTH2_GITHUB_CLIENT_ID
value: "533...des"
- name: CODER_OAUTH2_GITHUB_CLIENT_SECRET
value: "G0CSP...7qSM"
- - name: CODER_OAUTH2_GITHUB_ALLOW_EVERYONE
- value: true
+ # If setting allowed orgs, comment out CODER_OAUTH2_GITHUB_ALLOW_EVERYONE and its value
+ - name: CODER_OAUTH2_GITHUB_ALLOWED_ORGS
+ value: "your-org"
+ # If allowing everyone, comment out CODER_OAUTH2_GITHUB_ALLOWED_ORGS and it's value
+ #- name: CODER_OAUTH2_GITHUB_ALLOW_EVERYONE
+ # value: "true"
```
To upgrade Coder, run:
-```console
+```shell
helm upgrade coder-v2/coder -n -f values.yaml
```
@@ -82,7 +94,8 @@ helm upgrade coder-v2/coder -n -f values.yaml
## OpenID Connect
-The following steps through how to integrate any OpenID Connect provider (Okta, Active Directory, etc.) to Coder.
+The following steps through how to integrate any OpenID Connect provider (Okta,
+Active Directory, etc.) to Coder.
### Step 1: Set Redirect URI with your OIDC provider
@@ -95,15 +108,15 @@ Your OIDC provider will ask you for the following parameter:
Navigate to your Coder host and run the following command to start up the Coder
server:
-```console
+```shell
coder server --oidc-issuer-url="https://issuer.corp.com" --oidc-email-domain="your-domain-1,your-domain-2" --oidc-client-id="533...des" --oidc-client-secret="G0CSP...7qSM"
```
-If you are running Coder as a system service, you can achieve the
-same result as the command above by adding the following environment variables
-to the `/etc/coder.d/coder.env` file:
+If you are running Coder as a system service, you can achieve the same result as
+the command above by adding the following environment variables to the
+`/etc/coder.d/coder.env` file:
-```console
+```env
CODER_OIDC_ISSUER_URL="https://issuer.corp.com"
CODER_OIDC_EMAIL_DOMAIN="your-domain-1,your-domain-2"
CODER_OIDC_CLIENT_ID="533...des"
@@ -130,46 +143,46 @@ coder:
To upgrade Coder, run:
-```console
+```shell
helm upgrade coder-v2/coder -n -f values.yaml
```
## OIDC Claims
-When a user logs in for the first time via OIDC, Coder will merge both
-the claims from the ID token and the claims obtained from hitting the
-upstream provider's `userinfo` endpoint, and use the resulting data
-as a basis for creating a new user or looking up an existing user.
+When a user logs in for the first time via OIDC, Coder will merge both the
+claims from the ID token and the claims obtained from hitting the upstream
+provider's `userinfo` endpoint, and use the resulting data as a basis for
+creating a new user or looking up an existing user.
-To troubleshoot claims, set `CODER_VERBOSE=true` and follow the logs
-while signing in via OIDC as a new user. Coder will log the claim fields
-returned by the upstream identity provider in a message containing the
-string `got oidc claims`, as well as the user info returned.
+To troubleshoot claims, set `CODER_VERBOSE=true` and follow the logs while
+signing in via OIDC as a new user. Coder will log the claim fields returned by
+the upstream identity provider in a message containing the string
+`got oidc claims`, as well as the user info returned.
-> **Note:** If you need to ensure that Coder only uses information from
-> the ID token and does not hit the UserInfo endpoint, you can set the
-> configuration option `CODER_OIDC_IGNORE_USERINFO=true`.
+> **Note:** If you need to ensure that Coder only uses information from the ID
+> token and does not hit the UserInfo endpoint, you can set the configuration
+> option `CODER_OIDC_IGNORE_USERINFO=true`.
### Email Addresses
-By default, Coder will look for the OIDC claim named `email` and use that
-value for the newly created user's email address.
+By default, Coder will look for the OIDC claim named `email` and use that value
+for the newly created user's email address.
If your upstream identity provider users a different claim, you can set
`CODER_OIDC_EMAIL_FIELD` to the desired claim.
-> **Note:** If this field is not present, Coder will attempt to use the
-> claim field configured for `username` as an email address. If this field
-> is not a valid email address, OIDC logins will fail.
+> **Note** If this field is not present, Coder will attempt to use the claim
+> field configured for `username` as an email address. If this field is not a
+> valid email address, OIDC logins will fail.
### Email Address Verification
-Coder requires all OIDC email addresses to be verified by default. If
-the `email_verified` claim is present in the token response from the identity
+Coder requires all OIDC email addresses to be verified by default. If the
+`email_verified` claim is present in the token response from the identity
provider, Coder will validate that its value is `true`. If needed, you can
disable this behavior with the following setting:
-```console
+```env
CODER_OIDC_IGNORE_EMAIL_VERIFIED=true
```
@@ -178,14 +191,14 @@ CODER_OIDC_IGNORE_EMAIL_VERIFIED=true
### Usernames
-When a new user logs in via OIDC, Coder will by default use the value
-of the claim field named `preferred_username` as the the username.
+When a new user logs in via OIDC, Coder will by default use the value of the
+claim field named `preferred_username` as the the username.
-If your upstream identity provider uses a different claim, you can
-set `CODER_OIDC_USERNAME_FIELD` to the desired claim.
+If your upstream identity provider uses a different claim, you can set
+`CODER_OIDC_USERNAME_FIELD` to the desired claim.
-> **Note:** If this claim is empty, the email address will be stripped of
-> the domain, and become the username (e.g. `example@coder.com` becomes `example`).
+> **Note:** If this claim is empty, the email address will be stripped of the
+> domain, and become the username (e.g. `example@coder.com` becomes `example`).
> To avoid conflicts, Coder may also append a random word to the resulting
> username.
@@ -194,36 +207,38 @@ set `CODER_OIDC_USERNAME_FIELD` to the desired claim.
If you'd like to change the OpenID Connect button text and/or icon, you can
configure them like so:
-```console
+```env
CODER_OIDC_SIGN_IN_TEXT="Sign in with Gitea"
CODER_OIDC_ICON_URL=https://gitea.io/images/gitea.png
```
## Disable Built-in Authentication
-To remove email and password login, set the following environment variable on your
-Coder deployment:
+To remove email and password login, set the following environment variable on
+your Coder deployment:
-```console
+```env
CODER_DISABLE_PASSWORD_AUTH=true
```
## SCIM (enterprise)
Coder supports user provisioning and deprovisioning via SCIM 2.0 with header
-authentication. Upon deactivation, users are [suspended](./users.md#suspend-a-user)
-and are not deleted. [Configure](./configure.md) your SCIM application with an
-auth key and supply it the Coder server.
+authentication. Upon deactivation, users are
+[suspended](./users.md#suspend-a-user) and are not deleted.
+[Configure](./configure.md) your SCIM application with an auth key and supply it
+the Coder server.
-```console
+```env
CODER_SCIM_API_KEY="your-api-key"
```
## TLS
-If your OpenID Connect provider requires client TLS certificates for authentication, you can configure them like so:
+If your OpenID Connect provider requires client TLS certificates for
+authentication, you can configure them like so:
-```console
+```env
CODER_TLS_CLIENT_CERT_FILE=/path/to/cert.pem
CODER_TLS_CLIENT_KEY_FILE=/path/to/key.pem
```
@@ -233,22 +248,31 @@ CODER_TLS_CLIENT_KEY_FILE=/path/to/key.pem
If your OpenID Connect provider supports group claims, you can configure Coder
to synchronize groups in your auth provider to groups within Coder.
-To enable group sync, ensure that the `groups` claim is set by adding the correct scope to request. If group sync is
-enabled, the user's groups will be controlled by the OIDC provider. This means
-manual group additions/removals will be overwritten on the next login.
+To enable group sync, ensure that the `groups` claim is set by adding the
+correct scope to request. If group sync is enabled, the user's groups will be
+controlled by the OIDC provider. This means manual group additions/removals will
+be overwritten on the next login.
-```console
+```env
# as an environment variable
CODER_OIDC_SCOPES=openid,profile,email,groups
+```
+
+```shell
# as a flag
--oidc-scopes openid,profile,email,groups
```
-With the `groups` scope requested, we also need to map the `groups` claim name. Coder recommends using `groups` for the claim name. This step is necessary if your **scope's name** is something other than `groups`.
+With the `groups` scope requested, we also need to map the `groups` claim name.
+Coder recommends using `groups` for the claim name. This step is necessary if
+your **scope's name** is something other than `groups`.
-```console
+```env
# as an environment variable
CODER_OIDC_GROUP_FIELD=groups
+```
+
+```shell
# as a flag
--oidc-group-field groups
```
@@ -260,9 +284,12 @@ For cases when an OIDC provider only returns group IDs ([Azure AD][azure-gids])
or you want to have different group names in Coder than in your OIDC provider,
you can configure mapping between the two.
-```console
+```env
# as an environment variable
CODER_OIDC_GROUP_MAPPING='{"myOIDCGroupID": "myCoderGroupName"}'
+```
+
+```shell
# as a flag
--oidc-group-mapping '{"myOIDCGroupID": "myCoderGroupName"}'
```
@@ -282,12 +309,47 @@ OIDC provider will be added to the `myCoderGroupName` group in Coder.
> **Note:** Groups are only updated on login.
-[azure-gids]: https://github.com/MicrosoftDocs/azure-docs/issues/59766#issuecomment-664387195
+[azure-gids]:
+ https://github.com/MicrosoftDocs/azure-docs/issues/59766#issuecomment-664387195
### Troubleshooting
Some common issues when enabling group sync.
+#### User not being assigned / Group does not exist
+
+If you want Coder to create groups that do not exist, you can set the following
+environment variable. If you enable this, your OIDC provider might be sending
+over many unnecessary groups. Use filtering options on the OIDC provider to
+limit the groups sent over to prevent creating excess groups.
+
+```env
+# as an environment variable
+CODER_OIDC_GROUP_AUTO_CREATE=true
+```
+
+```shell
+# as a flag
+--oidc-group-auto-create=true
+```
+
+A basic regex filtering option on the Coder side is available. This is applied
+**after** the group mapping (`CODER_OIDC_GROUP_MAPPING`), meaning if the group
+is remapped, the remapped value is tested in the regex. This is useful if you
+want to filter out groups that do not match a certain pattern. For example, if
+you want to only allow groups that start with `my-group-` to be created, you can
+set the following environment variable.
+
+```env
+# as an environment variable
+CODER_OIDC_GROUP_REGEX_FILTER="^my-group-.*$"
+```
+
+```shell
+# as a flag
+--oidc-group-regex-filter="^my-group-.*$"
+```
+
#### Invalid Scope
If you see an error like the following, you may have an invalid scope.
@@ -296,28 +358,39 @@ If you see an error like the following, you may have an invalid scope.
The application '' asked for scope 'groups' that doesn't exist on the resource...
```
-This can happen because the identity provider has a different name for the scope. For example, Azure AD uses `GroupMember.Read.All` instead of `groups`. You can find the correct scope name in the IDP's documentation. Some IDP's allow configuring the name of this scope.
+This can happen because the identity provider has a different name for the
+scope. For example, Azure AD uses `GroupMember.Read.All` instead of `groups`.
+You can find the correct scope name in the IDP's documentation. Some IDP's allow
+configuring the name of this scope.
-The solution is to update the value of `CODER_OIDC_SCOPES` to the correct value for the identity provider.
+The solution is to update the value of `CODER_OIDC_SCOPES` to the correct value
+for the identity provider.
#### No `group` claim in the `got oidc claims` log
Steps to troubleshoot.
-1. Ensure the user is a part of a group in the IDP. If the user has 0 groups, no `groups` claim will be sent.
-2. Check if another claim appears to be the correct claim with a different name. A common name is `memberOf` instead of `groups`. If this is present, update `CODER_OIDC_GROUP_FIELD=memberOf`.
-3. Make sure the number of groups being sent is under the limit of the IDP. Some IDPs will return an error, while others will just omit the `groups` claim. A common solution is to create a filter on the identity provider that returns less than the limit for your IDP.
+1. Ensure the user is a part of a group in the IDP. If the user has 0 groups, no
+ `groups` claim will be sent.
+2. Check if another claim appears to be the correct claim with a different name.
+ A common name is `memberOf` instead of `groups`. If this is present, update
+ `CODER_OIDC_GROUP_FIELD=memberOf`.
+3. Make sure the number of groups being sent is under the limit of the IDP. Some
+ IDPs will return an error, while others will just omit the `groups` claim. A
+ common solution is to create a filter on the identity provider that returns
+ less than the limit for your IDP.
- [Azure AD limit is 200, and omits groups if exceeded.](https://learn.microsoft.com/en-us/azure/active-directory/hybrid/connect/how-to-connect-fed-group-claims#options-for-applications-to-consume-group-information)
- [Okta limit is 100, and returns an error if exceeded.](https://developer.okta.com/docs/reference/api/oidc/#scope-dependent-claims-not-always-returned)
## Role sync (enterprise)
If your OpenID Connect provider supports roles claims, you can configure Coder
-to synchronize roles in your auth provider to deployment-wide roles within Coder.
+to synchronize roles in your auth provider to deployment-wide roles within
+Coder.
Set the following in your Coder server [configuration](./configure.md).
-```console
+```env
# Depending on your identity provider configuration, you may need to explicitly request a "roles" scope
CODER_OIDC_SCOPES=openid,profile,email,roles
@@ -326,7 +399,8 @@ CODER_OIDC_USER_ROLE_FIELD=roles
CODER_OIDC_USER_ROLE_MAPPING='{"TemplateAuthor":["template-admin","user-admin"]}'
```
-> One role from your identity provider can be mapped to many roles in Coder (e.g. the example above maps to 2 roles in Coder.)
+> One role from your identity provider can be mapped to many roles in Coder
+> (e.g. the example above maps to 2 roles in Coder.)
## Provider-Specific Guides
@@ -336,17 +410,20 @@ Below are some details specific to individual OIDC providers.
> **Note:** Tested on ADFS 4.0, Windows Server 2019
-1. In your Federation Server, create a new application group for Coder. Follow the
- steps as described [here.](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/development/msal/adfs-msal-web-app-web-api#app-registration-in-ad-fs)
+1. In your Federation Server, create a new application group for Coder. Follow
+ the steps as described
+ [here.](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/development/msal/adfs-msal-web-app-web-api#app-registration-in-ad-fs)
- **Server Application**: Note the Client ID.
- **Configure Application Credentials**: Note the Client Secret.
- **Configure Web API**: Set the Client ID as the relying party identifier.
- - **Application Permissions**: Allow access to the claims `openid`, `email`, `profile`, and `allatclaims`.
-1. Visit your ADFS server's `/.well-known/openid-configuration` URL and note
- the value for `issuer`.
- > **Note:** This is usually of the form `https://adfs.corp/adfs/.well-known/openid-configuration`
-1. In Coder's configuration file (or Helm values as appropriate), set the following
- environment variables or their corresponding CLI arguments:
+ - **Application Permissions**: Allow access to the claims `openid`, `email`,
+ `profile`, and `allatclaims`.
+1. Visit your ADFS server's `/.well-known/openid-configuration` URL and note the
+ value for `issuer`.
+ > **Note:** This is usually of the form
+ > `https://adfs.corp/adfs/.well-known/openid-configuration`
+1. In Coder's configuration file (or Helm values as appropriate), set the
+ following environment variables or their corresponding CLI arguments:
- `CODER_OIDC_ISSUER_URL`: the `issuer` value from the previous step.
- `CODER_OIDC_CLIENT_ID`: the Client ID from step 1.
@@ -357,28 +434,44 @@ Below are some details specific to individual OIDC providers.
{"resource":"$CLIENT_ID"}
```
- where `$CLIENT_ID` is the Client ID from step 1 ([see here](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#:~:text=scope%E2%80%AFopenid.-,resource,-optional)).
- This is required for the upstream OIDC provider to return the requested claims.
+ where `$CLIENT_ID` is the Client ID from step 1
+ ([see here](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#:~:text=scope%E2%80%AFopenid.-,resource,-optional)).
+ This is required for the upstream OIDC provider to return the requested
+ claims.
- `CODER_OIDC_IGNORE_USERINFO`: Set to `true`.
-1. Configure [Issuance Transform Rules](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-rule-to-send-ldap-attributes-as-claims)
+1. Configure
+ [Issuance Transform Rules](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-rule-to-send-ldap-attributes-as-claims)
on your federation server to send the following claims:
- `preferred_username`: You can use e.g. "Display Name" as required.
- - `email`: You can use e.g. the LDAP attribute "E-Mail-Addresses" as required.
+ - `email`: You can use e.g. the LDAP attribute "E-Mail-Addresses" as
+ required.
- `email_verified`: Create a custom claim rule:
```console
=> issue(Type = "email_verified", Value = "true")
```
- - (Optional) If using Group Sync, send the required groups in the configured groups claim field. See [here](https://stackoverflow.com/a/55570286) for an example.
+ - (Optional) If using Group Sync, send the required groups in the configured
+ groups claim field. See [here](https://stackoverflow.com/a/55570286) for an
+ example.
### Keycloak
-The access_type parameter has two possible values: "online" and "offline." By default, the value is set to "offline". This means that when a user authenticates using OIDC, the application requests offline access to the user's resources, including the ability to refresh access tokens without requiring the user to reauthenticate.
-
-To enable the `offline_access` scope, which allows for the refresh token functionality, you need to add it to the list of requested scopes during the authentication flow. Including the `offline_access` scope in the requested scopes ensures that the user is granted the necessary permissions to obtain refresh tokens.
-
-By combining the `{"access_type":"offline"}` parameter in the OIDC Auth URL with the `offline_access` scope, you can achieve the desired behavior of obtaining refresh tokens for offline access to the user's resources.
+The access_type parameter has two possible values: "online" and "offline." By
+default, the value is set to "offline". This means that when a user
+authenticates using OIDC, the application requests offline access to the user's
+resources, including the ability to refresh access tokens without requiring the
+user to reauthenticate.
+
+To enable the `offline_access` scope, which allows for the refresh token
+functionality, you need to add it to the list of requested scopes during the
+authentication flow. Including the `offline_access` scope in the requested
+scopes ensures that the user is granted the necessary permissions to obtain
+refresh tokens.
+
+By combining the `{"access_type":"offline"}` parameter in the OIDC Auth URL with
+the `offline_access` scope, you can achieve the desired behavior of obtaining
+refresh tokens for offline access to the user's resources.
diff --git a/docs/admin/automation.md b/docs/admin/automation.md
index c564903d55361..c9fc78833033b 100644
--- a/docs/admin/automation.md
+++ b/docs/admin/automation.md
@@ -1,22 +1,24 @@
# Automation
-All actions possible through the Coder dashboard can also be automated as it utilizes the same public REST API. There are several ways to extend/automate Coder:
+All actions possible through the Coder dashboard can also be automated as it
+utilizes the same public REST API. There are several ways to extend/automate
+Coder:
- [CLI](../cli.md)
- [REST API](../api/)
-- [Coder SDK](https://pkg.go.dev/github.com/coder/coder/codersdk)
+- [Coder SDK](https://pkg.go.dev/github.com/coder/coder/v2/codersdk)
## Quickstart
Generate a token on your Coder deployment by visiting:
-```sh
+```shell
https://coder.example.com/settings/tokens
```
List your workspaces
-```sh
+```shell
# CLI
coder ls \
--url https://coder.example.com \
@@ -30,23 +32,34 @@ curl https://coder.example.com/api/v2/workspaces?q=owner:me \
## Documentation
-We publish an [API reference](../api/index.md) in our documentation. You can also enable a [Swagger endpoint](../cli/server.md#--swagger-enable) on your Coder deployment.
+We publish an [API reference](../api/index.md) in our documentation. You can
+also enable a [Swagger endpoint](../cli/server.md#--swagger-enable) on your
+Coder deployment.
## Use cases
-We strive to keep the following use cases up to date, but please note that changes to API queries and routes can occur. For the most recent queries and payloads, we recommend checking the CLI and API documentation.
+We strive to keep the following use cases up to date, but please note that
+changes to API queries and routes can occur. For the most recent queries and
+payloads, we recommend checking the CLI and API documentation.
### Templates
-- [Update templates in CI](../templates/change-management.md): Store all templates and git and update templates in CI/CD pipelines.
+- [Update templates in CI](../templates/change-management.md): Store all
+ templates and git and update templates in CI/CD pipelines.
### Workspace agents
-Workspace agents have a special token that can send logs, metrics, and workspace activity.
+Workspace agents have a special token that can send logs, metrics, and workspace
+activity.
-- [Custom workspace logs](../api/agents.md#patch-workspace-agent-logs): Expose messages prior to the Coder init script running (e.g. pulling image, VM starting, restoring snapshot). [coder-logstream-kube](https://github.com/coder/coder-logstream-kube) uses this to show Kubernetes events, such as image pulls or ResourceQuota restrictions.
+- [Custom workspace logs](../api/agents.md#patch-workspace-agent-logs): Expose
+ messages prior to the Coder init script running (e.g. pulling image, VM
+ starting, restoring snapshot).
+ [coder-logstream-kube](https://github.com/coder/coder-logstream-kube) uses
+ this to show Kubernetes events, such as image pulls or ResourceQuota
+ restrictions.
- ```sh
+ ```shell
curl -X PATCH https://coder.example.com/api/v2/workspaceagents/me/logs \
-H "Coder-Session-Token: $CODER_AGENT_TOKEN" \
-d "{
@@ -60,9 +73,11 @@ Workspace agents have a special token that can send logs, metrics, and workspace
}"
```
-- [Manually send workspace activity](../api/agents.md#submit-workspace-agent-stats): Keep a workspace "active," even if there is not an open connection (e.g. for a long-running machine learning job).
+- [Manually send workspace activity](../api/agents.md#submit-workspace-agent-stats):
+ Keep a workspace "active," even if there is not an open connection (e.g. for a
+ long-running machine learning job).
- ```sh
+ ```shell
#!/bin/bash
# Send workspace activity as long as the job is still running
diff --git a/docs/admin/configure.md b/docs/admin/configure.md
index e74d447c0b4e1..17ce483cb2f0f 100644
--- a/docs/admin/configure.md
+++ b/docs/admin/configure.md
@@ -1,23 +1,26 @@
-Coder server's primary configuration is done via environment variables. For a full list of the options, run `coder server --help` or see our [CLI documentation](../cli/server.md).
+Coder server's primary configuration is done via environment variables. For a
+full list of the options, run `coder server --help` or see our
+[CLI documentation](../cli/server.md).
## Access URL
-`CODER_ACCESS_URL` is required if you are not using the tunnel. Set this to the external URL
-that users and workspaces use to connect to Coder (e.g. ). This
-should not be localhost.
+`CODER_ACCESS_URL` is required if you are not using the tunnel. Set this to the
+external URL that users and workspaces use to connect to Coder (e.g.
+). This should not be localhost.
-> Access URL should be a external IP address or domain with DNS records pointing to Coder.
+> Access URL should be a external IP address or domain with DNS records pointing
+> to Coder.
### Tunnel
-If an access URL is not specified, Coder will create
-a publicly accessible URL to reverse proxy your deployment for simple setup.
+If an access URL is not specified, Coder will create a publicly accessible URL
+to reverse proxy your deployment for simple setup.
## Address
You can change which port(s) Coder listens on.
-```sh
+```shell
# Listen on port 80
export CODER_HTTP_ADDRESS=0.0.0.0:80
@@ -34,37 +37,76 @@ coder server
## Wildcard access URL
-`CODER_WILDCARD_ACCESS_URL` is necessary for [port forwarding](../networking/port-forwarding.md#dashboard)
-via the dashboard or running [coder_apps](../templates/index.md#coder-apps) on an absolute path. Set this to a wildcard
-subdomain that resolves to Coder (e.g. `*.coder.example.com`).
+`CODER_WILDCARD_ACCESS_URL` is necessary for
+[port forwarding](../networking/port-forwarding.md#dashboard) via the dashboard
+or running [coder_apps](../templates/index.md#coder-apps) on an absolute path.
+Set this to a wildcard subdomain that resolves to Coder (e.g.
+`*.coder.example.com`).
If you are providing TLS certificates directly to the Coder server, either
1. Use a single certificate and key for both the root and wildcard domains.
2. Configure multiple certificates and keys via
- [`coder.tls.secretNames`](https://github.com/coder/coder/blob/main/helm/values.yaml) in the Helm Chart, or
- [`--tls-cert-file`](../cli/server.md#--tls-cert-file) and [`--tls-key-file`](../cli/server.md#--tls-key-file) command
- line options (these both take a comma separated list of files; list certificates and their respective keys in the
- same order).
+ [`coder.tls.secretNames`](https://github.com/coder/coder/blob/main/helm/coder/values.yaml)
+ in the Helm Chart, or [`--tls-cert-file`](../cli/server.md#--tls-cert-file)
+ and [`--tls-key-file`](../cli/server.md#--tls-key-file) command line options
+ (these both take a comma separated list of files; list certificates and their
+ respective keys in the same order).
## TLS & Reverse Proxy
-The Coder server can directly use TLS certificates with `CODER_TLS_ENABLE` and accompanying configuration flags. However, Coder can also run behind a reverse-proxy to terminate TLS certificates from LetsEncrypt, for example.
+The Coder server can directly use TLS certificates with `CODER_TLS_ENABLE` and
+accompanying configuration flags. However, Coder can also run behind a
+reverse-proxy to terminate TLS certificates from LetsEncrypt, for example.
- [Apache](https://github.com/coder/coder/tree/main/examples/web-server/apache)
- [Caddy](https://github.com/coder/coder/tree/main/examples/web-server/caddy)
- [NGINX](https://github.com/coder/coder/tree/main/examples/web-server/nginx)
+### Kubernetes TLS configuration
+
+Below are the steps to configure Coder to terminate TLS when running on
+Kubernetes. You must have the certificate `.key` and `.crt` files in your
+working directory prior to step 1.
+
+1. Create the TLS secret in your Kubernetes cluster
+
+```shell
+kubectl create secret tls coder-tls -n --key="tls.key" --cert="tls.crt"
+```
+
+> You can use a single certificate for the both the access URL and wildcard
+> access URL. The certificate CN must match the wildcard domain, such as
+> `*.example.coder.com`.
+
+1. Reference the TLS secret in your Coder Helm chart values
+
+```yaml
+coder:
+ tls:
+ secretName:
+ - coder-tls
+
+ # Alternatively, if you use an Ingress controller to terminate TLS,
+ # set the following values:
+ ingress:
+ enable: true
+ secretName: coder-tls
+ wildcardSecretName: coder-tls
+```
+
## PostgreSQL Database
-Coder uses a PostgreSQL database to store users, workspace metadata, and other deployment information.
-Use `CODER_PG_CONNECTION_URL` to set the database that Coder connects to. If unset, PostgreSQL binaries will be
-downloaded from Maven () and store all data in the config root.
+Coder uses a PostgreSQL database to store users, workspace metadata, and other
+deployment information. Use `CODER_PG_CONNECTION_URL` to set the database that
+Coder connects to. If unset, PostgreSQL binaries will be downloaded from Maven
+() and store all data in the config root.
> Postgres 13 is the minimum supported version.
If you are using the built-in PostgreSQL deployment and need to use `psql` (aka
-the PostgreSQL interactive terminal), output the connection URL with the following command:
+the PostgreSQL interactive terminal), output the connection URL with the
+following command:
```console
coder server postgres-builtin-url
@@ -73,21 +115,26 @@ psql "postgres://coder@localhost:49627/coder?sslmode=disable&password=feU...yI1"
### Migrating from the built-in database to an external database
-To migrate from the built-in database to an external database, follow these steps:
+To migrate from the built-in database to an external database, follow these
+steps:
1. Stop your Coder deployment.
2. Run `coder server postgres-builtin-serve` in a background terminal.
3. Run `coder server postgres-builtin-url` and copy its output command.
-4. Run `pg_dump > coder.sql` to dump the internal database to a file.
-5. Restore that content to an external database with `psql < coder.sql`.
-6. Start your Coder deployment with `CODER_PG_CONNECTION_URL=`.
+4. Run `pg_dump > coder.sql` to dump the internal
+ database to a file.
+5. Restore that content to an external database with
+ `psql < coder.sql`.
+6. Start your Coder deployment with
+ `CODER_PG_CONNECTION_URL=`.
## System packages
-If you've installed Coder via a [system package](../install/packages.md) Coder, you can
-configure the server by setting the following variables in `/etc/coder.d/coder.env`:
+If you've installed Coder via a [system package](../install/packages.md) Coder,
+you can configure the server by setting the following variables in
+`/etc/coder.d/coder.env`:
-```console
+```env
# String. Specifies the external URL (HTTP/S) to access Coder.
CODER_ACCESS_URL=https://coder.example.com
@@ -115,7 +162,7 @@ CODER_TLS_KEY_FILE=
To run Coder as a system service on the host:
-```console
+```shell
# Use systemd to start Coder now and on reboot
sudo systemctl enable --now coder
@@ -125,15 +172,15 @@ journalctl -u coder.service -b
To restart Coder after applying system changes:
-```console
+```shell
sudo systemctl restart coder
```
## Configuring Coder behind a proxy
-To configure Coder behind a corporate proxy, set the environment variables `HTTP_PROXY` and
-`HTTPS_PROXY`. Be sure to restart the server. Lowercase values (e.g. `http_proxy`) are also
-respected in this case.
+To configure Coder behind a corporate proxy, set the environment variables
+`HTTP_PROXY` and `HTTPS_PROXY`. Be sure to restart the server. Lowercase values
+(e.g. `http_proxy`) are also respected in this case.
## Up Next
diff --git a/docs/admin/git-providers.md b/docs/admin/git-providers.md
index 293c88ab3cabb..0cbd0e00c94fa 100644
--- a/docs/admin/git-providers.md
+++ b/docs/admin/git-providers.md
@@ -1,10 +1,13 @@
# Git Providers
-Coder integrates with git providers to automate away the need for developers to authenticate with repositories within their workspace.
+Coder integrates with git providers to automate away the need for developers to
+authenticate with repositories within their workspace.
## How it works
-When developers use `git` inside their workspace, they are prompted to authenticate. After that, Coder will store and refresh tokens for future operations.
+When developers use `git` inside their workspace, they are prompted to
+authenticate. After that, Coder will store and refresh tokens for future
+operations.
@@ -13,18 +16,22 @@ Your browser does not support the video tag.
## Configuration
-To add a git provider, you'll need to create an OAuth application. The following providers are supported:
+To add a git provider, you'll need to create an OAuth application. The following
+providers are supported:
- [GitHub](#github-app)
- [GitLab](https://docs.gitlab.com/ee/integration/oauth_provider.html)
- [BitBucket](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/)
- [Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops)
-Example callback URL: `https://coder.example.com/gitauth/primary-github/callback`. Use an arbitrary ID for your provider (e.g. `primary-github`).
+Example callback URL:
+`https://coder.example.com/gitauth/primary-github/callback`. Use an arbitrary ID
+for your provider (e.g. `primary-github`).
-Set the following environment variables to [configure the Coder server](./configure.md):
+Set the following environment variables to
+[configure the Coder server](./configure.md):
-```console
+```env
CODER_GITAUTH_0_ID="primary-github"
CODER_GITAUTH_0_TYPE=github|gitlab|azure-devops|bitbucket
CODER_GITAUTH_0_CLIENT_ID=xxxxxx
@@ -33,11 +40,15 @@ CODER_GITAUTH_0_CLIENT_SECRET=xxxxxxx
### GitHub
-1. [Create a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) to enable fine-grained access to specific repositories, or a subset of permissions for security.
+1. [Create a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)
+ to enable fine-grained access to specific repositories, or a subset of
+ permissions for security.

-2. Adjust the GitHub App permissions. You can use more or less permissions than are listed here, this is merely a suggestion that allows users to clone repositories:
+2. Adjust the GitHub App permissions. You can use more or less permissions than
+ are listed here, this is merely a suggestion that allows users to clone
+ repositories:

@@ -48,7 +59,8 @@ CODER_GITAUTH_0_CLIENT_SECRET=xxxxxxx
| Workflows | Read & Write | Grants access to update files in `.github/workflows/`. |
| Metadata | Read-only | Grants access to metadata written by GitHub Apps. |
-3. Install the App for your organization. You may select a subset of repositories to grant access to.
+3. Install the App for your organization. You may select a subset of
+ repositories to grant access to.

@@ -56,7 +68,7 @@ CODER_GITAUTH_0_CLIENT_SECRET=xxxxxxx
GitHub Enterprise requires the following authentication and token URLs:
-```console
+```env
CODER_GITAUTH_0_VALIDATE_URL="https://github.example.com/login/oauth/access_token/info"
CODER_GITAUTH_0_AUTH_URL="https://github.example.com/login/oauth/authorize"
CODER_GITAUTH_0_TOKEN_URL="https://github.example.com/login/oauth/access_token"
@@ -66,7 +78,7 @@ CODER_GITAUTH_0_TOKEN_URL="https://github.example.com/login/oauth/access_token"
Azure DevOps requires the following environment variables:
-```console
+```env
CODER_GITAUTH_0_ID="primary-azure-devops"
CODER_GITAUTH_0_TYPE=azure-devops
CODER_GITAUTH_0_CLIENT_ID=xxxxxx
@@ -78,10 +90,10 @@ CODER_GITAUTH_0_TOKEN_URL="https://app.vssps.visualstudio.com/oauth2/token"
### Self-managed git providers
-Custom authentication and token URLs should be
-used for self-managed Git provider deployments.
+Custom authentication and token URLs should be used for self-managed Git
+provider deployments.
-```console
+```env
CODER_GITAUTH_0_AUTH_URL="https://github.example.com/oauth/authorize"
CODER_GITAUTH_0_TOKEN_URL="https://github.example.com/oauth/token"
CODER_GITAUTH_0_VALIDATE_URL="https://your-domain.com/oauth/token/info"
@@ -91,7 +103,7 @@ CODER_GITAUTH_0_VALIDATE_URL="https://your-domain.com/oauth/token/info"
Optionally, you can request custom scopes:
-```console
+```env
CODER_GITAUTH_0_SCOPES="repo:read repo:write write:gpg_key"
```
@@ -99,9 +111,10 @@ CODER_GITAUTH_0_SCOPES="repo:read repo:write write:gpg_key"
Multiple providers are an Enterprise feature. [Learn more](../enterprise.md).
-A custom regex can be used to match a specific repository or organization to limit auth scope. Here's a sample config:
+A custom regex can be used to match a specific repository or organization to
+limit auth scope. Here's a sample config:
-```console
+```env
# Provider 1) github.com
CODER_GITAUTH_0_ID=primary-github
CODER_GITAUTH_0_TYPE=github
@@ -120,20 +133,24 @@ CODER_GITAUTH_1_TOKEN_URL="https://github.example.com/login/oauth/access_token"
CODER_GITAUTH_1_VALIDATE_URL="https://github.example.com/login/oauth/access_token/info"
```
-To support regex matching for paths (e.g. github.com/orgname), you'll need to add this to the [Coder agent startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script):
+To support regex matching for paths (e.g. github.com/orgname), you'll need to
+add this to the
+[Coder agent startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script):
-```console
+```shell
git config --global credential.useHttpPath true
```
## Require git authentication in templates
-If your template requires git authentication (e.g. running `git clone` in the [startup_script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)), you can require users authenticate via git prior to creating a workspace:
+If your template requires git authentication (e.g. running `git clone` in the
+[startup_script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)),
+you can require users authenticate via git prior to creating a workspace:

-The following example will require users authenticate via GitHub and auto-clone a repo
-into the `~/coder` directory.
+The following example will require users authenticate via GitHub and auto-clone
+a repo into the `~/coder` directory.
```hcl
data "coder_git_auth" "github" {
@@ -156,4 +173,6 @@ EOF
}
```
-See the [Terraform provider documentation](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/git_auth) for all available options.
+See the
+[Terraform provider documentation](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/git_auth)
+for all available options.
diff --git a/docs/admin/groups.md b/docs/admin/groups.md
index 47ecf9e11ffe5..6d0c3ca765843 100644
--- a/docs/admin/groups.md
+++ b/docs/admin/groups.md
@@ -1,9 +1,12 @@
# Groups
-Groups can be used with [template RBAC](./rbac.md) to give groups of users access to specific templates. They can be defined in Coder or [synced from your identity provider](./auth.md#group-sync-enterprise).
+Groups can be used with [template RBAC](./rbac.md) to give groups of users
+access to specific templates. They can be defined in Coder or
+[synced from your identity provider](./auth.md#group-sync-enterprise).

## Enabling this feature
-This feature is only available with an enterprise license. [Learn more](../enterprise.md)
+This feature is only available with an enterprise license.
+[Learn more](../enterprise.md)
diff --git a/docs/admin/high-availability.md b/docs/admin/high-availability.md
index 430c0c8dbb4c4..5423c9597b4ed 100644
--- a/docs/admin/high-availability.md
+++ b/docs/admin/high-availability.md
@@ -1,27 +1,41 @@
# High Availability
-High Availability (HA) mode solves for horizontal scalability and automatic failover
-within a single region. When in HA mode, Coder continues using a single Postgres
-endpoint. [GCP](https://cloud.google.com/sql/docs/postgres/high-availability), [AWS](https://docs.aws.amazon.com/prescriptive-guidance/latest/saas-multitenant-managed-postgresql/availability.html),
+High Availability (HA) mode solves for horizontal scalability and automatic
+failover within a single region. When in HA mode, Coder continues using a single
+Postgres endpoint.
+[GCP](https://cloud.google.com/sql/docs/postgres/high-availability),
+[AWS](https://docs.aws.amazon.com/prescriptive-guidance/latest/saas-multitenant-managed-postgresql/availability.html),
and other cloud vendors offer fully-managed HA Postgres services that pair
nicely with Coder.
-For Coder to operate correctly, every node must be within 10ms of each other
-and Postgres. We make a best-effort attempt to warn the user when inter-Coder
-latency is too high, but if requests start dropping, this is one metric to investigate.
+For Coder to operate correctly, Coderd instances should have low-latency
+connections to each other so that they can effectively relay traffic between
+users and workspaces no matter which Coderd instance users or workspaces connect
+to. We make a best-effort attempt to warn the user when inter-Coderd latency is
+too high, but if requests start dropping, this is one metric to investigate.
+
+We also recommend that you deploy all Coderd instances such that they have
+low-latency connections to Postgres. Coderd often makes several database
+round-trips while processing a single API request, so prioritizing low-latency
+between Coderd and Postgres is more important than low-latency between users and
+Coderd.
+
Note that this latency requirement applies _only_ to Coder services. Coder will
-operate correctly even with few seconds of latency on
-workspace <-> Coder and user <-> Coder connections.
+operate correctly even with few seconds of latency on workspace <-> Coder and
+user <-> Coder connections.
## Setup
-Coder automatically enters HA mode when multiple instances simultaneously connect
-to the same Postgres endpoint.
+Coder automatically enters HA mode when multiple instances simultaneously
+connect to the same Postgres endpoint.
-HA brings one configuration variable to set in each Coder
-node: `CODER_DERP_SERVER_RELAY_URL`. The HA nodes use these URLs to communicate
-with each other. Inter-node communication is only required while using the
-embedded relay (default). If you're using [custom relays](../networking/index.md#custom-relays), Coder ignores `CODER_DERP_SERVER_RELAY_URL` since Postgres is the sole rendezvous for the Coder nodes.
+HA brings one configuration variable to set in each Coderd node:
+`CODER_DERP_SERVER_RELAY_URL`. The HA nodes use these URLs to communicate with
+each other. Inter-node communication is only required while using the embedded
+relay (default). If you're using
+[custom relays](../networking/index.md#custom-relays), Coder ignores
+`CODER_DERP_SERVER_RELAY_URL` since Postgres is the sole rendezvous for the
+Coder nodes.
`CODER_DERP_SERVER_RELAY_URL` will never be `CODER_ACCESS_URL` because
`CODER_ACCESS_URL` is a load balancer to all Coder nodes.
diff --git a/docs/admin/prometheus.md b/docs/admin/prometheus.md
index af8928bed6ba0..3af6a08466edb 100644
--- a/docs/admin/prometheus.md
+++ b/docs/admin/prometheus.md
@@ -1,16 +1,25 @@
# Prometheus
-Coder exposes many metrics which can be consumed by a Prometheus server, and give insight into the current state of a live Coder deployment.
+Coder exposes many metrics which can be consumed by a Prometheus server, and
+give insight into the current state of a live Coder deployment.
-If you don't have an Prometheus server installed, you can follow the Prometheus [Getting started](https://prometheus.io/docs/prometheus/latest/getting_started/) guide.
+If you don't have an Prometheus server installed, you can follow the Prometheus
+[Getting started](https://prometheus.io/docs/prometheus/latest/getting_started/)
+guide.
## Enable Prometheus metrics
-Coder server exports metrics via the HTTP endpoint, which can be enabled using either the environment variable `CODER_PROMETHEUS_ENABLE` or the flag `--prometheus-enable`.
+Coder server exports metrics via the HTTP endpoint, which can be enabled using
+either the environment variable `CODER_PROMETHEUS_ENABLE` or the flag
+`--prometheus-enable`.
-The Prometheus endpoint address is `http://localhost:2112/` by default. You can use either the environment variable `CODER_PROMETHEUS_ADDRESS` or the flag ` --prometheus-address :` to select a different listen address.
+The Prometheus endpoint address is `http://localhost:2112/` by default. You can
+use either the environment variable `CODER_PROMETHEUS_ADDRESS` or the flag
+`--prometheus-address :` to select a different listen
+address.
-If `coder server --prometheus-enable` is started locally, you can preview the metrics endpoint in your browser or by using curl: http://localhost:2112/.
+If `coder server --prometheus-enable` is started locally, you can preview the
+metrics endpoint in your browser or by using curl:
```console
$ curl http://localhost:2112/
@@ -22,14 +31,17 @@ coderd_api_active_users_duration_hour 0
### Kubernetes deployment
-The Prometheus endpoint can be enabled in the [Helm chart's](https://github.com/coder/coder/tree/main/helm) `values.yml` by setting the environment variable `CODER_PROMETHEUS_ADDRESS` to `0.0.0.0:2112`.
-The environment variable `CODER_PROMETHEUS_ENABLE` will be enabled automatically.
+The Prometheus endpoint can be enabled in the
+[Helm chart's](https://github.com/coder/coder/tree/main/helm) `values.yml` by
+setting the environment variable `CODER_PROMETHEUS_ADDRESS` to `0.0.0.0:2112`.
+The environment variable `CODER_PROMETHEUS_ENABLE` will be enabled
+automatically.
### Prometheus configuration
-To allow Prometheus to scrape the Coder metrics, you will need to create a `scape_config`
-in your `prometheus.yml` file, or in the Prometheus Helm chart values. Below is an
-example `scrape_config`:
+To allow Prometheus to scrape the Coder metrics, you will need to create a
+`scape_config` in your `prometheus.yml` file, or in the Prometheus Helm chart
+values. Below is an example `scrape_config`:
```yaml
scrape_configs:
diff --git a/docs/admin/provisioners.md b/docs/admin/provisioners.md
index 8f733fdccaa7d..47dd9188f427d 100644
--- a/docs/admin/provisioners.md
+++ b/docs/admin/provisioners.md
@@ -1,37 +1,63 @@
# Provisioners
-By default, the Coder server runs [built-in provisioner daemons](../cli/server.md#provisioner-daemons), which execute `terraform` during workspace and template builds. However, there are sometimes benefits to running external provisioner daemons:
+By default, the Coder server runs
+[built-in provisioner daemons](../cli/server.md#provisioner-daemons), which
+execute `terraform` during workspace and template builds. However, there are
+sometimes benefits to running external provisioner daemons:
-- **Secure build environments:** Run build jobs in isolated containers, preventing malicious templates from gaining shell access to the Coder host.
+- **Secure build environments:** Run build jobs in isolated containers,
+ preventing malicious templates from gaining shell access to the Coder host.
-- **Isolate APIs:** Deploy provisioners in isolated environments (on-prem, AWS, Azure) instead of exposing APIs (Docker, Kubernetes, VMware) to the Coder server. See [Provider Authentication](../templates/authentication.md) for more details.
+- **Isolate APIs:** Deploy provisioners in isolated environments (on-prem, AWS,
+ Azure) instead of exposing APIs (Docker, Kubernetes, VMware) to the Coder
+ server. See [Provider Authentication](../templates/authentication.md) for more
+ details.
-- **Isolate secrets**: Keep Coder unaware of cloud secrets, manage/rotate secrets on provisoner servers.
+- **Isolate secrets**: Keep Coder unaware of cloud secrets, manage/rotate
+ secrets on provisoner servers.
-- **Reduce server load**: External provisioners reduce load and build queue times from the Coder server. See [Scaling Coder](./scale.md#concurrent-workspace-builds) for more details.
+- **Reduce server load**: External provisioners reduce load and build queue
+ times from the Coder server. See
+ [Scaling Coder](./scale.md#concurrent-workspace-builds) for more details.
-> External provisioners are in an [alpha state](../contributing/feature-stages.md#alpha-features) and the behavior is subject to change. Use [GitHub issues](https://github.com/coder/coder) to leave feedback.
+Each provisioner can run a single
+[concurrent workspace build](./scale.md#concurrent-workspace-builds). For
+example, running 30 provisioner containers will allow 30 users to start
+workspaces at the same time.
-## Running external provisioners
+Provisioners are started with the
+[coder provisionerd start](../cli/provisionerd_start.md) command.
-Each provisioner can run a single [concurrent workspace build](./scale.md#concurrent-workspace-builds). For example, running 30 provisioner containers will allow 30 users to start workspaces at the same time.
+## Authentication
-### Requirements
+The provisioner daemon must authenticate with your Coder deployment.
-- The [Coder CLI](../cli.md) must installed on and authenticated as a user with the Owner or Template Admin role.
-- Your environment must be [authenticated](../templates/authentication.md) against the cloud environments templates need to provision against.
+Set a
+[provisioner daemon pre-shared key (PSK)](../cli/server.md#--provisioner-daemon-psk)
+on the Coder server and start the provisioner with
+`coder provisionerd start --psk `. If you are
+[installing with Helm](../install/kubernetes.md#install-coder-with-helm), see
+the [Helm example](#example-running-an-external-provisioner-with-helm) below.
-### Types of provisioners
+> Coder still supports authenticating the provisioner daemon with a
+> [token](../cli.md#--token) from a user with the Template Admin or Owner role.
+> This method is deprecated in favor of the PSK, which only has permission to
+> access provisioner daemon APIs. We recommend migrating to the PSK as soon as
+> practical.
-- **Generic provisioners** can pick up any build job from templates without provisioner tags.
+## Types of provisioners
- ```sh
+- **Generic provisioners** can pick up any build job from templates without
+ provisioner tags.
+
+ ```shell
coder provisionerd start
```
-- **Tagged provisioners** can be used to pick up build jobs from templates (and corresponding workspaces) with matching tags.
+- **Tagged provisioners** can be used to pick up build jobs from templates (and
+ corresponding workspaces) with matching tags.
- ```sh
+ ```shell
coder provisionerd start \
--tag environment=on_prem \
--tag data_center=chicago
@@ -47,11 +73,16 @@ Each provisioner can run a single [concurrent workspace build](./scale.md#concur
--provisioner-tag data_center=chicago
```
- > At this time, tagged provisioners can also pick jobs from untagged templates. This behavior is [subject to change](https://github.com/coder/coder/issues/6442).
+ > At this time, tagged provisioners can also pick jobs from untagged
+ > templates. This behavior is
+ > [subject to change](https://github.com/coder/coder/issues/6442).
-- **User provisioners** can only pick up jobs from user-tagged templates. Unlike the other provisioner types, any Coder can run user provisioners, but they have no impact unless there is at least one template with the `scope=user` provisioner tag.
+- **User provisioners** can only pick up jobs from user-tagged templates. Unlike
+ the other provisioner types, any Coder can run user provisioners, but they
+ have no impact unless there is at least one template with the `scope=user`
+ provisioner tag.
- ```sh
+ ```shell
coder provisionerd start \
--tag scope=user
@@ -61,18 +92,85 @@ Each provisioner can run a single [concurrent workspace build](./scale.md#concur
--provisioner-tag scope=user
```
-### Example: Running an external provisioner on a VM
+## Example: Running an external provisioner with Helm
+
+Coder provides a Helm chart for running external provisioner daemons, which you
+will use in concert with the Helm chart for deploying the Coder server.
+
+1. Create a long, random pre-shared key (PSK) and store it in a Kubernetes
+ secret
+
+ ```shell
+ kubectl create secret generic coder-provisioner-psk --from-literal=psk=`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 26`
+ ```
+
+1. Modify your Coder `values.yaml` to include
+
+ ```yaml
+ provisionerDaemon:
+ pskSecretName: "coder-provisioner-psk"
+ ```
+
+1. Redeploy Coder with the new `values.yaml` to roll out the PSK. You can omit
+ `--version ` to also upgrade Coder to the latest version.
+
+ ```shell
+ helm upgrade coder coder-v2/coder \
+ --namespace coder \
+ --version \
+ --values values.yaml
+ ```
+
+1. Create a `provisioner-values.yaml` file for the provisioner daemons Helm
+ chart. For example
+
+ ```yaml
+ coder:
+ env:
+ - name: CODER_URL
+ value: "https://coder.example.com"
+ replicaCount: 10
+ provisionerDaemon:
+ pskSecretName: "coder-provisioner-psk"
+ tags:
+ location: auh
+ kind: k8s
+ ```
+
+ This example creates a deployment of 10 provisioner daemons (for 10
+ concurrent builds) with the listed tags. For generic provisioners, remove the
+ tags.
+
+ > Refer to the
+ > [values.yaml](https://github.com/coder/coder/blob/main/helm/provisioner/values.yaml)
+ > file for the coder-provisioner chart for information on what values can be
+ > specified.
+
+1. Install the provisioner daemon chart
+
+ ```shell
+ helm install coder-provisioner coder-v2/coder-provisioner \
+ --namespace coder \
+ --version \
+ --values provisioner-values.yaml
+ ```
+
+ You can verify that your provisioner daemons have successfully connected to
+ Coderd by looking for a debug log message that says
+ `provisionerd: successfully connected to coderd` from each Pod.
+
+## Example: Running an external provisioner on a VM
-```sh
+```shell
curl -L https://coder.com/install.sh | sh
export CODER_URL=https://coder.example.com
export CODER_SESSION_TOKEN=your_token
coder provisionerd start
```
-### Example: Running an external provisioner via Docker
+## Example: Running an external provisioner via Docker
-```sh
+```shell
docker run --rm -it \
-e CODER_URL=https://coder.example.com/ \
-e CODER_SESSION_TOKEN=your_token \
@@ -83,8 +181,10 @@ docker run --rm -it \
## Disable built-in provisioners
-As mentioned above, the Coder server will run built-in provisioners by default. This can be disabled with a server-wide [flag or environment variable](../cli/server.md#provisioner-daemons).
+As mentioned above, the Coder server will run built-in provisioners by default.
+This can be disabled with a server-wide
+[flag or environment variable](../cli/server.md#provisioner-daemons).
-```sh
+```shell
coder server --provisioner-daemons=0
```
diff --git a/docs/admin/quotas.md b/docs/admin/quotas.md
index bd52fd668c32a..aa12cf328c4d1 100644
--- a/docs/admin/quotas.md
+++ b/docs/admin/quotas.md
@@ -2,19 +2,19 @@
Quotas are a mechanism for controlling spend by associating costs with workspace
templates and assigning budgets to users. Users that exceed their budget will be
-blocked from launching more workspaces until they either delete their other workspaces
-or get their budget extended.
+blocked from launching more workspaces until they either delete their other
+workspaces or get their budget extended.
-For example: A template is configured with a cost of 5 credits per day,
-and the user is granted 15 credits, which can be consumed by both started and
-stopped workspaces. This budget limits the user to 3 concurrent workspaces.
+For example: A template is configured with a cost of 5 credits per day, and the
+user is granted 15 credits, which can be consumed by both started and stopped
+workspaces. This budget limits the user to 3 concurrent workspaces.
Quotas are licensed with [Groups](./groups.md).
## Definitions
-- **Credits** is the fundamental unit representing cost in the quota system. This integer
- can be arbitrary, or it can map to your preferred currency.
+- **Credits** is the fundamental unit representing cost in the quota system.
+ This integer can be arbitrary, or it can map to your preferred currency.
- **Budget** is the per-user, enforced, upper limit to credit spend.
- **Allowance** is a grant of credits to the budget.
@@ -22,10 +22,11 @@ Quotas are licensed with [Groups](./groups.md).
Templates describe their cost through the `daily_cost` attribute in
[`resource_metadata`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/metadata).
-Since costs are associated with resources, an offline workspace may consume
-less quota than an online workspace.
+Since costs are associated with resources, an offline workspace may consume less
+quota than an online workspace.
-A common use case is separating costs for a persistent volume and ephemeral compute:
+A common use case is separating costs for a persistent volume and ephemeral
+compute:
```hcl
resource "docker_volume" "home_volume" {
@@ -56,11 +57,11 @@ resource "coder_metadata" "workspace" {
```
When the workspace above is shut down, the `docker_container` and
-`coder_metadata` both get deleted. This reduces the cost from 30 credits to
-10 credits.
+`coder_metadata` both get deleted. This reduces the cost from 30 credits to 10
+credits.
-Resources without a `daily_cost` value are considered to cost 0. If the cost
-was removed on the `docker_volume` above, the template would consume 0 credits when
+Resources without a `daily_cost` value are considered to cost 0. If the cost was
+removed on the `docker_volume` above, the template would consume 0 credits when
it's offline. This technique is good for incentivizing users to shut down their
unused workspaces and freeing up compute in the cluster.
@@ -92,10 +93,10 @@ By default, groups are assumed to have a default allowance of 0.
## Quota Enforcement
-Coder enforces Quota on workspace start and stop operations. The workspace
-build process dynamically calculates costs, so quota violation fails builds
-as opposed to failing the build-triggering operation. For example, the Workspace
-Create Form will never get held up by quota enforcement.
+Coder enforces Quota on workspace start and stop operations. The workspace build
+process dynamically calculates costs, so quota violation fails builds as opposed
+to failing the build-triggering operation. For example, the Workspace Create
+Form will never get held up by quota enforcement.

diff --git a/docs/admin/rbac.md b/docs/admin/rbac.md
index 211f134443889..554650ea675b8 100644
--- a/docs/admin/rbac.md
+++ b/docs/admin/rbac.md
@@ -1,10 +1,13 @@
# Role Based Access Control (RBAC)
-Use RBAC to define which users and [groups](./groups.md) can use specific templates in Coder. These can be defined in Coder or [synced from your identity provider](./auth.md)
+Use RBAC to define which users and [groups](./groups.md) can use specific
+templates in Coder. These can be defined in Coder or
+[synced from your identity provider](./auth.md)

-The "Everyone" group makes a template accessible to all users. This can be removed to make a template private.
+The "Everyone" group makes a template accessible to all users. This can be
+removed to make a template private.
## Permissions
@@ -15,4 +18,5 @@ You can set the following permissions:
## Enabling this feature
-This feature is only available with an enterprise license. [Learn more](../enterprise.md)
+This feature is only available with an enterprise license.
+[Learn more](../enterprise.md)
diff --git a/docs/admin/scale.md b/docs/admin/scale.md
index 999a30aeae44a..2825deffe88ca 100644
--- a/docs/admin/scale.md
+++ b/docs/admin/scale.md
@@ -1,14 +1,22 @@
-We scale-test Coder with [a built-in utility](#scaletest-utility) that can be used in your environment for insights into how Coder scales with your infrastructure.
+We scale-test Coder with [a built-in utility](#scaletest-utility) that can be
+used in your environment for insights into how Coder scales with your
+infrastructure.
## General concepts
-Coder runs workspace operations in a queue. The number of concurrent builds will be limited to the number of provisioner daemons across all coderd replicas.
-
-- **coderd**: Coder’s primary service. Learn more about [Coder’s architecture](../about/architecture.md)
-- **coderd replicas**: Replicas (often via Kubernetes) for high availability, this is an [enterprise feature](../enterprise.md)
-- **concurrent workspace builds**: Workspace operations (e.g. create/stop/delete/apply) across all users
-- **concurrent connections**: Any connection to a workspace (e.g. SSH, web terminal, `coder_app`)
-- **provisioner daemons**: Coder runs one workspace build per provisioner daemon. One coderd replica can host many daemons
+Coder runs workspace operations in a queue. The number of concurrent builds will
+be limited to the number of provisioner daemons across all coderd replicas.
+
+- **coderd**: Coder’s primary service. Learn more about
+ [Coder’s architecture](../about/architecture.md)
+- **coderd replicas**: Replicas (often via Kubernetes) for high availability,
+ this is an [enterprise feature](../enterprise.md)
+- **concurrent workspace builds**: Workspace operations (e.g.
+ create/stop/delete/apply) across all users
+- **concurrent connections**: Any connection to a workspace (e.g. SSH, web
+ terminal, `coder_app`)
+- **provisioner daemons**: Coder runs one workspace build per provisioner
+ daemon. One coderd replica can host many daemons
- **scaletest**: Our scale-testing utility, built into the `coder` command line.
```text
@@ -17,69 +25,114 @@ Coder runs workspace operations in a queue. The number of concurrent builds will
## Infrastructure recommendations
-> Note: The below are guidelines for planning your infrastructure. Your mileage may vary depending on your templates, workflows, and users.
+> Note: The below are guidelines for planning your infrastructure. Your mileage
+> may vary depending on your templates, workflows, and users.
When planning your infrastructure, we recommend you consider the following:
-1. CPU and memory requirements for `coderd`. We recommend allocating 1 CPU core and 2 GB RAM per `coderd` replica at minimum. See [Concurrent users](#concurrent-users) for more details.
-1. CPU and memory requirements for [external provisioners](../admin/provisioners.md#running-external-provisioners), if required. We recommend allocating 1 CPU core and 1 GB RAM per 5 concurrent workspace builds to external provisioners. Note that this may vary depending on the template used. See [Concurrent workspace builds](#concurrent-workspace-builds) for more details. By default, `coderd` runs 3 integrated provisioners.
-1. CPU and memory requirements for the database used by `coderd`. We recommend allocating an additional 1 CPU core to the database used by Coder for every 1000 active users.
-1. CPU and memory requirements for workspaces created by Coder. This will vary depending on users' needs. However, the Coder agent itself requires at minimum 0.1 CPU cores and 256 MB to run inside a workspace.
+1. CPU and memory requirements for `coderd`. We recommend allocating 1 CPU core
+ and 2 GB RAM per `coderd` replica at minimum. See
+ [Concurrent users](#concurrent-users) for more details.
+1. CPU and memory requirements for
+ [external provisioners](../admin/provisioners.md#running-external-provisioners),
+ if required. We recommend allocating 1 CPU core and 1 GB RAM per 5 concurrent
+ workspace builds to external provisioners. Note that this may vary depending
+ on the template used. See
+ [Concurrent workspace builds](#concurrent-workspace-builds) for more details.
+ By default, `coderd` runs 3 integrated provisioners.
+1. CPU and memory requirements for the database used by `coderd`. We recommend
+ allocating an additional 1 CPU core to the database used by Coder for every
+ 1000 active users.
+1. CPU and memory requirements for workspaces created by Coder. This will vary
+ depending on users' needs. However, the Coder agent itself requires at
+ minimum 0.1 CPU cores and 256 MB to run inside a workspace.
### Concurrent users
-We recommend allocating 2 CPU cores and 4 GB RAM per `coderd` replica per 1000 active users.
-We also recommend allocating an additional 1 CPU core to the database used by Coder for every 1000 active users.
-Inactive users do not consume Coder resources, although workspaces configured to auto-start will consume resources when they are built.
+We recommend allocating 2 CPU cores and 4 GB RAM per `coderd` replica per 1000
+active users. We also recommend allocating an additional 1 CPU core to the
+database used by Coder for every 1000 active users. Inactive users do not
+consume Coder resources, although workspaces configured to auto-start will
+consume resources when they are built.
Users' primary mode of accessing Coder will also affect resource requirements.
-If users will be accessing workspaces primarily via Coder's HTTP interface, we recommend doubling the number of cores and RAM allocated per user.
-For example, if you expect 1000 users accessing workspaces via the web, we recommend allocating 4 CPU cores and 8 GB RAM.
+If users will be accessing workspaces primarily via Coder's HTTP interface, we
+recommend doubling the number of cores and RAM allocated per user. For example,
+if you expect 1000 users accessing workspaces via the web, we recommend
+allocating 4 CPU cores and 8 GB RAM.
-Users accessing workspaces via SSH will consume fewer resources, as SSH connections are not proxied through Coder.
+Users accessing workspaces via SSH will consume fewer resources, as SSH
+connections are not proxied through Coder.
### Concurrent workspace builds
-Workspace builds are CPU-intensive, as it relies on Terraform. Various [Terraform providers](https://registry.terraform.io/browse/providers) have different resource requirements.
-When tested with our [kubernetes](https://github.com/coder/coder/tree/main/examples/templates/kubernetes) template, `coderd` will consume roughly 0.25 cores per concurrent workspace build.
-For effective provisioning, our helm chart prefers to schedule [one coderd replica per-node](https://github.com/coder/coder/blob/main/helm/values.yaml#L110-L121).
+Workspace builds are CPU-intensive, as it relies on Terraform. Various
+[Terraform providers](https://registry.terraform.io/browse/providers) have
+different resource requirements. When tested with our
+[kubernetes](https://github.com/coder/coder/tree/main/examples/templates/kubernetes)
+template, `coderd` will consume roughly 0.25 cores per concurrent workspace
+build. For effective provisioning, our helm chart prefers to schedule
+[one coderd replica per-node](https://github.com/coder/coder/blob/main/helm/coder/values.yaml#L188-L202).
We recommend:
-- Running `coderd` on a dedicated set of nodes. This will prevent other workloads from interfering with workspace builds. You can use [node selectors](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector), or [taints and tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) to achieve this.
-- Disabling autoscaling for `coderd` nodes. Autoscaling can cause interruptions for users, see [Autoscaling](#autoscaling) for more details.
-- (Enterprise-only) Running external provisioners instead of Coder's built-in provisioners (`CODER_PROVISIONER_DAEMONS=0`) will separate the load caused by workspace provisioning on the `coderd` nodes. For more details, see [External provisioners](../admin/provisioners.md#running-external-provisioners).
-- Alternatively, if increasing the number of integrated provisioner daemons in `coderd` (`CODER_PROVISIONER_DAEMONS>3`), allocate additional resources to `coderd` to compensate (approx. 0.25 cores and 256 MB per provisioner daemon).
+- Running `coderd` on a dedicated set of nodes. This will prevent other
+ workloads from interfering with workspace builds. You can use
+ [node selectors](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector),
+ or
+ [taints and tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
+ to achieve this.
+- Disabling autoscaling for `coderd` nodes. Autoscaling can cause interruptions
+ for users, see [Autoscaling](#autoscaling) for more details.
+- (Enterprise-only) Running external provisioners instead of Coder's built-in
+ provisioners (`CODER_PROVISIONER_DAEMONS=0`) will separate the load caused by
+ workspace provisioning on the `coderd` nodes. For more details, see
+ [External provisioners](../admin/provisioners.md#running-external-provisioners).
+- Alternatively, if increasing the number of integrated provisioner daemons in
+ `coderd` (`CODER_PROVISIONER_DAEMONS>3`), allocate additional resources to
+ `coderd` to compensate (approx. 0.25 cores and 256 MB per provisioner daemon).
For example, to support 120 concurrent workspace builds:
-- Create a cluster/nodepool with 4 nodes, 8-core each (AWS: `t3.2xlarge` GCP: `e2-highcpu-8`)
-- Run coderd with 4 replicas, 30 provisioner daemons each. (`CODER_PROVISIONER_DAEMONS=30`)
-- Ensure Coder's [PostgreSQL server](./configure.md#postgresql-database) can use up to 2 cores and 4 GB RAM
+- Create a cluster/nodepool with 4 nodes, 8-core each (AWS: `t3.2xlarge` GCP:
+ `e2-highcpu-8`)
+- Run coderd with 4 replicas, 30 provisioner daemons each.
+ (`CODER_PROVISIONER_DAEMONS=30`)
+- Ensure Coder's [PostgreSQL server](./configure.md#postgresql-database) can use
+ up to 2 cores and 4 GB RAM
## Recent scale tests
-> Note: the below information is for reference purposes only, and are not intended to be used as guidelines for infrastructure sizing.
+> Note: the below information is for reference purposes only, and are not
+> intended to be used as guidelines for infrastructure sizing.
| Environment | Coder CPU | Coder RAM | Database | Users | Concurrent builds | Concurrent connections (Terminal/SSH) | Coder Version | Last tested |
| ---------------- | --------- | --------- | ---------------- | ----- | ----------------- | ------------------------------------- | ------------- | ------------ |
-| Kubernetes (GKE) | 3 cores | 12 GB | db-f1-micro | 200 | 3 | 200 (40KB/s web terminal each) | `v0.24.1` | Jun 26, 2023 |
-| Kubernetes (GKE) | 4 cores | 8 GB | db-custom-1-3840 | 1500 | 20 | 1,500 | `v0.24.1` | Jun 27, 2023 |
-| Kubernetes (GKE) | 2 cores | 4 GB | db-custom-1-3840 | 500 | 20 | 500 (640KB/s SSH each) | `v0.27.2` | Jul 27, 2023 |
+| Kubernetes (GKE) | 3 cores | 12 GB | db-f1-micro | 200 | 3 | 200 simulated | `v0.24.1` | Jun 26, 2023 |
+| Kubernetes (GKE) | 4 cores | 8 GB | db-custom-1-3840 | 1500 | 20 | 1,500 simulated | `v0.24.1` | Jun 27, 2023 |
+| Kubernetes (GKE) | 2 cores | 4 GB | db-custom-1-3840 | 500 | 20 | 500 simulated | `v0.27.2` | Jul 27, 2023 |
+
+> Note: a simulated connection reads and writes random data at 40KB/s per
+> connection.
## Scale testing utility
-Since Coder's performance is highly dependent on the templates and workflows you support, you may wish to use our internal scale testing utility against your own environments.
+Since Coder's performance is highly dependent on the templates and workflows you
+support, you may wish to use our internal scale testing utility against your own
+environments.
-> Note: This utility is intended for internal use only. It is not subject to any compatibility guarantees, and may cause interruptions for your users.
-> To avoid potential outages and orphaned resources, we recommend running scale tests on a secondary "staging" environment.
-> Run it against a production environment at your own risk.
+> Note: This utility is intended for internal use only. It is not subject to any
+> compatibility guarantees, and may cause interruptions for your users. To avoid
+> potential outages and orphaned resources, we recommend running scale tests on
+> a secondary "staging" environment. Run it against a production environment at
+> your own risk.
### Workspace Creation
-The following command will run our scale test against your own Coder deployment. You can also specify a template name and any parameter values.
+The following command will run our scale test against your own Coder deployment.
+You can also specify a template name and any parameter values.
-```sh
+```shell
coder exp scaletest create-workspaces \
--count 1000 \
--template "kubernetes" \
@@ -99,16 +152,20 @@ The test does the following:
1. close connections, attempt to delete all workspaces
1. return results (e.g. `998 succeeded, 2 failed to connect`)
-Concurrency is configurable. `concurrency 0` means the scaletest test will attempt to create & connect to all workspaces immediately.
+Concurrency is configurable. `concurrency 0` means the scaletest test will
+attempt to create & connect to all workspaces immediately.
-If you wish to leave the workspaces running for a period of time, you can specify `--no-cleanup` to skip the cleanup step.
-You are responsible for deleting these resources later.
+If you wish to leave the workspaces running for a period of time, you can
+specify `--no-cleanup` to skip the cleanup step. You are responsible for
+deleting these resources later.
### Traffic Generation
-Given an existing set of workspaces created previously with `create-workspaces`, the following command will generate traffic similar to that of Coder's web terminal against those workspaces.
+Given an existing set of workspaces created previously with `create-workspaces`,
+the following command will generate traffic similar to that of Coder's web
+terminal against those workspaces.
-```sh
+```shell
coder exp scaletest workspace-traffic \
--byes-per-tick 128 \
--tick-interval 100ms \
@@ -119,9 +176,10 @@ To generate SSH traffic, add the `--ssh` flag.
### Cleanup
-The scaletest utility will attempt to clean up all workspaces it creates. If you wish to clean up all workspaces, you can run the following command:
+The scaletest utility will attempt to clean up all workspaces it creates. If you
+wish to clean up all workspaces, you can run the following command:
-```sh
+```shell
coder exp scaletest cleanup
```
@@ -129,33 +187,45 @@ This will delete all workspaces and users with the prefix `scaletest-`.
## Autoscaling
-We generally do not recommend using an autoscaler that modifies the number of coderd replicas. In particular, scale
-down events can cause interruptions for a large number of users.
-
-Coderd is different from a simple request-response HTTP service in that it services long-lived connections whenever it
-proxies HTTP applications like IDEs or terminals that rely on websockets, or when it relays tunneled connections to
-workspaces. Loss of a coderd replica will drop these long-lived connections and interrupt users. For example, if you
-have 4 coderd replicas behind a load balancer, and an autoscaler decides to reduce it to 3, roughly 25% of the
-connections will drop. An even larger proportion of users could be affected if they use applications that use more
-than one websocket.
-
-The severity of the interruption varies by application. Coder's web terminal, for example, will reconnect to the same
-session and continue. So, this should not be interpreted as saying coderd replicas should never be taken down for any
+We generally do not recommend using an autoscaler that modifies the number of
+coderd replicas. In particular, scale down events can cause interruptions for a
+large number of users.
+
+Coderd is different from a simple request-response HTTP service in that it
+services long-lived connections whenever it proxies HTTP applications like IDEs
+or terminals that rely on websockets, or when it relays tunneled connections to
+workspaces. Loss of a coderd replica will drop these long-lived connections and
+interrupt users. For example, if you have 4 coderd replicas behind a load
+balancer, and an autoscaler decides to reduce it to 3, roughly 25% of the
+connections will drop. An even larger proportion of users could be affected if
+they use applications that use more than one websocket.
+
+The severity of the interruption varies by application. Coder's web terminal,
+for example, will reconnect to the same session and continue. So, this should
+not be interpreted as saying coderd replicas should never be taken down for any
reason.
-We recommend you plan to run enough coderd replicas to comfortably meet your weekly high-water-mark load, and monitor
-coderd peak CPU & memory utilization over the long term, reevaluating periodically. When scaling down (or performing
-upgrades), schedule these outside normal working hours to minimize user interruptions.
+We recommend you plan to run enough coderd replicas to comfortably meet your
+weekly high-water-mark load, and monitor coderd peak CPU & memory utilization
+over the long term, reevaluating periodically. When scaling down (or performing
+upgrades), schedule these outside normal working hours to minimize user
+interruptions.
### A note for Kubernetes users
-When running on Kubernetes on cloud infrastructure (i.e. not bare metal), many operators choose to employ a _cluster_
-autoscaler that adds and removes Kubernetes _nodes_ according to load. Coder can coexist with such cluster autoscalers,
-but we recommend you take steps to prevent the autoscaler from evicting coderd pods, as an eviction will cause the same
-interruptions as described above. For example, if you are using the [Kubernetes cluster
-autoscaler](https://kubernetes.io/docs/reference/labels-annotations-taints/#cluster-autoscaler-kubernetes-io-safe-to-evict),
-you may wish to set `cluster-autoscaler.kubernetes.io/safe-to-evict: "false"` as an annotation on the coderd deployment.
+When running on Kubernetes on cloud infrastructure (i.e. not bare metal), many
+operators choose to employ a _cluster_ autoscaler that adds and removes
+Kubernetes _nodes_ according to load. Coder can coexist with such cluster
+autoscalers, but we recommend you take steps to prevent the autoscaler from
+evicting coderd pods, as an eviction will cause the same interruptions as
+described above. For example, if you are using the
+[Kubernetes cluster autoscaler](https://kubernetes.io/docs/reference/labels-annotations-taints/#cluster-autoscaler-kubernetes-io-safe-to-evict),
+you may wish to set `cluster-autoscaler.kubernetes.io/safe-to-evict: "false"` as
+an annotation on the coderd deployment.
## Troubleshooting
-If a load test fails or if you are experiencing performance issues during day-to-day use, you can leverage Coder's [prometheus metrics](./prometheus.md) to identify bottlenecks during scale tests. Additionally, you can use your existing cloud monitoring stack to measure load, view server logs, etc.
+If a load test fails or if you are experiencing performance issues during
+day-to-day use, you can leverage Coder's [prometheus metrics](./prometheus.md)
+to identify bottlenecks during scale tests. Additionally, you can use your
+existing cloud monitoring stack to measure load, view server logs, etc.
diff --git a/docs/admin/telemetry.md b/docs/admin/telemetry.md
index f7d586b690bd1..c27e78840be46 100644
--- a/docs/admin/telemetry.md
+++ b/docs/admin/telemetry.md
@@ -1,25 +1,38 @@
# Telemetry
-Coder collects telemetry data from all free installations. Our users have the right to know what we collect, why we collect it, and how we use the data.
+Coder collects telemetry data from all free installations. Our users have the
+right to know what we collect, why we collect it, and how we use the data.
## What we collect
-First of all, we do not collect any information that could threaten the security of your installation. For example, we do not collect parameters, environment variables, or passwords.
+First of all, we do not collect any information that could threaten the security
+of your installation. For example, we do not collect parameters, environment
+variables, or passwords.
-You can find a full list of the data we collect in the source code [here](https://github.com/coder/coder/blob/main/coderd/telemetry/telemetry.go).
+You can find a full list of the data we collect in the source code
+[here](https://github.com/coder/coder/blob/main/coderd/telemetry/telemetry.go).
Telemetry can be configured with the `CODER_TELEMETRY=x` environment variable.
For example, telemetry can be disabled with `CODER_TELEMETRY=false`.
-`CODER_TELEMETRY=true` is our default level. It includes user email and IP addresses. This information is used in aggregate to understand where our users are and general demographic information. We may reach out to the deployment admin, but will never use these emails for outbound marketing.
+`CODER_TELEMETRY=true` is our default level. It includes user email and IP
+addresses. This information is used in aggregate to understand where our users
+are and general demographic information. We may reach out to the deployment
+admin, but will never use these emails for outbound marketing.
`CODER_TELEMETRY=false` disables telemetry altogether.
## How we use telemetry
-We use telemetry to build product better and faster. Without telemetry, we don't know which features are most useful, we don't know where users are dropping off in our funnel, and we don't know if our roadmap is aligned with the demographics that really use Coder.
+We use telemetry to build product better and faster. Without telemetry, we don't
+know which features are most useful, we don't know where users are dropping off
+in our funnel, and we don't know if our roadmap is aligned with the demographics
+that really use Coder.
-Typical SaaS companies collect far more than what we do with little transparency and configurability. It's hard to imagine our favorite products today existing without their backers having good intelligence.
+Typical SaaS companies collect far more than what we do with little transparency
+and configurability. It's hard to imagine our favorite products today existing
+without their backers having good intelligence.
-We've decided the only way we can make our product open-source _and_ build at a fast pace is by collecting usage data as well.
+We've decided the only way we can make our product open-source _and_ build at a
+fast pace is by collecting usage data as well.
diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md
index 72bcdbf27512a..57b885286ee3f 100644
--- a/docs/admin/upgrade.md
+++ b/docs/admin/upgrade.md
@@ -14,18 +14,18 @@ of [install](../install).
## Via install.sh
-If you installed Coder using the `install.sh` script, re-run the below
-command on the host:
+If you installed Coder using the `install.sh` script, re-run the below command
+on the host:
-```console
+```shell
curl -L https://coder.com/install.sh | sh
```
-The script will unpack the new `coder` binary version over the one currently installed.
-Next, you can restart Coder with the following commands (if running it as a system
-service):
+The script will unpack the new `coder` binary version over the one currently
+installed. Next, you can restart Coder with the following commands (if running
+it as a system service):
-```console
+```shell
systemctl daemon-reload
systemctl restart coder
```
@@ -35,19 +35,22 @@ systemctl restart coder
If you installed using `docker-compose`, run the below command to upgrade the
Coder container:
-```console
+```shell
docker-compose pull coder && docker-compose up coder -d
```
## Via Kubernetes
-See [Upgrading Coder via Helm](../install/kubernetes.md#upgrading-coder-via-helm).
+See
+[Upgrading Coder via Helm](../install/kubernetes.md#upgrading-coder-via-helm).
## Via Windows
-Download the latest Windows installer or binary from [GitHub releases](https://github.com/coder/coder/releases/latest), or upgrade from Winget.
+Download the latest Windows installer or binary from
+[GitHub releases](https://github.com/coder/coder/releases/latest), or upgrade
+from Winget.
-```sh
+```pwsh
winget install Coder.Coder
```
diff --git a/docs/admin/users.md b/docs/admin/users.md
index b8edeb1619f91..dc4969edc1d82 100644
--- a/docs/admin/users.md
+++ b/docs/admin/users.md
@@ -1,6 +1,7 @@
# Users
-This article walks you through the user roles available in Coder and creating and managing users.
+This article walks you through the user roles available in Coder and creating
+and managing users.
## Roles
@@ -17,36 +18,54 @@ Coder offers these user roles in the community edition:
| Execute and use **ALL** Workspaces | | | | ✅ |
| View all user operation [Audit Logs](./audit-logs.md) | ✅ | | | ✅ |
-A user may have one or more roles. All users have an implicit Member role
-that may use personal workspaces.
+A user may have one or more roles. All users have an implicit Member role that
+may use personal workspaces.
## Security notes
-A malicious Template Admin could write a template that executes commands on the host (or `coder server` container), which potentially escalates their privileges or shuts down the Coder server. To avoid this, run [external provisioners](./provisioners.md).
+A malicious Template Admin could write a template that executes commands on the
+host (or `coder server` container), which potentially escalates their privileges
+or shuts down the Coder server. To avoid this, run
+[external provisioners](./provisioners.md).
-In low-trust environments, we do not recommend giving users direct access to edit templates. Instead, use [CI/CD pipelines to update templates](../templates/change-management.md) with proper security scans and code reviews in place.
+In low-trust environments, we do not recommend giving users direct access to
+edit templates. Instead, use
+[CI/CD pipelines to update templates](../templates/change-management.md) with
+proper security scans and code reviews in place.
## User status
-Coder user accounts can have different status types: active, dormant, and suspended.
+Coder user accounts can have different status types: active, dormant, and
+suspended.
### Active user
-An _active_ user account in Coder is the default and desired state for all users. When a user's account is marked as _active_, they have complete access to the Coder platform
-and can utilize all of its features and functionalities without any limitations. Active users can access workspaces, templates, and interact with Coder using CLI.
+An _active_ user account in Coder is the default and desired state for all
+users. When a user's account is marked as _active_, they have complete access to
+the Coder platform and can utilize all of its features and functionalities
+without any limitations. Active users can access workspaces, templates, and
+interact with Coder using CLI.
### Dormant user
-A user account is set to _dormant_ status when they have not yet logged in, or have not logged into the Coder platform for the past 90 days. Once the user logs in to the platform, the account status will switch to _active_.
+A user account is set to _dormant_ status when they have not yet logged in, or
+have not logged into the Coder platform for the past 90 days. Once the user logs
+in to the platform, the account status will switch to _active_.
-Dormant accounts do not count towards the total number of licensed seats in a Coder subscription, allowing organizations to optimize their license usage.
+Dormant accounts do not count towards the total number of licensed seats in a
+Coder subscription, allowing organizations to optimize their license usage.
### Suspended user
-When a user's account is marked as _suspended_ in Coder, it means that the account has been temporarily deactivated, and the user is unable to access the platform.
+When a user's account is marked as _suspended_ in Coder, it means that the
+account has been temporarily deactivated, and the user is unable to access the
+platform.
-Only user administrators or owners have the necessary permissions to manage suspended accounts and decide whether to lift the suspension and allow the user back into the Coder environment.
-This level of control ensures that administrators can enforce security measures and handle any compliance-related issues promptly.
+Only user administrators or owners have the necessary permissions to manage
+suspended accounts and decide whether to lift the suspension and allow the user
+back into the Coder environment. This level of control ensures that
+administrators can enforce security measures and handle any compliance-related
+issues promptly.
## Create a user
@@ -64,7 +83,7 @@ The new user will appear in the **Users** list. Use the toggle to change their
To create a user via the Coder CLI, run:
-```console
+```shell
coder users create
```
@@ -98,7 +117,7 @@ To suspend a user via the web UI:
To suspend a user via the CLI, run:
-```console
+```shell
coder users suspend
```
@@ -117,7 +136,7 @@ To activate a user via the web UI:
To activate a user via the CLI, run:
-```console
+```shell
coder users activate
```
@@ -128,25 +147,27 @@ Confirm the user activation by typing **yes** and pressing **enter**.
To reset a user's via the web UI:
1. Go to **Users**.
-2. Find the user whose password you want to reset, click the vertical ellipsis to the right,
- and select **Reset password**.
+2. Find the user whose password you want to reset, click the vertical ellipsis
+ to the right, and select **Reset password**.
3. Coder displays a temporary password that you can send to the user; copy the
password and click **Reset password**.
-Coder will prompt the user to change their temporary password immediately after logging in.
+Coder will prompt the user to change their temporary password immediately after
+logging in.
You can also reset a password via the CLI:
-```console
+```shell
# run `coder reset-password --help` for usage instructions
coder reset-password
```
-> Resetting a user's password, e.g., the initial `owner` role-based user, only works when run on the host running the Coder control plane.
+> Resetting a user's password, e.g., the initial `owner` role-based user, only
+> works when run on the host running the Coder control plane.
### Resetting a password on Kubernetes
-```sh
+```shell
kubectl exec -it deployment/coder /bin/bash -n coder
coder reset-password
@@ -154,12 +175,22 @@ coder reset-password
## User filtering
-In the Coder UI, you can filter your users using pre-defined filters or by utilizing the Coder's filter query. The examples provided below demonstrate how to use the Coder's filter query:
+In the Coder UI, you can filter your users using pre-defined filters or by
+utilizing the Coder's filter query. The examples provided below demonstrate how
+to use the Coder's filter query:
- To find active users, use the filter `status:active`.
- To find admin users, use the filter `role:admin`.
+- To find users have not been active since July 2023:
+ `status:active last_seen_before:"2023-07-01T00:00:00Z"`
The following filters are supported:
-- `status` - Indicates the status of the user. It can be either `active`, `dormant` or `suspended`.
-- `role` - Represents the role of the user. You can refer to the [TemplateRole documentation](https://pkg.go.dev/github.com/coder/coder/codersdk#TemplateRole) for a list of supported user roles.
+- `status` - Indicates the status of the user. It can be either `active`,
+ `dormant` or `suspended`.
+- `role` - Represents the role of the user. You can refer to the
+ [TemplateRole documentation](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#TemplateRole)
+ for a list of supported user roles.
+- `last_seen_before` and `last_seen_after` - The last time a used has used the
+ platform (e.g. logging in, any API requests, connecting to workspaces). Uses
+ the RFC3339Nano format.
diff --git a/docs/admin/workspace-proxies.md b/docs/admin/workspace-proxies.md
index 4cabf0b2bf570..e88c40831e59f 100644
--- a/docs/admin/workspace-proxies.md
+++ b/docs/admin/workspace-proxies.md
@@ -1,28 +1,47 @@
# Workspace Proxies
-> Workspace proxies are in an [experimental state](../contributing/feature-stages.md#experimental-features) and the behavior is subject to change. Use [GitHub issues](https://github.com/coder/coder) to leave feedback. This experiment must be specifically enabled with the `--experiments="moons"` option on both coderd and the workspace proxy. If you have all experiements enabled, you have to add moons as well. `--experiments="*,moons"`
+> Workspace proxies are in an
+> [experimental state](../contributing/feature-stages.md#experimental-features)
+> and the behavior is subject to change. Use
+> [GitHub issues](https://github.com/coder/coder) to leave feedback. This
+> experiment must be specifically enabled with the `--experiments="moons"`
+> option on both coderd and the workspace proxy. If you have all experiements
+> enabled, you have to add moons as well. `--experiments="*,moons"`
Workspace proxies provide low-latency experiences for geo-distributed teams.
-Coder's networking does a best effort to make direct connections to a workspace. In situations where this is not possible, such as connections via the web terminal and [web IDEs](../ides/web-ides.md), workspace proxies are able to reduce the amount of distance the network traffic needs to travel.
+Coder's networking does a best effort to make direct connections to a workspace.
+In situations where this is not possible, such as connections via the web
+terminal and [web IDEs](../ides/web-ides.md), workspace proxies are able to
+reduce the amount of distance the network traffic needs to travel.
-A workspace proxy is a relay connection a developer can choose to use when connecting with their workspace over SSH, a workspace app, port forwarding, etc. Dashboard connections and API calls (e.g. the workspaces list) are not served over workspace proxies.
+A workspace proxy is a relay connection a developer can choose to use when
+connecting with their workspace over SSH, a workspace app, port forwarding, etc.
+Dashboard connections and API calls (e.g. the workspaces list) are not served
+over workspace proxies.

# Deploy a workspace proxy
-Each workspace proxy should be a unique instance. At no point should 2 workspace proxy instances share the same authentication token. They only require port 443 to be open and are expected to have network connectivity to the coderd dashboard. Workspace proxies **do not** make any database connections.
+Each workspace proxy should be a unique instance. At no point should 2 workspace
+proxy instances share the same authentication token. They only require port 443
+to be open and are expected to have network connectivity to the coderd
+dashboard. Workspace proxies **do not** make any database connections.
-Workspace proxies can be used in the browser by navigating to the user `Account -> Workspace Proxy`
+Workspace proxies can be used in the browser by navigating to the user
+`Account -> Workspace Proxy`
## Requirements
-- The [Coder CLI](../cli.md) must be installed and authenticated as a user with the Owner role.
+- The [Coder CLI](../cli.md) must be installed and authenticated as a user with
+ the Owner role.
## Step 1: Create the proxy
-Create the workspace proxy and make sure to save the returned authentication token for said proxy. This is the token the workspace proxy will use to authenticate back to primary coderd.
+Create the workspace proxy and make sure to save the returned authentication
+token for said proxy. This is the token the workspace proxy will use to
+authenticate back to primary coderd.
```bash
$ coder wsproxy create --name=newyork --display-name="USA East" --icon="/emojis/2194.png"
@@ -40,7 +59,9 @@ newyork unregistered
## Step 2: Deploy the proxy
-Deploying the workspace proxy will also register the proxy with coderd and make the workspace proxy usable. If the proxy deployment is successful, `coder wsproxy ls` will show an `ok` status code:
+Deploying the workspace proxy will also register the proxy with coderd and make
+the workspace proxy usable. If the proxy deployment is successful,
+`coder wsproxy ls` will show an `ok` status code:
```
$ coder wsproxy ls
@@ -53,13 +74,18 @@ sydney https://sydney.example.com ok
Other Status codes:
- `unregistered` : The workspace proxy was created, and not yet deployed
-- `unreachable` : The workspace proxy was registered, but is not responding. Likely the proxy went offline.
-- `unhealthy` : The workspace proxy is reachable, but has some issue that is preventing the proxy from being used. `coder wsproxy ls` should show the error message.
+- `unreachable` : The workspace proxy was registered, but is not responding.
+ Likely the proxy went offline.
+- `unhealthy` : The workspace proxy is reachable, but has some issue that is
+ preventing the proxy from being used. `coder wsproxy ls` should show the error
+ message.
- `ok` : The workspace proxy is healthy and working properly!
### Configuration
-Workspace proxy configuration overlaps with a subset of the coderd configuration. To see the full list of configuration options: `coder wsproxy server --help`
+Workspace proxy configuration overlaps with a subset of the coderd
+configuration. To see the full list of configuration options:
+`coder wsproxy server --help`
```bash
# Proxy specific configuration. These are REQUIRED
@@ -87,7 +113,8 @@ CODER_TLS_KEY_FILE=""
Make a `values-wsproxy.yaml` with the workspace proxy configuration:
-> Notice the `workspaceProxy` configuration which is `false` by default in the coder Helm chart.
+> Notice the `workspaceProxy` configuration which is `false` by default in the
+> coder Helm chart.
```yaml
coder:
@@ -121,7 +148,9 @@ Using Helm, install the workspace proxy chart
helm install coder coder-v2/coder --namespace -f ./values-wsproxy.yaml
```
-Test that the workspace proxy is reachable with `curl -vvv`. If for some reason, the Coder dashboard still shows the workspace proxy is `UNHEALTHY`, scale down and up the deployment's replicas.
+Test that the workspace proxy is reachable with `curl -vvv`. If for some reason,
+the Coder dashboard still shows the workspace proxy is `UNHEALTHY`, scale down
+and up the deployment's replicas.
### Running on a VM
@@ -132,11 +161,14 @@ coder wsproxy server
### Running in Docker
-Modify the default entrypoint to run a workspace proxy server instead of a regular Coder server.
+Modify the default entrypoint to run a workspace proxy server instead of a
+regular Coder server.
#### Docker Compose
-Change the provided [`docker-compose.yml`](https://github.com/coder/coder/blob/main/docker-compose.yaml) file to include a custom entrypoint:
+Change the provided
+[`docker-compose.yml`](https://github.com/coder/coder/blob/main/docker-compose.yaml)
+file to include a custom entrypoint:
```diff
image: ghcr.io/coder/coder:${CODER_VERSION:-latest}
@@ -158,6 +190,9 @@ ENTRYPOINT ["/opt/coder", "wsproxy", "server"]
### Selecting a proxy
-Users can select a workspace proxy at the top-right of the browser-based Coder dashboard. Workspace proxy preferences are cached by the web browser. If a proxy goes offline, the session will fall back to the primary proxy. This could take up to 60 seconds.
+Users can select a workspace proxy at the top-right of the browser-based Coder
+dashboard. Workspace proxy preferences are cached by the web browser. If a proxy
+goes offline, the session will fall back to the primary proxy. This could take
+up to 60 seconds.

diff --git a/docs/api/agents.md b/docs/api/agents.md
index 2316fdaafde51..e1eaafa060411 100644
--- a/docs/api/agents.md
+++ b/docs/api/agents.md
@@ -392,7 +392,14 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \
"url": "string"
}
],
+ "derp_force_websockets": true,
"derpmap": {
+ "homeParams": {
+ "regionScore": {
+ "property1": 0,
+ "property2": 0
+ }
+ },
"omitDefaultRegions": true,
"regions": {
"property1": {
@@ -400,6 +407,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -423,6 +431,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -695,7 +704,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -735,7 +744,14 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con
```json
{
+ "derp_force_websockets": true,
"derp_map": {
+ "homeParams": {
+ "regionScore": {
+ "property1": 0,
+ "property2": 0
+ }
+ },
"omitDefaultRegions": true,
"regions": {
"property1": {
@@ -743,6 +759,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -766,6 +783,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
diff --git a/docs/api/audit.md b/docs/api/audit.md
index d5aeb78665d31..5efe1f3410809 100644
--- a/docs/api/audit.md
+++ b/docs/api/audit.md
@@ -63,7 +63,7 @@ curl -X GET http://coder-server:8080/api/v2/audit?q=string \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
diff --git a/docs/api/authentication.md b/docs/api/authentication.md
index e0bdb045ee139..77668fe030781 100644
--- a/docs/api/authentication.md
+++ b/docs/api/authentication.md
@@ -2,7 +2,7 @@
Long-lived tokens can be generated to perform actions on behalf of your user account:
-```console
+```shell
coder tokens create
```
diff --git a/docs/api/authorization.md b/docs/api/authorization.md
index d57a5e7542c35..17fc2e81d2299 100644
--- a/docs/api/authorization.md
+++ b/docs/api/authorization.md
@@ -129,7 +129,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \
```json
{
"password": "string",
- "to_type": "password"
+ "to_type": ""
}
```
@@ -148,7 +148,7 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/convert-login \
{
"expires_at": "2019-08-24T14:15:22Z",
"state_string": "string",
- "to_type": "password",
+ "to_type": "",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
diff --git a/docs/api/builds.md b/docs/api/builds.md
index daf7958de387f..f6aa71be7a555 100644
--- a/docs/api/builds.md
+++ b/docs/api/builds.md
@@ -39,7 +39,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -120,7 +120,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -201,7 +201,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -282,7 +282,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -583,7 +583,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -672,7 +672,7 @@ Status Code **200**
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |
| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | |
-| `»» subsystem` | [codersdk.AgentSubsystem](schemas.md#codersdkagentsubsystem) | false | | |
+| `»» subsystems` | array | false | | |
| `»» troubleshooting_url` | string | false | | |
| `»» updated_at` | string(date-time) | false | | |
| `»» version` | string | false | | |
@@ -716,7 +716,6 @@ Status Code **200**
| `status` | `connected` |
| `status` | `disconnected` |
| `status` | `timeout` |
-| `subsystem` | `envbox` |
| `workspace_transition` | `start` |
| `workspace_transition` | `stop` |
| `workspace_transition` | `delete` |
@@ -760,7 +759,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -841,7 +840,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -927,7 +926,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -1008,7 +1007,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -1133,7 +1132,7 @@ Status Code **200**
| `»»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `»»» startup_script_timeout_seconds` | integer | false | | »»startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |
| `»»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | |
-| `»»» subsystem` | [codersdk.AgentSubsystem](schemas.md#codersdkagentsubsystem) | false | | |
+| `»»» subsystems` | array | false | | |
| `»»» troubleshooting_url` | string | false | | |
| `»»» updated_at` | string(date-time) | false | | |
| `»»» version` | string | false | | |
@@ -1164,7 +1163,6 @@ Status Code **200**
| Property | Value |
| ------------------------- | ----------------------------- |
-| `error_code` | `MISSING_TEMPLATE_PARAMETER` |
| `error_code` | `REQUIRED_TEMPLATE_VARIABLES` |
| `status` | `pending` |
| `status` | `running` |
@@ -1197,7 +1195,6 @@ Status Code **200**
| `status` | `connected` |
| `status` | `disconnected` |
| `status` | `timeout` |
-| `subsystem` | `envbox` |
| `workspace_transition` | `start` |
| `workspace_transition` | `stop` |
| `workspace_transition` | `delete` |
@@ -1275,7 +1272,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -1356,7 +1353,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
diff --git a/docs/api/debug.md b/docs/api/debug.md
index e3382c6586504..5016f6a87b256 100644
--- a/docs/api/debug.md
+++ b/docs/api/debug.md
@@ -102,6 +102,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
"error": "string",
"healthy": true,
"node": {
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -134,6 +135,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -164,6 +166,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
"error": "string",
"healthy": true,
"node": {
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -196,6 +199,7 @@ curl -X GET http://coder-server:8080/api/v2/debug/health \
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md
index d859ac59ad09b..b60b9cf9fc4c6 100644
--- a/docs/api/enterprise.md
+++ b/docs/api/enterprise.md
@@ -134,6 +134,7 @@ curl -X GET http://coder-server:8080/api/v2/entitlements \
}
},
"has_license": true,
+ "refreshed_at": "2019-08-24T14:15:22Z",
"require_telemetry": true,
"trial": true,
"warnings": ["string"]
@@ -148,7 +149,7 @@ curl -X GET http://coder-server:8080/api/v2/entitlements \
To perform this operation, you must be authenticated. [Learn more](authentication.md).
-## Get group by name
+## Get group by ID
### Code samples
@@ -165,7 +166,7 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \
| Name | In | Type | Required | Description |
| ------- | ---- | ------ | -------- | ----------- |
-| `group` | path | string | true | Group name |
+| `group` | path | string | true | Group id |
### Example responses
@@ -183,7 +184,7 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -197,7 +198,8 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
- "quota_allowance": 0
+ "quota_allowance": 0,
+ "source": "user"
}
```
@@ -244,7 +246,7 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -258,7 +260,8 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group} \
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
- "quota_allowance": 0
+ "quota_allowance": 0,
+ "source": "user"
}
```
@@ -277,17 +280,32 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio
```shell
# Example request using curl
curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \
+ -H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`PATCH /groups/{group}`
+> Body parameter
+
+```json
+{
+ "add_users": ["string"],
+ "avatar_url": "string",
+ "display_name": "string",
+ "name": "string",
+ "quota_allowance": 0,
+ "remove_users": ["string"]
+}
+```
+
### Parameters
-| Name | In | Type | Required | Description |
-| ------- | ---- | ------ | -------- | ----------- |
-| `group` | path | string | true | Group name |
+| Name | In | Type | Required | Description |
+| ------- | ---- | ------------------------------------------------------------------ | -------- | ------------------- |
+| `group` | path | string | true | Group name |
+| `body` | body | [codersdk.PatchGroupRequest](schemas.md#codersdkpatchgrouprequest) | true | Patch group request |
### Example responses
@@ -305,7 +323,7 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -319,7 +337,8 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
- "quota_allowance": 0
+ "quota_allowance": 0,
+ "source": "user"
}
```
@@ -441,7 +460,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -455,7 +474,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
- "quota_allowance": 0
+ "quota_allowance": 0,
+ "source": "user"
}
]
```
@@ -470,33 +490,35 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups
Status Code **200**
-| Name | Type | Required | Restrictions | Description |
-| --------------------- | ---------------------------------------------------- | -------- | ------------ | ----------- |
-| `[array item]` | array | false | | |
-| `» avatar_url` | string | false | | |
-| `» display_name` | string | false | | |
-| `» id` | string(uuid) | false | | |
-| `» members` | array | false | | |
-| `»» avatar_url` | string(uri) | false | | |
-| `»» created_at` | string(date-time) | true | | |
-| `»» email` | string(email) | true | | |
-| `»» id` | string(uuid) | true | | |
-| `»» last_seen_at` | string(date-time) | false | | |
-| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
-| `»» organization_ids` | array | false | | |
-| `»» roles` | array | false | | |
-| `»»» display_name` | string | false | | |
-| `»»» name` | string | false | | |
-| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
-| `»» username` | string | true | | |
-| `» name` | string | false | | |
-| `» organization_id` | string(uuid) | false | | |
-| `» quota_allowance` | integer | false | | |
+| Name | Type | Required | Restrictions | Description |
+| --------------------- | ------------------------------------------------------ | -------- | ------------ | ----------- |
+| `[array item]` | array | false | | |
+| `» avatar_url` | string | false | | |
+| `» display_name` | string | false | | |
+| `» id` | string(uuid) | false | | |
+| `» members` | array | false | | |
+| `»» avatar_url` | string(uri) | false | | |
+| `»» created_at` | string(date-time) | true | | |
+| `»» email` | string(email) | true | | |
+| `»» id` | string(uuid) | true | | |
+| `»» last_seen_at` | string(date-time) | false | | |
+| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
+| `»» organization_ids` | array | false | | |
+| `»» roles` | array | false | | |
+| `»»» display_name` | string | false | | |
+| `»»» name` | string | false | | |
+| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
+| `»» username` | string | true | | |
+| `» name` | string | false | | |
+| `» organization_id` | string(uuid) | false | | |
+| `» quota_allowance` | integer | false | | |
+| `» source` | [codersdk.GroupSource](schemas.md#codersdkgroupsource) | false | | |
#### Enumerated Values
| Property | Value |
| ------------ | ----------- |
+| `login_type` | `` |
| `login_type` | `password` |
| `login_type` | `github` |
| `login_type` | `oidc` |
@@ -504,6 +526,8 @@ Status Code **200**
| `login_type` | `none` |
| `status` | `active` |
| `status` | `suspended` |
+| `source` | `user` |
+| `source` | `oidc` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
@@ -555,7 +579,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -569,7 +593,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
- "quota_allowance": 0
+ "quota_allowance": 0,
+ "source": "user"
}
```
@@ -617,7 +642,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups/
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -631,7 +656,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups/
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
- "quota_allowance": 0
+ "quota_allowance": 0,
+ "source": "user"
}
```
@@ -979,7 +1005,7 @@ curl -X PATCH http://coder-server:8080/api/v2/scim/v2/Users/{id} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1031,7 +1057,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"role": "admin",
"roles": [
@@ -1077,6 +1103,7 @@ Status Code **200**
| Property | Value |
| ------------ | ----------- |
+| `login_type` | `` |
| `login_type` | `password` |
| `login_type` | `github` |
| `login_type` | `oidc` |
@@ -1188,7 +1215,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1202,7 +1229,8 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
- "quota_allowance": 0
+ "quota_allowance": 0,
+ "source": "user"
}
],
"users": [
@@ -1212,7 +1240,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1238,35 +1266,37 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \
Status Code **200**
-| Name | Type | Required | Restrictions | Description |
-| ---------------------- | ---------------------------------------------------- | -------- | ------------ | ----------- |
-| `[array item]` | array | false | | |
-| `» groups` | array | false | | |
-| `»» avatar_url` | string | false | | |
-| `»» display_name` | string | false | | |
-| `»» id` | string(uuid) | false | | |
-| `»» members` | array | false | | |
-| `»»» avatar_url` | string(uri) | false | | |
-| `»»» created_at` | string(date-time) | true | | |
-| `»»» email` | string(email) | true | | |
-| `»»» id` | string(uuid) | true | | |
-| `»»» last_seen_at` | string(date-time) | false | | |
-| `»»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
-| `»»» organization_ids` | array | false | | |
-| `»»» roles` | array | false | | |
-| `»»»» display_name` | string | false | | |
-| `»»»» name` | string | false | | |
-| `»»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
-| `»»» username` | string | true | | |
-| `»» name` | string | false | | |
-| `»» organization_id` | string(uuid) | false | | |
-| `»» quota_allowance` | integer | false | | |
-| `» users` | array | false | | |
+| Name | Type | Required | Restrictions | Description |
+| ---------------------- | ------------------------------------------------------ | -------- | ------------ | ----------- |
+| `[array item]` | array | false | | |
+| `» groups` | array | false | | |
+| `»» avatar_url` | string | false | | |
+| `»» display_name` | string | false | | |
+| `»» id` | string(uuid) | false | | |
+| `»» members` | array | false | | |
+| `»»» avatar_url` | string(uri) | false | | |
+| `»»» created_at` | string(date-time) | true | | |
+| `»»» email` | string(email) | true | | |
+| `»»» id` | string(uuid) | true | | |
+| `»»» last_seen_at` | string(date-time) | false | | |
+| `»»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
+| `»»» organization_ids` | array | false | | |
+| `»»» roles` | array | false | | |
+| `»»»» display_name` | string | false | | |
+| `»»»» name` | string | false | | |
+| `»»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
+| `»»» username` | string | true | | |
+| `»» name` | string | false | | |
+| `»» organization_id` | string(uuid) | false | | |
+| `»» quota_allowance` | integer | false | | |
+| `»» source` | [codersdk.GroupSource](schemas.md#codersdkgroupsource) | false | | |
+| `» users` | array | false | | |
#### Enumerated Values
| Property | Value |
| ------------ | ----------- |
+| `login_type` | `` |
| `login_type` | `password` |
| `login_type` | `github` |
| `login_type` | `oidc` |
@@ -1274,6 +1304,8 @@ Status Code **200**
| `login_type` | `none` |
| `status` | `active` |
| `status` | `suspended` |
+| `source` | `user` |
+| `source` | `oidc` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
diff --git a/docs/api/general.md b/docs/api/general.md
index bb64823bd86c9..604a2993723e5 100644
--- a/docs/api/general.md
+++ b/docs/api/general.md
@@ -168,6 +168,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"derp": {
"config": {
"block_direct": true,
+ "force_websockets": true,
"path": "string",
"url": "string"
},
@@ -256,11 +257,15 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"oidc": {
"allow_signups": true,
"auth_url_params": {},
+ "client_cert_file": "string",
"client_id": "string",
+ "client_key_file": "string",
"client_secret": "string",
"email_domain": ["string"],
"email_field": "string",
+ "group_auto_create": true,
"group_mapping": {},
+ "group_regex_filter": {},
"groups_field": "string",
"icon_url": {
"forceQuery": true,
@@ -305,6 +310,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"provisioner": {
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
+ "daemon_psk": "string",
"daemons": 0,
"daemons_echo": true,
"force_cancel_interval": 0
diff --git a/docs/api/index.md b/docs/api/index.md
index 8a93a4439efbc..a13df98156a77 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -4,13 +4,13 @@ Get started with the Coder API:
Generate a token on your Coder deployment by visiting:
-```sh
+```shell
https://coder.example.com/settings/tokens
```
List your workspaces
-```sh
+```shell
# CLI
curl https://coder.example.com/api/v2/workspaces?q=owner:me \
-H "Coder-Session-Token: "
diff --git a/docs/api/insights.md b/docs/api/insights.md
index 11843e63f0316..3c421c64173cd 100644
--- a/docs/api/insights.md
+++ b/docs/api/insights.md
@@ -80,6 +80,7 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates \
"end_time": "2019-08-24T14:15:22Z",
"parameters_usage": [
{
+ "description": "string",
"display_name": "string",
"name": "string",
"options": [
@@ -91,6 +92,7 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates \
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
+ "type": "string",
"values": [
{
"count": 0,
diff --git a/docs/api/organizations.md b/docs/api/organizations.md
index 4b5b15c5dca16..011d3cac5eb2e 100644
--- a/docs/api/organizations.md
+++ b/docs/api/organizations.md
@@ -49,6 +49,44 @@ curl -X POST http://coder-server:8080/api/v2/licenses \
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+## Update license entitlements
+
+### Code samples
+
+```shell
+# Example request using curl
+curl -X POST http://coder-server:8080/api/v2/licenses/refresh-entitlements \
+ -H 'Accept: application/json' \
+ -H 'Coder-Session-Token: API_KEY'
+```
+
+`POST /licenses/refresh-entitlements`
+
+### Example responses
+
+> 201 Response
+
+```json
+{
+ "detail": "string",
+ "message": "string",
+ "validations": [
+ {
+ "detail": "string",
+ "field": "string"
+ }
+ ]
+}
+```
+
+### Responses
+
+| Status | Meaning | Description | Schema |
+| ------ | ------------------------------------------------------------ | ----------- | ------------------------------------------------ |
+| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.Response](schemas.md#codersdkresponse) |
+
+To perform this operation, you must be authenticated. [Learn more](authentication.md).
+
## Create organization
### Code samples
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index 39bc8d16b6e6f..60bb7a6208c3c 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -201,7 +201,14 @@
"url": "string"
}
],
+ "derp_force_websockets": true,
"derpmap": {
+ "homeParams": {
+ "regionScore": {
+ "property1": 0,
+ "property2": 0
+ }
+ },
"omitDefaultRegions": true,
"regions": {
"property1": {
@@ -209,6 +216,7 @@
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -232,6 +240,7 @@
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -283,6 +292,7 @@
| ---------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id` | string | false | | |
| `apps` | array of [codersdk.WorkspaceApp](#codersdkworkspaceapp) | false | | |
+| `derp_force_websockets` | boolean | false | | |
| `derpmap` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | |
| `directory` | string | false | | |
| `disable_direct_connections` | boolean | false | | |
@@ -377,18 +387,18 @@
```json
{
"expanded_directory": "string",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"version": "string"
}
```
### Properties
-| Name | Type | Required | Restrictions | Description |
-| -------------------- | -------------------------------------------------- | -------- | ------------ | ----------- |
-| `expanded_directory` | string | false | | |
-| `subsystem` | [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | |
-| `version` | string | false | | |
+| Name | Type | Required | Restrictions | Description |
+| -------------------- | ----------------------------------------------------------- | -------- | ------------ | ----------- |
+| `expanded_directory` | string | false | | |
+| `subsystems` | array of [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | |
+| `version` | string | false | | |
## agentsdk.Stats
@@ -595,6 +605,16 @@
| `value_source` | [clibase.ValueSource](#clibasevaluesource) | false | | |
| `yaml` | string | false | | Yaml is the YAML key used to configure this option. If unset, YAML configuring is disabled. |
+## clibase.Regexp
+
+```json
+{}
+```
+
+### Properties
+
+_None_
+
## clibase.Struct-array_codersdk_GitAuthConfig
```json
@@ -774,7 +794,7 @@
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -788,7 +808,8 @@
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
- "quota_allowance": 0
+ "quota_allowance": 0,
+ "source": "user"
}
],
"users": [
@@ -798,7 +819,7 @@
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -902,9 +923,11 @@
#### Enumerated Values
-| Value |
-| -------- |
-| `envbox` |
+| Value |
+| ------------ |
+| `envbox` |
+| `envbuilder` |
+| `exectrace` |
## codersdk.AppHostResponse
@@ -1065,7 +1088,7 @@
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1142,7 +1165,7 @@
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1367,7 +1390,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json
{
"password": "string",
- "to_type": "password"
+ "to_type": ""
}
```
@@ -1456,13 +1479,13 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"allow_user_autostop": true,
"allow_user_cancel_workspace_jobs": true,
"default_ttl_ms": 0,
+ "delete_ttl_ms": 0,
"description": "string",
"disable_everyone_group_access": true,
"display_name": "string",
+ "dormant_ttl_ms": 0,
"failure_ttl_ms": 0,
"icon": "string",
- "inactivity_ttl_ms": 0,
- "locked_ttl_ms": 0,
"max_ttl_ms": 0,
"name": "string",
"restart_requirement": {
@@ -1481,13 +1504,13 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `allow_user_autostop` | boolean | false | | Allow user autostop allows users to set a custom workspace TTL to use in place of the template's DefaultTTL field. By default this is true. If false, the DefaultTTL will always be used. This can only be disabled when using an enterprise license. |
| `allow_user_cancel_workspace_jobs` | boolean | false | | Allow users to cancel in-progress workspace jobs. \*bool as the default value is "true". |
| `default_ttl_ms` | integer | false | | Default ttl ms allows optionally specifying the default TTL for all workspaces created from this template. |
+| `delete_ttl_ms` | integer | false | | Delete ttl ms allows optionally specifying the max lifetime before Coder permanently deletes dormant workspaces created from this template. |
| `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. |
| `disable_everyone_group_access` | boolean | false | | Disable everyone group access allows optionally disabling the default behavior of granting the 'everyone' group access to use the template. If this is set to true, the template will not be available to all users, and must be explicitly granted to users or groups in the permissions settings of the template. |
| `display_name` | string | false | | Display name is the displayed name of the template. |
+| `dormant_ttl_ms` | integer | false | | Dormant ttl ms allows optionally specifying the max lifetime before Coder locks inactive workspaces created from this template. |
| `failure_ttl_ms` | integer | false | | Failure ttl ms allows optionally specifying the max lifetime before Coder stops all resources for failed workspaces created from this template. |
| `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. |
-| `inactivity_ttl_ms` | integer | false | | Inactivity ttl ms allows optionally specifying the max lifetime before Coder locks inactive workspaces created from this template. |
-| `locked_ttl_ms` | integer | false | | Locked ttl ms allows optionally specifying the max lifetime before Coder permanently deletes locked workspaces created from this template. |
| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once restart_requirement is matured |
| `name` | string | true | | Name is the name of the template. |
| `restart_requirement` | [codersdk.TemplateRestartRequirement](#codersdktemplaterestartrequirement) | false | | Restart requirement allows optionally specifying the restart requirement for workspaces created from this template. This is an enterprise feature. |
@@ -1644,6 +1667,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"disable_login": true,
"email": "user@example.com",
+ "login_type": "",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string",
"username": "string"
@@ -1652,13 +1676,14 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ----------------- | ------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `disable_login` | boolean | false | | Disable login sets the user's login type to 'none'. This prevents the user from being able to use a password or any other authentication method to login. |
-| `email` | string | true | | |
-| `organization_id` | string | false | | |
-| `password` | string | false | | |
-| `username` | string | true | | |
+| Name | Type | Required | Restrictions | Description |
+| ----------------- | ---------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `disable_login` | boolean | false | | Disable login sets the user's login type to 'none'. This prevents the user from being able to use a password or any other authentication method to login. Deprecated: Set UserLoginType=LoginTypeDisabled instead. |
+| `email` | string | true | | |
+| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | Login type defaults to LoginTypePassword. |
+| `organization_id` | string | false | | |
+| `password` | string | false | | |
+| `username` | string | true | | |
## codersdk.CreateWorkspaceBuildRequest
@@ -1789,6 +1814,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"config": {
"block_direct": true,
+ "force_websockets": true,
"path": "string",
"url": "string"
},
@@ -1827,6 +1853,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json
{
"block_direct": true,
+ "force_websockets": true,
"path": "string",
"url": "string"
}
@@ -1834,11 +1861,12 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties
-| Name | Type | Required | Restrictions | Description |
-| -------------- | ------- | -------- | ------------ | ----------- |
-| `block_direct` | boolean | false | | |
-| `path` | string | false | | |
-| `url` | string | false | | |
+| Name | Type | Required | Restrictions | Description |
+| ------------------ | ------- | -------- | ------------ | ----------- |
+| `block_direct` | boolean | false | | |
+| `force_websockets` | boolean | false | | |
+| `path` | string | false | | |
+| `url` | string | false | | |
## codersdk.DERPRegion
@@ -1962,6 +1990,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"derp": {
"config": {
"block_direct": true,
+ "force_websockets": true,
"path": "string",
"url": "string"
},
@@ -2050,11 +2079,15 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"oidc": {
"allow_signups": true,
"auth_url_params": {},
+ "client_cert_file": "string",
"client_id": "string",
+ "client_key_file": "string",
"client_secret": "string",
"email_domain": ["string"],
"email_field": "string",
+ "group_auto_create": true,
"group_mapping": {},
+ "group_regex_filter": {},
"groups_field": "string",
"icon_url": {
"forceQuery": true,
@@ -2099,6 +2132,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"provisioner": {
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
+ "daemon_psk": "string",
"daemons": 0,
"daemons_echo": true,
"force_cancel_interval": 0
@@ -2319,6 +2353,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"derp": {
"config": {
"block_direct": true,
+ "force_websockets": true,
"path": "string",
"url": "string"
},
@@ -2407,11 +2442,15 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"oidc": {
"allow_signups": true,
"auth_url_params": {},
+ "client_cert_file": "string",
"client_id": "string",
+ "client_key_file": "string",
"client_secret": "string",
"email_domain": ["string"],
"email_field": "string",
+ "group_auto_create": true,
"group_mapping": {},
+ "group_regex_filter": {},
"groups_field": "string",
"icon_url": {
"forceQuery": true,
@@ -2456,6 +2495,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"provisioner": {
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
+ "daemon_psk": "string",
"daemons": 0,
"daemons_echo": true,
"force_cancel_interval": 0
@@ -2641,6 +2681,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
}
},
"has_license": true,
+ "refreshed_at": "2019-08-24T14:15:22Z",
"require_telemetry": true,
"trial": true,
"warnings": ["string"]
@@ -2655,6 +2696,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `features` | object | false | | |
| » `[any property]` | [codersdk.Feature](#codersdkfeature) | false | | |
| `has_license` | boolean | false | | |
+| `refreshed_at` | string | false | | |
| `require_telemetry` | boolean | false | | |
| `trial` | boolean | false | | |
| `warnings` | array of string | false | | |
@@ -2677,6 +2719,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `single_tailnet` |
| `template_restart_requirement` |
| `deployment_health_page` |
+| `workspaces_batch_actions` |
## codersdk.Feature
@@ -2724,7 +2767,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -2942,7 +2985,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -2956,21 +2999,38 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
],
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
- "quota_allowance": 0
+ "quota_allowance": 0,
+ "source": "user"
}
```
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ----------------- | --------------------------------------- | -------- | ------------ | ----------- |
-| `avatar_url` | string | false | | |
-| `display_name` | string | false | | |
-| `id` | string | false | | |
-| `members` | array of [codersdk.User](#codersdkuser) | false | | |
-| `name` | string | false | | |
-| `organization_id` | string | false | | |
-| `quota_allowance` | integer | false | | |
+| Name | Type | Required | Restrictions | Description |
+| ----------------- | -------------------------------------------- | -------- | ------------ | ----------- |
+| `avatar_url` | string | false | | |
+| `display_name` | string | false | | |
+| `id` | string | false | | |
+| `members` | array of [codersdk.User](#codersdkuser) | false | | |
+| `name` | string | false | | |
+| `organization_id` | string | false | | |
+| `quota_allowance` | integer | false | | |
+| `source` | [codersdk.GroupSource](#codersdkgroupsource) | false | | |
+
+## codersdk.GroupSource
+
+```json
+"user"
+```
+
+### Properties
+
+#### Enumerated Values
+
+| Value |
+| ------ |
+| `user` |
+| `oidc` |
## codersdk.Healthcheck
@@ -3037,7 +3097,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
## codersdk.JobErrorCode
```json
-"MISSING_TEMPLATE_PARAMETER"
+"REQUIRED_TEMPLATE_VARIABLES"
```
### Properties
@@ -3046,7 +3106,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Value |
| ----------------------------- |
-| `MISSING_TEMPLATE_PARAMETER` |
| `REQUIRED_TEMPLATE_VARIABLES` |
## codersdk.License
@@ -3143,7 +3202,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
## codersdk.LoginType
```json
-"password"
+""
```
### Properties
@@ -3152,6 +3211,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Value |
| ---------- |
+| `` |
| `password` |
| `github` |
| `oidc` |
@@ -3260,7 +3320,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"expires_at": "2019-08-24T14:15:22Z",
"state_string": "string",
- "to_type": "password",
+ "to_type": "",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
@@ -3298,11 +3358,15 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"allow_signups": true,
"auth_url_params": {},
+ "client_cert_file": "string",
"client_id": "string",
+ "client_key_file": "string",
"client_secret": "string",
"email_domain": ["string"],
"email_field": "string",
+ "group_auto_create": true,
"group_mapping": {},
+ "group_regex_filter": {},
"groups_field": "string",
"icon_url": {
"forceQuery": true,
@@ -3331,26 +3395,30 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ----------------------- | -------------------------- | -------- | ------------ | ----------- |
-| `allow_signups` | boolean | false | | |
-| `auth_url_params` | object | false | | |
-| `client_id` | string | false | | |
-| `client_secret` | string | false | | |
-| `email_domain` | array of string | false | | |
-| `email_field` | string | false | | |
-| `group_mapping` | object | false | | |
-| `groups_field` | string | false | | |
-| `icon_url` | [clibase.URL](#clibaseurl) | false | | |
-| `ignore_email_verified` | boolean | false | | |
-| `ignore_user_info` | boolean | false | | |
-| `issuer_url` | string | false | | |
-| `scopes` | array of string | false | | |
-| `sign_in_text` | string | false | | |
-| `user_role_field` | string | false | | |
-| `user_role_mapping` | object | false | | |
-| `user_roles_default` | array of string | false | | |
-| `username_field` | string | false | | |
+| Name | Type | Required | Restrictions | Description |
+| ----------------------- | -------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------- |
+| `allow_signups` | boolean | false | | |
+| `auth_url_params` | object | false | | |
+| `client_cert_file` | string | false | | |
+| `client_id` | string | false | | |
+| `client_key_file` | string | false | | Client key file & ClientCertFile are used in place of ClientSecret for PKI auth. |
+| `client_secret` | string | false | | |
+| `email_domain` | array of string | false | | |
+| `email_field` | string | false | | |
+| `group_auto_create` | boolean | false | | |
+| `group_mapping` | object | false | | |
+| `group_regex_filter` | [clibase.Regexp](#clibaseregexp) | false | | |
+| `groups_field` | string | false | | |
+| `icon_url` | [clibase.URL](#clibaseurl) | false | | |
+| `ignore_email_verified` | boolean | false | | |
+| `ignore_user_info` | boolean | false | | |
+| `issuer_url` | string | false | | |
+| `scopes` | array of string | false | | |
+| `sign_in_text` | string | false | | |
+| `user_role_field` | string | false | | |
+| `user_role_mapping` | object | false | | |
+| `user_roles_default` | array of string | false | | |
+| `username_field` | string | false | | |
## codersdk.Organization
@@ -3399,6 +3467,30 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `updated_at` | string | false | | |
| `user_id` | string | false | | |
+## codersdk.PatchGroupRequest
+
+```json
+{
+ "add_users": ["string"],
+ "avatar_url": "string",
+ "display_name": "string",
+ "name": "string",
+ "quota_allowance": 0,
+ "remove_users": ["string"]
+}
+```
+
+### Properties
+
+| Name | Type | Required | Restrictions | Description |
+| ----------------- | --------------- | -------- | ------------ | ----------- |
+| `add_users` | array of string | false | | |
+| `avatar_url` | string | false | | |
+| `display_name` | string | false | | |
+| `name` | string | false | | |
+| `quota_allowance` | integer | false | | |
+| `remove_users` | array of string | false | | |
+
## codersdk.PatchTemplateVersionRequest
```json
@@ -3485,6 +3577,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"daemon_poll_interval": 0,
"daemon_poll_jitter": 0,
+ "daemon_psk": "string",
"daemons": 0,
"daemons_echo": true,
"force_cancel_interval": 0
@@ -3497,6 +3590,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| ----------------------- | ------- | -------- | ------------ | ----------- |
| `daemon_poll_interval` | integer | false | | |
| `daemon_poll_jitter` | integer | false | | |
+| `daemon_psk` | string | false | | |
| `daemons` | integer | false | | |
| `daemons_echo` | boolean | false | | |
| `force_cancel_interval` | integer | false | | |
@@ -3540,7 +3634,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -3578,7 +3672,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Property | Value |
| ------------ | ----------------------------- |
-| `error_code` | `MISSING_TEMPLATE_PARAMETER` |
| `error_code` | `REQUIRED_TEMPLATE_VARIABLES` |
| `status` | `pending` |
| `status` | `running` |
@@ -3901,6 +3994,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `group` |
| `license` |
| `convert_login` |
+| `workspace_proxy` |
+| `organization` |
## codersdk.Response
@@ -4144,8 +4239,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"failure_ttl_ms": 0,
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "inactivity_ttl_ms": 0,
- "locked_ttl_ms": 0,
"max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
@@ -4154,37 +4247,39 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"days_of_week": ["monday"],
"weeks": 0
},
+ "time_til_dormant_autodelete_ms": 0,
+ "time_til_dormant_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
```
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ---------------------------------- | -------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `active_user_count` | integer | false | | Active user count is set to -1 when loading. |
-| `active_version_id` | string | false | | |
-| `allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. |
-| `allow_user_autostop` | boolean | false | | |
-| `allow_user_cancel_workspace_jobs` | boolean | false | | |
-| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | |
-| `created_at` | string | false | | |
-| `created_by_id` | string | false | | |
-| `created_by_name` | string | false | | |
-| `default_ttl_ms` | integer | false | | |
-| `description` | string | false | | |
-| `display_name` | string | false | | |
-| `failure_ttl_ms` | integer | false | | Failure ttl ms InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
-| `icon` | string | false | | |
-| `id` | string | false | | |
-| `inactivity_ttl_ms` | integer | false | | |
-| `locked_ttl_ms` | integer | false | | |
-| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once restart_requirement is matured |
-| `name` | string | false | | |
-| `organization_id` | string | false | | |
-| `provisioner` | string | false | | |
-| `restart_requirement` | [codersdk.TemplateRestartRequirement](#codersdktemplaterestartrequirement) | false | | Restart requirement is an enterprise feature. Its value is only used if your license is entitled to use the advanced template scheduling feature. |
-| `updated_at` | string | false | | |
+| Name | Type | Required | Restrictions | Description |
+| ---------------------------------- | -------------------------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `active_user_count` | integer | false | | Active user count is set to -1 when loading. |
+| `active_version_id` | string | false | | |
+| `allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. |
+| `allow_user_autostop` | boolean | false | | |
+| `allow_user_cancel_workspace_jobs` | boolean | false | | |
+| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | |
+| `created_at` | string | false | | |
+| `created_by_id` | string | false | | |
+| `created_by_name` | string | false | | |
+| `default_ttl_ms` | integer | false | | |
+| `description` | string | false | | |
+| `display_name` | string | false | | |
+| `failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
+| `icon` | string | false | | |
+| `id` | string | false | | |
+| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once restart_requirement is matured |
+| `name` | string | false | | |
+| `organization_id` | string | false | | |
+| `provisioner` | string | false | | |
+| `restart_requirement` | [codersdk.TemplateRestartRequirement](#codersdktemplaterestartrequirement) | false | | Restart requirement is an enterprise feature. Its value is only used if your license is entitled to use the advanced template scheduling feature. |
+| `time_til_dormant_autodelete_ms` | integer | false | | |
+| `time_til_dormant_ms` | integer | false | | |
+| `updated_at` | string | false | | |
#### Enumerated Values
@@ -4229,6 +4324,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Value |
| --------- |
| `builtin` |
+| `app` |
## codersdk.TemplateBuildTimeStats
@@ -4317,6 +4413,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"end_time": "2019-08-24T14:15:22Z",
"parameters_usage": [
{
+ "description": "string",
"display_name": "string",
"name": "string",
"options": [
@@ -4328,6 +4425,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
+ "type": "string",
"values": [
{
"count": 0,
@@ -4380,6 +4478,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"end_time": "2019-08-24T14:15:22Z",
"parameters_usage": [
{
+ "description": "string",
"display_name": "string",
"name": "string",
"options": [
@@ -4391,6 +4490,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
+ "type": "string",
"values": [
{
"count": 0,
@@ -4416,6 +4516,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json
{
+ "description": "string",
"display_name": "string",
"name": "string",
"options": [
@@ -4427,6 +4528,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
+ "type": "string",
"values": [
{
"count": 0,
@@ -4440,10 +4542,12 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Name | Type | Required | Restrictions | Description |
| -------------- | ------------------------------------------------------------------------------------------- | -------- | ------------ | ----------- |
+| `description` | string | false | | |
| `display_name` | string | false | | |
| `name` | string | false | | |
| `options` | array of [codersdk.TemplateVersionParameterOption](#codersdktemplateversionparameteroption) | false | | |
| `template_ids` | array of string | false | | |
+| `type` | string | false | | |
| `values` | array of [codersdk.TemplateParameterValue](#codersdktemplateparametervalue) | false | | |
## codersdk.TemplateParameterValue
@@ -4504,7 +4608,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"role": "admin",
"roles": [
@@ -4559,7 +4663,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -4947,19 +5051,19 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| ---------- | ------ | -------- | ------------ | ----------- |
| `schedule` | string | false | | |
-## codersdk.UpdateWorkspaceLock
+## codersdk.UpdateWorkspaceDormancy
```json
{
- "lock": true
+ "dormant": true
}
```
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ------ | ------- | -------- | ------------ | ----------- |
-| `lock` | boolean | false | | |
+| Name | Type | Required | Restrictions | Description |
+| --------- | ------- | -------- | ------------ | ----------- |
+| `dormant` | boolean | false | | |
## codersdk.UpdateWorkspaceRequest
@@ -5012,7 +5116,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -5137,7 +5241,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
```json
{
- "login_type": "password"
+ "login_type": ""
}
```
@@ -5253,6 +5357,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
+ "dormant_at": "2019-08-24T14:15:22Z",
"health": {
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"healthy": false
@@ -5272,7 +5377,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -5353,7 +5458,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -5387,12 +5492,12 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string"
},
- "locked_at": "2019-08-24T14:15:22Z",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true,
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
+ "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
"template_icon": "string",
@@ -5405,28 +5510,29 @@ If the schedule is empty, the user will be updated to use the default schedule.|
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ------------------------------------------- | ---------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `autostart_schedule` | string | false | | |
-| `created_at` | string | false | | |
-| `deleting_at` | string | false | | Deleting at indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. |
-| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. |
-| `id` | string | false | | |
-| `last_used_at` | string | false | | |
-| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | |
-| `locked_at` | string | false | | Locked at being non-nil indicates a workspace that has been locked. A locked workspace is no longer accessible by a user and must be unlocked by an admin. It is subject to deletion if it breaches the duration of the locked_ttl field on its template. |
-| `name` | string | false | | |
-| `organization_id` | string | false | | |
-| `outdated` | boolean | false | | |
-| `owner_id` | string | false | | |
-| `owner_name` | string | false | | |
-| `template_allow_user_cancel_workspace_jobs` | boolean | false | | |
-| `template_display_name` | string | false | | |
-| `template_icon` | string | false | | |
-| `template_id` | string | false | | |
-| `template_name` | string | false | | |
-| `ttl_ms` | integer | false | | |
-| `updated_at` | string | false | | |
+| Name | Type | Required | Restrictions | Description |
+| ------------------------------------------- | ---------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `autostart_schedule` | string | false | | |
+| `created_at` | string | false | | |
+| `deleting_at` | string | false | | Deleting at indicates the time at which the workspace will be permanently deleted. A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) and a value has been specified for time_til_dormant_autodelete on its template. |
+| `dormant_at` | string | false | | Dormant at being non-nil indicates a workspace that is dormant. A dormant workspace is no longer accessible must be activated. It is subject to deletion if it breaches the duration of the time*til* field on its template. |
+| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. |
+| `id` | string | false | | |
+| `last_used_at` | string | false | | |
+| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | |
+| `name` | string | false | | |
+| `organization_id` | string | false | | |
+| `outdated` | boolean | false | | |
+| `owner_id` | string | false | | |
+| `owner_name` | string | false | | |
+| `template_active_version_id` | string | false | | |
+| `template_allow_user_cancel_workspace_jobs` | boolean | false | | |
+| `template_display_name` | string | false | | |
+| `template_icon` | string | false | | |
+| `template_id` | string | false | | |
+| `template_name` | string | false | | |
+| `ttl_ms` | integer | false | | |
+| `updated_at` | string | false | | |
## codersdk.WorkspaceAgent
@@ -5494,7 +5600,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -5536,7 +5642,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `startup_script_timeout_seconds` | integer | false | | Startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |
| `status` | [codersdk.WorkspaceAgentStatus](#codersdkworkspaceagentstatus) | false | | |
-| `subsystem` | [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | |
+| `subsystems` | array of [codersdk.AgentSubsystem](#codersdkagentsubsystem) | false | | |
| `troubleshooting_url` | string | false | | |
| `updated_at` | string | false | | |
| `version` | string | false | | |
@@ -5545,7 +5651,14 @@ If the schedule is empty, the user will be updated to use the default schedule.|
```json
{
+ "derp_force_websockets": true,
"derp_map": {
+ "homeParams": {
+ "regionScore": {
+ "property1": 0,
+ "property2": 0
+ }
+ },
"omitDefaultRegions": true,
"regions": {
"property1": {
@@ -5553,6 +5666,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -5576,6 +5690,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -5604,6 +5719,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| Name | Type | Required | Restrictions | Description |
| ---------------------------- | ---------------------------------- | -------- | ------------ | ----------- |
+| `derp_force_websockets` | boolean | false | | |
| `derp_map` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | |
| `disable_direct_connections` | boolean | false | | |
@@ -5871,7 +5987,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -5952,7 +6068,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -6263,7 +6379,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -6379,6 +6495,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
+ "dormant_at": "2019-08-24T14:15:22Z",
"health": {
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"healthy": false
@@ -6398,7 +6515,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -6475,7 +6592,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -6509,12 +6626,12 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string"
},
- "locked_at": "2019-08-24T14:15:22Z",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true,
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
+ "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
"template_icon": "string",
@@ -6586,6 +6703,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"error": "string",
"healthy": true,
"node": {
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -6644,6 +6762,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"error": "string",
"healthy": true,
"node": {
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -6676,6 +6795,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -6756,6 +6876,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"error": "string",
"healthy": true,
"node": {
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -6788,6 +6909,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -6818,6 +6940,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"error": "string",
"healthy": true,
"node": {
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -6850,6 +6973,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -6992,6 +7116,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"error": "string",
"healthy": true,
"node": {
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -7024,6 +7149,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -7054,6 +7180,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"error": "string",
"healthy": true,
"node": {
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -7086,6 +7213,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -7230,10 +7358,38 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `time` | string | false | | |
| `valid` | boolean | false | | Valid is true if Time is not NULL |
+## tailcfg.DERPHomeParams
+
+```json
+{
+ "regionScore": {
+ "property1": 0,
+ "property2": 0
+ }
+}
+```
+
+### Properties
+
+| Name | Type | Required | Restrictions | Description |
+| ------------- | ------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `regionScore` | object | false | | Regionscore scales latencies of DERP regions by a given scaling factor when determining which region to use as the home ("preferred") DERP. Scores in the range (0, 1) will cause this region to be proportionally more preferred, and scores in the range (1, ∞) will penalize a region. |
+
+If a region is not present in this map, it is treated as having a score of 1.0.
+Scores should not be 0 or negative; such scores will be ignored.
+A nil map means no change from the previous value (if any); an empty non-nil map can be sent to reset all scores back to 1.0.|
+|» `[any property]`|number|false|||
+
## tailcfg.DERPMap
```json
{
+ "homeParams": {
+ "regionScore": {
+ "property1": 0,
+ "property2": 0
+ }
+ },
"omitDefaultRegions": true,
"regions": {
"property1": {
@@ -7241,6 +7397,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -7264,6 +7421,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -7288,10 +7446,13 @@ If the schedule is empty, the user will be updated to use the default schedule.|
### Properties
-| Name | Type | Required | Restrictions | Description |
-| -------------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `omitDefaultRegions` | boolean | false | | Omitdefaultregions specifies to not use Tailscale's DERP servers, and only use those specified in this DERPMap. If there are none set outside of the defaults, this is a noop. |
-| `regions` | object | false | | Regions is the set of geographic regions running DERP node(s). |
+| Name | Type | Required | Restrictions | Description |
+| ---------------------------------------------------------------------------------- | ------------------------------------------------ | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `homeParams` | [tailcfg.DERPHomeParams](#tailcfgderphomeparams) | false | | Homeparams if non-nil, is a change in home parameters. |
+| The rest of the DEPRMap fields, if zero, means unchanged. |
+| `omitDefaultRegions` | boolean | false | | Omitdefaultregions specifies to not use Tailscale's DERP servers, and only use those specified in this DERPMap. If there are none set outside of the defaults, this is a noop. |
+| This field is only meaningful if the Regions map is non-nil (indicating a change). |
+| `regions` | object | false | | Regions is the set of geographic regions running DERP node(s). |
It's keyed by the DERPRegion.RegionID.
The numbers are not necessarily contiguous.|
@@ -7301,6 +7462,7 @@ The numbers are not necessarily contiguous.|
```json
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -7320,6 +7482,7 @@ The numbers are not necessarily contiguous.|
| Name | Type | Required | Restrictions | Description |
| --------------------------------------------------------------------------------------------------------------------- | ------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `canPort80` | boolean | false | | Canport80 specifies whether this DERP node is accessible over HTTP on port 80 specifically. This is used for captive portal checks. |
| `certName` | string | false | | Certname optionally specifies the expected TLS cert common name. If empty, HostName is used. If CertName is non-empty, HostName is only used for the TCP dial (if IPv4/IPv6 are not present) + TLS ClientHello. |
| `derpport` | integer | false | | Derpport optionally provides an alternate TLS port number for the DERP HTTPS server. |
| If zero, 443 is used. |
@@ -7343,6 +7506,7 @@ The numbers are not necessarily contiguous.|
"embeddedRelay": true,
"nodes": [
{
+ "canPort80": true,
"certName": "string",
"derpport": 0,
"forceHTTP": true,
@@ -7460,6 +7624,36 @@ _None_
| `username_or_id` | string | false | | For the following fields, if the AccessMethod is AccessMethodTerminal, then only AgentNameOrID may be set and it must be a UUID. The other fields must be left blank. |
| `workspace_name_or_id` | string | false | | |
+## workspaceapps.StatsReport
+
+```json
+{
+ "access_method": "path",
+ "agent_id": "string",
+ "requests": 0,
+ "session_ended_at": "string",
+ "session_id": "string",
+ "session_started_at": "string",
+ "slug_or_port": "string",
+ "user_id": "string",
+ "workspace_id": "string"
+}
+```
+
+### Properties
+
+| Name | Type | Required | Restrictions | Description |
+| -------------------- | -------------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------- |
+| `access_method` | [workspaceapps.AccessMethod](#workspaceappsaccessmethod) | false | | |
+| `agent_id` | string | false | | |
+| `requests` | integer | false | | |
+| `session_ended_at` | string | false | | Updated periodically while app is in use active and when the last connection is closed. |
+| `session_id` | string | false | | |
+| `session_started_at` | string | false | | |
+| `slug_or_port` | string | false | | |
+| `user_id` | string | false | | |
+| `workspace_id` | string | false | | |
+
## wsproxysdk.AgentIsLegacyResponse
```json
@@ -7540,6 +7734,66 @@ _None_
```json
{
"app_security_key": "string",
+ "derp_force_websockets": true,
+ "derp_map": {
+ "homeParams": {
+ "regionScore": {
+ "property1": 0,
+ "property2": 0
+ }
+ },
+ "omitDefaultRegions": true,
+ "regions": {
+ "property1": {
+ "avoid": true,
+ "embeddedRelay": true,
+ "nodes": [
+ {
+ "canPort80": true,
+ "certName": "string",
+ "derpport": 0,
+ "forceHTTP": true,
+ "hostName": "string",
+ "insecureForTests": true,
+ "ipv4": "string",
+ "ipv6": "string",
+ "name": "string",
+ "regionID": 0,
+ "stunonly": true,
+ "stunport": 0,
+ "stuntestIP": "string"
+ }
+ ],
+ "regionCode": "string",
+ "regionID": 0,
+ "regionName": "string"
+ },
+ "property2": {
+ "avoid": true,
+ "embeddedRelay": true,
+ "nodes": [
+ {
+ "canPort80": true,
+ "certName": "string",
+ "derpport": 0,
+ "forceHTTP": true,
+ "hostName": "string",
+ "insecureForTests": true,
+ "ipv4": "string",
+ "ipv6": "string",
+ "name": "string",
+ "regionID": 0,
+ "stunonly": true,
+ "stunport": 0,
+ "stuntestIP": "string"
+ }
+ ],
+ "regionCode": "string",
+ "regionID": 0,
+ "regionName": "string"
+ }
+ }
+ },
"derp_mesh_key": "string",
"derp_region_id": 0,
"sibling_replicas": [
@@ -7558,9 +7812,37 @@ _None_
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ------------------ | --------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------- |
-| `app_security_key` | string | false | | |
-| `derp_mesh_key` | string | false | | |
-| `derp_region_id` | integer | false | | |
-| `sibling_replicas` | array of [codersdk.Replica](#codersdkreplica) | false | | Sibling replicas is a list of all other replicas of the proxy that have not timed out. |
+| Name | Type | Required | Restrictions | Description |
+| ----------------------- | --------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------- |
+| `app_security_key` | string | false | | |
+| `derp_force_websockets` | boolean | false | | |
+| `derp_map` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | |
+| `derp_mesh_key` | string | false | | |
+| `derp_region_id` | integer | false | | |
+| `sibling_replicas` | array of [codersdk.Replica](#codersdkreplica) | false | | Sibling replicas is a list of all other replicas of the proxy that have not timed out. |
+
+## wsproxysdk.ReportAppStatsRequest
+
+```json
+{
+ "stats": [
+ {
+ "access_method": "path",
+ "agent_id": "string",
+ "requests": 0,
+ "session_ended_at": "string",
+ "session_id": "string",
+ "session_started_at": "string",
+ "slug_or_port": "string",
+ "user_id": "string",
+ "workspace_id": "string"
+ }
+ ]
+}
+```
+
+### Properties
+
+| Name | Type | Required | Restrictions | Description |
+| ------- | --------------------------------------------------------------- | -------- | ------------ | ----------- |
+| `stats` | array of [workspaceapps.StatsReport](#workspaceappsstatsreport) | false | | |
diff --git a/docs/api/templates.md b/docs/api/templates.md
index f8a1612161f81..bb67535e786c2 100644
--- a/docs/api/templates.md
+++ b/docs/api/templates.md
@@ -50,8 +50,6 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
"failure_ttl_ms": 0,
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "inactivity_ttl_ms": 0,
- "locked_ttl_ms": 0,
"max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
@@ -60,6 +58,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
"days_of_week": ["monday"],
"weeks": 0
},
+ "time_til_dormant_autodelete_ms": 0,
+ "time_til_dormant_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
]
@@ -93,11 +93,9 @@ Status Code **200**
| `» default_ttl_ms` | integer | false | | |
| `» description` | string | false | | |
| `» display_name` | string | false | | |
-| `» failure_ttl_ms` | integer | false | | Failure ttl ms InactivityTTLMillis, and LockedTTLMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
+| `» failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. |
| `» icon` | string | false | | |
| `» id` | string(uuid) | false | | |
-| `» inactivity_ttl_ms` | integer | false | | |
-| `» locked_ttl_ms` | integer | false | | |
| `» max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once restart_requirement is matured |
| `» name` | string | false | | |
| `» organization_id` | string(uuid) | false | | |
@@ -106,6 +104,8 @@ Status Code **200**
| `»» days_of_week` | array | false | | »days of week is a list of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required. Weekdays cannot be specified twice. |
| Restarts will only happen on weekdays in this list on weeks which line up with Weeks. |
| `»» weeks` | integer | false | | Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc. |
+| `» time_til_dormant_autodelete_ms` | integer | false | | |
+| `» time_til_dormant_ms` | integer | false | | |
| `» updated_at` | string(date-time) | false | | |
#### Enumerated Values
@@ -138,13 +138,13 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
"allow_user_autostop": true,
"allow_user_cancel_workspace_jobs": true,
"default_ttl_ms": 0,
+ "delete_ttl_ms": 0,
"description": "string",
"disable_everyone_group_access": true,
"display_name": "string",
+ "dormant_ttl_ms": 0,
"failure_ttl_ms": 0,
"icon": "string",
- "inactivity_ttl_ms": 0,
- "locked_ttl_ms": 0,
"max_ttl_ms": 0,
"name": "string",
"restart_requirement": {
@@ -192,8 +192,6 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
"failure_ttl_ms": 0,
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "inactivity_ttl_ms": 0,
- "locked_ttl_ms": 0,
"max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
@@ -202,6 +200,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
"days_of_week": ["monday"],
"weeks": 0
},
+ "time_til_dormant_autodelete_ms": 0,
+ "time_til_dormant_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
```
@@ -324,8 +324,6 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
"failure_ttl_ms": 0,
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "inactivity_ttl_ms": 0,
- "locked_ttl_ms": 0,
"max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
@@ -334,6 +332,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
"days_of_week": ["monday"],
"weeks": 0
},
+ "time_til_dormant_autodelete_ms": 0,
+ "time_til_dormant_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
```
@@ -385,7 +385,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -455,7 +455,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -549,7 +549,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -629,8 +629,6 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
"failure_ttl_ms": 0,
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "inactivity_ttl_ms": 0,
- "locked_ttl_ms": 0,
"max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
@@ -639,6 +637,8 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
"days_of_week": ["monday"],
"weeks": 0
},
+ "time_til_dormant_autodelete_ms": 0,
+ "time_til_dormant_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
```
@@ -744,8 +744,6 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
"failure_ttl_ms": 0,
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "inactivity_ttl_ms": 0,
- "locked_ttl_ms": 0,
"max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
@@ -754,6 +752,8 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
"days_of_week": ["monday"],
"weeks": 0
},
+ "time_til_dormant_autodelete_ms": 0,
+ "time_til_dormant_ms": 0,
"updated_at": "2019-08-24T14:15:22Z"
}
```
@@ -850,7 +850,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions \
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -920,7 +920,6 @@ Status Code **200**
| Property | Value |
| ------------ | ----------------------------- |
-| `error_code` | `MISSING_TEMPLATE_PARAMETER` |
| `error_code` | `REQUIRED_TEMPLATE_VARIABLES` |
| `status` | `pending` |
| `status` | `running` |
@@ -1024,7 +1023,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/versions/{templ
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -1094,7 +1093,6 @@ Status Code **200**
| Property | Value |
| ------------ | ----------------------------- |
-| `error_code` | `MISSING_TEMPLATE_PARAMETER` |
| `error_code` | `REQUIRED_TEMPLATE_VARIABLES` |
| `status` | `pending` |
| `status` | `running` |
@@ -1142,7 +1140,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion} \
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -1221,7 +1219,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templateversions/{templateversion}
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -1347,7 +1345,7 @@ curl -X POST http://coder-server:8080/api/v2/templateversions/{templateversion}/
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -1400,7 +1398,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -1633,7 +1631,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -1722,7 +1720,7 @@ Status Code **200**
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |
| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | |
-| `»» subsystem` | [codersdk.AgentSubsystem](schemas.md#codersdkagentsubsystem) | false | | |
+| `»» subsystems` | array | false | | |
| `»» troubleshooting_url` | string | false | | |
| `»» updated_at` | string(date-time) | false | | |
| `»» version` | string | false | | |
@@ -1766,7 +1764,6 @@ Status Code **200**
| `status` | `connected` |
| `status` | `disconnected` |
| `status` | `timeout` |
-| `subsystem` | `envbox` |
| `workspace_transition` | `start` |
| `workspace_transition` | `stop` |
| `workspace_transition` | `delete` |
@@ -2025,7 +2022,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -2114,7 +2111,7 @@ Status Code **200**
| `»» startup_script_behavior` | [codersdk.WorkspaceAgentStartupScriptBehavior](schemas.md#codersdkworkspaceagentstartupscriptbehavior) | false | | |
| `»» startup_script_timeout_seconds` | integer | false | | »startup script timeout seconds is the number of seconds to wait for the startup script to complete. If the script does not complete within this time, the agent lifecycle will be marked as start_timeout. |
| `»» status` | [codersdk.WorkspaceAgentStatus](schemas.md#codersdkworkspaceagentstatus) | false | | |
-| `»» subsystem` | [codersdk.AgentSubsystem](schemas.md#codersdkagentsubsystem) | false | | |
+| `»» subsystems` | array | false | | |
| `»» troubleshooting_url` | string | false | | |
| `»» updated_at` | string(date-time) | false | | |
| `»» version` | string | false | | |
@@ -2158,7 +2155,6 @@ Status Code **200**
| `status` | `connected` |
| `status` | `disconnected` |
| `status` | `timeout` |
-| `subsystem` | `envbox` |
| `workspace_transition` | `start` |
| `workspace_transition` | `stop` |
| `workspace_transition` | `delete` |
diff --git a/docs/api/users.md b/docs/api/users.md
index 3c583e15787db..fdeed691da48f 100644
--- a/docs/api/users.md
+++ b/docs/api/users.md
@@ -36,7 +36,7 @@ curl -X GET http://coder-server:8080/api/v2/users \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -79,6 +79,7 @@ curl -X POST http://coder-server:8080/api/v2/users \
{
"disable_login": true,
"email": "user@example.com",
+ "login_type": "",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"password": "string",
"username": "string"
@@ -102,7 +103,7 @@ curl -X POST http://coder-server:8080/api/v2/users \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -360,7 +361,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -411,7 +412,7 @@ curl -X DELETE http://coder-server:8080/api/v2/users/{user} \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -821,7 +822,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/login-type \
```json
{
- "login_type": "password"
+ "login_type": ""
}
```
@@ -1005,7 +1006,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/profile \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1056,7 +1057,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/roles \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1117,7 +1118,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/roles \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1168,7 +1169,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/activate \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
@@ -1219,7 +1220,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/status/suspend \
"email": "user@example.com",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"last_seen_at": "2019-08-24T14:15:22Z",
- "login_type": "password",
+ "login_type": "",
"organization_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"roles": [
{
diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md
index 390a82448886c..ac4eda1069fd3 100644
--- a/docs/api/workspaces.md
+++ b/docs/api/workspaces.md
@@ -48,6 +48,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
+ "dormant_at": "2019-08-24T14:15:22Z",
"health": {
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"healthy": false
@@ -67,7 +68,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -148,7 +149,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -182,12 +183,12 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string"
},
- "locked_at": "2019-08-24T14:15:22Z",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true,
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
+ "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
"template_icon": "string",
@@ -236,6 +237,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
+ "dormant_at": "2019-08-24T14:15:22Z",
"health": {
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"healthy": false
@@ -255,7 +257,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -336,7 +338,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -370,12 +372,12 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string"
},
- "locked_at": "2019-08-24T14:15:22Z",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true,
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
+ "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
"template_icon": "string",
@@ -427,6 +429,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
+ "dormant_at": "2019-08-24T14:15:22Z",
"health": {
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"healthy": false
@@ -446,7 +449,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -523,7 +526,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -557,12 +560,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string"
},
- "locked_at": "2019-08-24T14:15:22Z",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true,
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
+ "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
"template_icon": "string",
@@ -612,6 +615,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
+ "dormant_at": "2019-08-24T14:15:22Z",
"health": {
"failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"healthy": false
@@ -631,7 +635,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
"completed_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"error": "string",
- "error_code": "MISSING_TEMPLATE_PARAMETER",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
"file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"queue_position": 0,
@@ -712,7 +716,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
"startup_script_behavior": "blocking",
"startup_script_timeout_seconds": 0,
"status": "connecting",
- "subsystem": "envbox",
+ "subsystems": ["envbox"],
"troubleshooting_url": "string",
"updated_at": "2019-08-24T14:15:22Z",
"version": "string"
@@ -746,12 +750,12 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
"workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
"workspace_owner_name": "string"
},
- "locked_at": "2019-08-24T14:15:22Z",
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"outdated": true,
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
"owner_name": "string",
+ "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
"template_allow_user_cancel_workspace_jobs": true,
"template_display_name": "string",
"template_icon": "string",
@@ -842,34 +846,34 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/autostart \
To perform this operation, you must be authenticated. [Learn more](authentication.md).
-## Extend workspace deadline by ID
+## Update workspace dormancy status by id.
### Code samples
```shell
# Example request using curl
-curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/extend \
+curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
-`PUT /workspaces/{workspace}/extend`
+`PUT /workspaces/{workspace}/dormant`
> Body parameter
```json
{
- "deadline": "2019-08-24T14:15:22Z"
+ "dormant": true
}
```
### Parameters
-| Name | In | Type | Required | Description |
-| ----------- | ---- | ---------------------------------------------------------------------------------- | -------- | ------------------------------ |
-| `workspace` | path | string(uuid) | true | Workspace ID |
-| `body` | body | [codersdk.PutExtendWorkspaceRequest](schemas.md#codersdkputextendworkspacerequest) | true | Extend deadline update request |
+| Name | In | Type | Required | Description |
+| ----------- | ---- | ------------------------------------------------------------------------------ | -------- | ---------------------------------- |
+| `workspace` | path | string(uuid) | true | Workspace ID |
+| `body` | body | [codersdk.UpdateWorkspaceDormancy](schemas.md#codersdkupdateworkspacedormancy) | true | Make a workspace dormant or active |
### Example responses
@@ -877,53 +881,196 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/extend \
```json
{
- "detail": "string",
- "message": "string",
- "validations": [
- {
- "detail": "string",
- "field": "string"
- }
- ]
+ "autostart_schedule": "string",
+ "created_at": "2019-08-24T14:15:22Z",
+ "deleting_at": "2019-08-24T14:15:22Z",
+ "dormant_at": "2019-08-24T14:15:22Z",
+ "health": {
+ "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
+ "healthy": false
+ },
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "last_used_at": "2019-08-24T14:15:22Z",
+ "latest_build": {
+ "build_number": 0,
+ "created_at": "2019-08-24T14:15:22Z",
+ "daily_cost": 0,
+ "deadline": "2019-08-24T14:15:22Z",
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "initiator_id": "06588898-9a84-4b35-ba8f-f9cbd64946f3",
+ "initiator_name": "string",
+ "job": {
+ "canceled_at": "2019-08-24T14:15:22Z",
+ "completed_at": "2019-08-24T14:15:22Z",
+ "created_at": "2019-08-24T14:15:22Z",
+ "error": "string",
+ "error_code": "REQUIRED_TEMPLATE_VARIABLES",
+ "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "queue_position": 0,
+ "queue_size": 0,
+ "started_at": "2019-08-24T14:15:22Z",
+ "status": "pending",
+ "tags": {
+ "property1": "string",
+ "property2": "string"
+ },
+ "worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
+ },
+ "max_deadline": "2019-08-24T14:15:22Z",
+ "reason": "initiator",
+ "resources": [
+ {
+ "agents": [
+ {
+ "apps": [
+ {
+ "command": "string",
+ "display_name": "string",
+ "external": true,
+ "health": "disabled",
+ "healthcheck": {
+ "interval": 0,
+ "threshold": 0,
+ "url": "string"
+ },
+ "icon": "string",
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "sharing_level": "owner",
+ "slug": "string",
+ "subdomain": true,
+ "url": "string"
+ }
+ ],
+ "architecture": "string",
+ "connection_timeout_seconds": 0,
+ "created_at": "2019-08-24T14:15:22Z",
+ "directory": "string",
+ "disconnected_at": "2019-08-24T14:15:22Z",
+ "environment_variables": {
+ "property1": "string",
+ "property2": "string"
+ },
+ "expanded_directory": "string",
+ "first_connected_at": "2019-08-24T14:15:22Z",
+ "health": {
+ "healthy": false,
+ "reason": "agent has lost connection"
+ },
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "instance_id": "string",
+ "last_connected_at": "2019-08-24T14:15:22Z",
+ "latency": {
+ "property1": {
+ "latency_ms": 0,
+ "preferred": true
+ },
+ "property2": {
+ "latency_ms": 0,
+ "preferred": true
+ }
+ },
+ "lifecycle_state": "created",
+ "login_before_ready": true,
+ "logs_length": 0,
+ "logs_overflowed": true,
+ "name": "string",
+ "operating_system": "string",
+ "ready_at": "2019-08-24T14:15:22Z",
+ "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f",
+ "shutdown_script": "string",
+ "shutdown_script_timeout_seconds": 0,
+ "started_at": "2019-08-24T14:15:22Z",
+ "startup_script": "string",
+ "startup_script_behavior": "blocking",
+ "startup_script_timeout_seconds": 0,
+ "status": "connecting",
+ "subsystems": ["envbox"],
+ "troubleshooting_url": "string",
+ "updated_at": "2019-08-24T14:15:22Z",
+ "version": "string"
+ }
+ ],
+ "created_at": "2019-08-24T14:15:22Z",
+ "daily_cost": 0,
+ "hide": true,
+ "icon": "string",
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",
+ "metadata": [
+ {
+ "key": "string",
+ "sensitive": true,
+ "value": "string"
+ }
+ ],
+ "name": "string",
+ "type": "string",
+ "workspace_transition": "start"
+ }
+ ],
+ "status": "pending",
+ "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1",
+ "template_version_name": "string",
+ "transition": "start",
+ "updated_at": "2019-08-24T14:15:22Z",
+ "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9",
+ "workspace_name": "string",
+ "workspace_owner_id": "e7078695-5279-4c86-8774-3ac2367a2fc7",
+ "workspace_owner_name": "string"
+ },
+ "name": "string",
+ "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
+ "outdated": true,
+ "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
+ "owner_name": "string",
+ "template_active_version_id": "b0da9c29-67d8-4c87-888c-bafe356f7f3c",
+ "template_allow_user_cancel_workspace_jobs": true,
+ "template_display_name": "string",
+ "template_icon": "string",
+ "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc",
+ "template_name": "string",
+ "ttl_ms": 0,
+ "updated_at": "2019-08-24T14:15:22Z"
}
```
### Responses
-| Status | Meaning | Description | Schema |
-| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ |
-| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) |
+| Status | Meaning | Description | Schema |
+| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------- |
+| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Workspace](schemas.md#codersdkworkspace) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
-## Update workspace lock by id.
+## Extend workspace deadline by ID
### Code samples
```shell
# Example request using curl
-curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/lock \
+curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/extend \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
-`PUT /workspaces/{workspace}/lock`
+`PUT /workspaces/{workspace}/extend`
> Body parameter
```json
{
- "lock": true
+ "deadline": "2019-08-24T14:15:22Z"
}
```
### Parameters
-| Name | In | Type | Required | Description |
-| ----------- | ---- | ---------------------------------------------------------------------- | -------- | -------------------------- |
-| `workspace` | path | string(uuid) | true | Workspace ID |
-| `body` | body | [codersdk.UpdateWorkspaceLock](schemas.md#codersdkupdateworkspacelock) | true | Lock or unlock a workspace |
+| Name | In | Type | Required | Description |
+| ----------- | ---- | ---------------------------------------------------------------------------------- | -------- | ------------------------------ |
+| `workspace` | path | string(uuid) | true | Workspace ID |
+| `body` | body | [codersdk.PutExtendWorkspaceRequest](schemas.md#codersdkputextendworkspacerequest) | true | Extend deadline update request |
### Example responses
diff --git a/docs/changelogs/README.md b/docs/changelogs/README.md
index e5740216e4f87..7e2493d7a3c3e 100644
--- a/docs/changelogs/README.md
+++ b/docs/changelogs/README.md
@@ -1,17 +1,19 @@
# Changelogs
-These are the changelogs used by [get-changelog.sh](https://github.com/coder/coder/blob/main/scripts/release/changelog.sh) for a release.
+These are the changelogs used by [generate_release_notes.sh]https://github.com/coder/coder/blob/main/scripts/release/generate_release_notes.sh) for a release.
-These changelogs are currently not kept in-sync with GitHub releases. Use [GitHub releases](https://github.com/coder/coder/releases) for the latest information!
+These changelogs are currently not kept in sync with GitHub releases. Use [GitHub releases](https://github.com/coder/coder/releases) for the latest information!
## Writing a changelog
Run this command to generate release notes:
-```sh
+```shell
+export CODER_IGNORE_MISSING_COMMIT_METADATA=1
+export BRANCH=main
./scripts/release/generate_release_notes.sh \
- --old-version=v0.27.0 \
- --new-version=v0.28.0 \
- --ref=$(git rev-parse --short "${ref:-origin/$branch}") \
- > ./docs/changelogs/v0.28.0.md
+ --old-version=v2.1.4 \
+ --new-version=v2.1.5 \
+ --ref=$(git rev-parse --short "${ref:-origin/$BRANCH}") \
+ > ./docs/changelogs/v2.1.5.md
```
diff --git a/docs/changelogs/v0.25.0.md b/docs/changelogs/v0.25.0.md
index 26411eba3b16b..e31fd0dbf959d 100644
--- a/docs/changelogs/v0.25.0.md
+++ b/docs/changelogs/v0.25.0.md
@@ -1,17 +1,27 @@
## Changelog
-> **Warning**: This release has a known issue: #8351. Upgrade directly to v0.26.0 which includes a fix
+> **Warning**: This release has a known issue: #8351. Upgrade directly to
+> v0.26.0 which includes a fix
### Features
-- The `coder stat` fetches workspace utilization metrics, even from within a container. Our example templates have been updated to use this to show CPU, memory, disk via [agent metadata](https://coder.com/docs/v2/latest/templates/agent-metadata) (#8005)
-- Helm: `coder.command` can specify a different command for the Coder pod (#8116)
-- Enterprise deployments can create templates without 'everyone' group access (#7982)
+- The `coder stat` fetches workspace utilization metrics, even from within a
+ container. Our example templates have been updated to use this to show CPU,
+ memory, disk via
+ [agent metadata](https://coder.com/docs/v2/latest/templates/agent-metadata)
+ (#8005)
+- Helm: `coder.command` can specify a different command for the Coder pod
+ (#8116)
+- Enterprise deployments can create templates without 'everyone' group access
+ (#7982)

-- Add login type 'none' to prevent password login. This can come in handy for machine accounts for CI/CD pipelines or other automation (#8009)
+- Add login type 'none' to prevent password login. This can come in handy for
+ machine accounts for CI/CD pipelines or other automation (#8009)
- Healthcheck endpoint has a database section: `/api/v2/debug/health`
- Force DERP connections in CLI with `--disable-direct` flag (#8131)
-- Disable all direct connections for a Coder deployment with [--block-direct-connections](https://coder.com/docs/v2/latest/cli/server#--block-direct-connections) (#7936)
+- Disable all direct connections for a Coder deployment with
+ [--block-direct-connections](https://coder.com/docs/v2/latest/cli/server#--block-direct-connections)
+ (#7936)
- Search for workspaces based on last activity (#2658)
```text
last_seen_before:"2023-01-14T23:59:59Z" last_seen_after:"2023-01-08T00:00:00Z"
@@ -20,8 +30,11 @@
- Enable Terraform debug mode via deployment configuration (#8260)
- Add github device flow for authentication (#8232)
-- Sort Coder parameters with [display_order](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/parameter) property (#8227)
-- Users can convert from username/password accounts to OIDC accounts in Account settings (#8105) (@Emyrk)
+- Sort Coder parameters with
+ [display_order](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/parameter)
+ property (#8227)
+- Users can convert from username/password accounts to OIDC accounts in Account
+ settings (#8105) (@Emyrk)

- Show service banner in SSH/TTY sessions (#8186)
- Helm chart now supports RBAC for deployments (#8233)
@@ -29,7 +42,8 @@
### Bug fixes
- `coder logout` will not invalidate long-lived API tokens (#8275)
-- Helm: use `/healthz` for liveness and readiness probes instead of `/api/v2/buildinfo` (#8035)
+- Helm: use `/healthz` for liveness and readiness probes instead of
+ `/api/v2/buildinfo` (#8035)
- Close output writer before reader on Windows to unblock close (#8299)
- Resize terminal when dismissing warning (#8028)
- Fix footer year (#8036)
@@ -57,9 +71,11 @@
- Add default dir for VS Code Desktop (#8184)
- Agent metadata is now GA (#8111) (@bpmct)
- Note SSH key location in workspaces (#8264)
-- Update examples of IDEs: remove JetBrains Projector and add VS Code Server (#8310)
+- Update examples of IDEs: remove JetBrains Projector and add VS Code Server
+ (#8310)
-Compare: [`v0.24.1...v0.25.0`](https://github.com/coder/coder/compare/v0.24.1...v0.25.0)
+Compare:
+[`v0.24.1...v0.25.0`](https://github.com/coder/coder/compare/v0.24.1...v0.25.0)
## Container image
@@ -67,4 +83,6 @@ Compare: [`v0.24.1...v0.25.0`](https://github.com/coder/coder/compare/v0.24.1...
## Install/upgrade
-Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v0.26.0.md b/docs/changelogs/v0.26.0.md
index cdba94c5d8ffc..b5b24929dfc90 100644
--- a/docs/changelogs/v0.26.0.md
+++ b/docs/changelogs/v0.26.0.md
@@ -2,7 +2,9 @@
### Important changes
-- [Managed variables](https://coder.com/docs/v2/latest/templates/parameters#terraform-template-wide-variables) are enabled by default. The following block within templates is obsolete and can be removed from your templates:
+- [Managed variables](https://coder.com/docs/v2/latest/templates/parameters#terraform-template-wide-variables)
+ are enabled by default. The following block within templates is obsolete and
+ can be removed from your templates:
```diff
provider "coder" {
@@ -10,19 +12,26 @@
}
```
- > The change does not affect your templates because this attribute was previously necessary to activate this additional feature.
+ > The change does not affect your templates because this attribute was
+ > previously necessary to activate this additional feature.
-- Our scale test CLI is [experimental](https://coder.com/docs/v2/latest/contributing/feature-stages#experimental-features) to allow for rapid iteration. You can still interact with it via `coder exp scaletest` (#8339)
+- Our scale test CLI is
+ [experimental](https://coder.com/docs/v2/latest/contributing/feature-stages#experimental-features)
+ to allow for rapid iteration. You can still interact with it via
+ `coder exp scaletest` (#8339)
### Features
-- [coder dotfiles](https://coder.com/docs/v2/latest/cli/dotfiles) can checkout a specific branch
+- [coder dotfiles](https://coder.com/docs/v2/latest/cli/dotfiles) can checkout a
+ specific branch
### Bug fixes
-- Delay "Workspace build is pending" banner to avoid quick re-render when a workspace is created (#8309)
+- Delay "Workspace build is pending" banner to avoid quick re-render when a
+ workspace is created (#8309)
- `coder stat` handles cgroups with no limits
-- Remove concurrency to allow migrations when `coderd` runs on multiple replicas (#8353)
+- Remove concurrency to allow migrations when `coderd` runs on multiple replicas
+ (#8353)
- Pass oauth configs to site (#8390)
- Improve error message for missing action in Audit log (#8335)
- Add missing fields to extract api key config (#8393)
@@ -31,7 +40,8 @@
- Resolve nil pointer dereference on missing oauth config (#8352)
- Update fly.io example to remove deprecated parameters (#8194)
-Compare: [`v0.25.0...0.26.0`](https://github.com/coder/coder/compare/v0.25.0...v0.26.0)
+Compare:
+[`v0.25.0...0.26.0`](https://github.com/coder/coder/compare/v0.25.0...v0.26.0)
## Container image
@@ -39,4 +49,6 @@ Compare: [`v0.25.0...0.26.0`](https://github.com/coder/coder/compare/v0.25.0...v
## Install/upgrade
-Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v0.26.1.md b/docs/changelogs/v0.26.1.md
index b3b7840a51520..87f5938972aa5 100644
--- a/docs/changelogs/v0.26.1.md
+++ b/docs/changelogs/v0.26.1.md
@@ -2,13 +2,16 @@
### Features
-- [Devcontainer templates](https://coder.com/docs/v2/latest/templates/devcontainers) for Coder (#8256)
+- [Devcontainer templates](https://coder.com/docs/v2/latest/templates/devcontainers)
+ for Coder (#8256)
- The dashboard will warn users when a workspace is unhealthy (#8422)
-- Audit logs `resource_target` search query allows you to search by resource name (#8423)
+- Audit logs `resource_target` search query allows you to search by resource
+ name (#8423)
### Refactors
-- [pgCoordinator](https://github.com/coder/coder/pull/8044) is generally available (#8419)
+- [pgCoordinator](https://github.com/coder/coder/pull/8044) is generally
+ available (#8419)
### Bug fixes
@@ -19,7 +22,8 @@
- Document workspace filter query param correctly (#8408)
- Use numeric comparison to check monotonicity (#8436)
-Compare: [`v0.26.0...v0.26.1`](https://github.com/coder/coder/compare/v0.26.0...v0.26.1)
+Compare:
+[`v0.26.0...v0.26.1`](https://github.com/coder/coder/compare/v0.26.0...v0.26.1)
## Container image
@@ -27,4 +31,6 @@ Compare: [`v0.26.0...v0.26.1`](https://github.com/coder/coder/compare/v0.26.0...
## Install/upgrade
-Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v0.27.0.md b/docs/changelogs/v0.27.0.md
index c03d4608e6d32..d212579a6fed0 100644
--- a/docs/changelogs/v0.27.0.md
+++ b/docs/changelogs/v0.27.0.md
@@ -4,26 +4,34 @@
Agent logs can be pushed after a workspace has started (#8528)
-> ⚠️ **Warning:** You will need to [update](https://coder.com/docs/v2/latest/install) your local Coder CLI v0.27 to connect via `coder ssh`.
+> ⚠️ **Warning:** You will need to
+> [update](https://coder.com/docs/v2/latest/install) your local Coder CLI v0.27
+> to connect via `coder ssh`.
### Features
-- [Empeheral parameters](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/parameter#ephemeral) allow users to specify a value for a single build (#8415) (#8524)
+- [Empeheral parameters](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/parameter#ephemeral)
+ allow users to specify a value for a single build (#8415) (#8524)

- > Upgrade to Coder Terraform Provider v0.11.1 to use ephemeral parameters in your templates
+ > Upgrade to Coder Terraform Provider v0.11.1 to use ephemeral parameters in
+ > your templates
- Create template, if it doesn't exist with `templates push --create` (#8454)
-- Workspaces now appear `unhealthy` in the dashboard and CLI if one or more agents do not exist (#8541) (#8548)
+- Workspaces now appear `unhealthy` in the dashboard and CLI if one or more
+ agents do not exist (#8541) (#8548)

- Reverse port-forward with `coder ssh -R` (#8515)
- Helm: custom command arguments in Helm chart (#8567)
- Template version messages (#8435)
- TTL and max TTL validation increased to 30 days (#8258)
-- [Self-hosted docs](https://coder.com/docs/v2/latest/install/offline#offline-docs): Host your own copy of Coder's documentation in your own environment (#8527) (#8601)
+- [Self-hosted docs](https://coder.com/docs/v2/latest/install/offline#offline-docs):
+ Host your own copy of Coder's documentation in your own environment (#8527)
+ (#8601)
- Add custom coder bin path for `config-ssh` (#8425)
- Admins can create workspaces for other users via the CLI (#8481)
- `coder_app` supports localhost apps running https (#8585)
-- Base container image contains [jq](https://github.com/coder/coder/pull/8563) for parsing mounted JSON secrets
+- Base container image contains [jq](https://github.com/coder/coder/pull/8563)
+ for parsing mounted JSON secrets
### Bug fixes
@@ -31,7 +39,8 @@ Agent logs can be pushed after a workspace has started (#8528)
- `coder stat` fixes
- Read from alternate cgroup path (#8591)
- Improve detection of container environment (#8643)
- - Unskip TestStatCPUCmd/JSON and explicitly set --host in test cmd invocation (#8558)
+ - Unskip TestStatCPUCmd/JSON and explicitly set --host in test cmd invocation
+ (#8558)
- Avoid initial license reconfig if feature isn't enabled (#8586)
- Audit log records delete workspace action properly (#8494)
- Audit logs are properly paginated (#8513)
@@ -47,26 +56,34 @@ Agent logs can be pushed after a workspace has started (#8528)
Agent logs can be pushed after a workspace has started (#8528)
-> ⚠️ **Warning:** You will need to [update](https://coder.com/docs/v2/latest/install) your local Coder CLI v0.27 to connect via `coder ssh`.
+> ⚠️ **Warning:** You will need to
+> [update](https://coder.com/docs/v2/latest/install) your local Coder CLI v0.27
+> to connect via `coder ssh`.
### Features
-- [Empeheral parameters](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/parameter#ephemeral) allow users to specify a value for a single build (#8415) (#8524)
+- [Empeheral parameters](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/parameter#ephemeral)
+ allow users to specify a value for a single build (#8415) (#8524)

- > Upgrade to Coder Terraform Provider v0.11.1 to use ephemeral parameters in your templates
+ > Upgrade to Coder Terraform Provider v0.11.1 to use ephemeral parameters in
+ > your templates
- Create template, if it doesn't exist with `templates push --create` (#8454)
-- Workspaces now appear `unhealthy` in the dashboard and CLI if one or more agents do not exist (#8541) (#8548)
+- Workspaces now appear `unhealthy` in the dashboard and CLI if one or more
+ agents do not exist (#8541) (#8548)

- Reverse port-forward with `coder ssh -R` (#8515)
- Helm: custom command arguments in Helm chart (#8567)
- Template version messages (#8435)
- TTL and max TTL validation increased to 30 days (#8258)
-- [Self-hosted docs](https://coder.com/docs/v2/latest/install/offline#offline-docs): Host your own copy of Coder's documentation in your own environment (#8527) (#8601)
+- [Self-hosted docs](https://coder.com/docs/v2/latest/install/offline#offline-docs):
+ Host your own copy of Coder's documentation in your own environment (#8527)
+ (#8601)
- Add custom coder bin path for `config-ssh` (#8425)
- Admins can create workspaces for other users via the CLI (#8481)
- `coder_app` supports localhost apps running https (#8585)
-- Base container image contains [jq](https://github.com/coder/coder/pull/8563) for parsing mounted JSON secrets
+- Base container image contains [jq](https://github.com/coder/coder/pull/8563)
+ for parsing mounted JSON secrets
### Bug fixes
@@ -74,7 +91,8 @@ Agent logs can be pushed after a workspace has started (#8528)
- `coder stat` fixes
- Read from alternate cgroup path (#8591)
- Improve detection of container environment (#8643)
- - Unskip TestStatCPUCmd/JSON and explicitly set --host in test cmd invocation (#8558)
+ - Unskip TestStatCPUCmd/JSON and explicitly set --host in test cmd invocation
+ (#8558)
- Avoid initial license reconfig if feature isn't enabled (#8586)
- Audit log records delete workspace action properly (#8494)
- Audit logs are properly paginated (#8513)
@@ -88,7 +106,8 @@ Agent logs can be pushed after a workspace has started (#8528)
- Docs on using remote Docker hosts (#8479)
- Added kubernetes option to workspace proxies (#8533)
-Compare: [`v0.26.1...v0.26.2`](https://github.com/coder/coder/compare/v0.26.1...v0.27.0)
+Compare:
+[`v0.26.1...v0.26.2`](https://github.com/coder/coder/compare/v0.26.1...v0.27.0)
## Container image
@@ -96,13 +115,16 @@ Compare: [`v0.26.1...v0.26.2`](https://github.com/coder/coder/compare/v0.26.1...
## Install/upgrade
-Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
- Custom API use cases (custom agent logs, CI/CD pipelines) (#8445)
- Docs on using remote Docker hosts (#8479)
- Added kubernetes option to workspace proxies (#8533)
-Compare: [`v0.26.1...v0.26.2`](https://github.com/coder/coder/compare/v0.26.1...v0.27.0)
+Compare:
+[`v0.26.1...v0.26.2`](https://github.com/coder/coder/compare/v0.26.1...v0.27.0)
## Container image
@@ -110,4 +132,6 @@ Compare: [`v0.26.1...v0.26.2`](https://github.com/coder/coder/compare/v0.26.1...
## Install/upgrade
-Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v0.27.1.md b/docs/changelogs/v0.27.1.md
index 93e5d3b3f4498..7a02b12dbaf37 100644
--- a/docs/changelogs/v0.27.1.md
+++ b/docs/changelogs/v0.27.1.md
@@ -12,7 +12,8 @@
- Add steps for postgres SSL cert config (#8648)
-Compare: [`v0.27.0...v0.27.1`](https://github.com/coder/coder/compare/v0.27.0...v0.27.1)
+Compare:
+[`v0.27.0...v0.27.1`](https://github.com/coder/coder/compare/v0.27.0...v0.27.1)
## Container image
@@ -20,4 +21,6 @@ Compare: [`v0.27.0...v0.27.1`](https://github.com/coder/coder/compare/v0.27.0...
## Install/upgrade
-Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v0.27.3.md b/docs/changelogs/v0.27.3.md
index ce87c5a3ceb09..b9bb5a4c1988b 100644
--- a/docs/changelogs/v0.27.3.md
+++ b/docs/changelogs/v0.27.3.md
@@ -6,7 +6,8 @@
- be2e6f443 fix(enterprise): ensure creating a SCIM user is idempotent (#8730)
-Compare: [`v0.27.2...v0.27.3`](https://github.com/coder/coder/compare/v0.27.2...v0.27.3)
+Compare:
+[`v0.27.2...v0.27.3`](https://github.com/coder/coder/compare/v0.27.2...v0.27.3)
## Container image
@@ -14,4 +15,6 @@ Compare: [`v0.27.2...v0.27.3`](https://github.com/coder/coder/compare/v0.27.2...
## Install/upgrade
-Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v2.0.0.md b/docs/changelogs/v2.0.0.md
index 4bcfe513c2caf..fb43de0e9581d 100644
--- a/docs/changelogs/v2.0.0.md
+++ b/docs/changelogs/v2.0.0.md
@@ -1,60 +1,106 @@
-We are thrilled to release Coder v2.0.0. You can safely upgrade from any previous
-[coder/coder](https://github.com/coder/coder) release,
-but we feel like we have outgrown development (v0.x) releases:
+We are thrilled to release Coder v2.0.0. You can safely upgrade from any
+previous [coder/coder](https://github.com/coder/coder) release, but we feel like
+we have outgrown development (v0.x) releases:
- 1600+ users develop on Coder every day
-- A single 4-core Coder server can [happily support](https://coder.com/docs/v2/latest/admin/scale) 1000+ users and workspace connections
-- We have a full suite of [paid features](https://coder.com/docs/v2/latest/enterprise) and enterprise customers deployed in production
-- Users depend on our CLI to [automate Coder](https://coder.com/docs/v2/latest/admin/automation) in Ci/Cd pipelines and templates
-
-Why not v1.0? At the time of writing, our legacy product is currently on v1.34. While Coder v1 is being sunset, we still wanted to avoid versioning conflicts.
+- A single 4-core Coder server can
+ [happily support](https://coder.com/docs/v2/latest/admin/scale) 1000+ users
+ and workspace connections
+- We have a full suite of
+ [paid features](https://coder.com/docs/v2/latest/enterprise) and enterprise
+ customers deployed in production
+- Users depend on our CLI to
+ [automate Coder](https://coder.com/docs/v2/latest/admin/automation) in Ci/Cd
+ pipelines and templates
+
+Why not v1.0? At the time of writing, our legacy product is currently on v1.34.
+While Coder v1 is being sunset, we still wanted to avoid versioning conflicts.
What is not changing:
- Our feature roadmap: See what we have planned at https://coder.com/roadmap
-- Your upgrade path: You can safely upgrade from previous coder/coder releases to v2.x releases!
-- Our release cadence: We want features out as quickly as possible and feature flag any work that isn’t ready for production yet!
+- Your upgrade path: You can safely upgrade from previous coder/coder releases
+ to v2.x releases!
+- Our release cadence: We want features out as quickly as possible and feature
+ flag any work that isn’t ready for production yet!
What is changing:
-- Our deprecation policy: Major features will be deprecated for at least 1 minor release before being removed. Any breaking changes to the REST API and SDK are done via minor releases and will be called out in our changelog.
-- Regular scale testing: Follow along on our [ Google Sheets or Grafana dashboard ]
+- Our deprecation policy: Major features will be deprecated for at least 1 minor
+ release before being removed. Any breaking changes to the REST API and SDK are
+ done via minor releases and will be called out in our changelog.
+- Regular scale testing: Follow along on our [ Google Sheets or Grafana
+ dashboard ]
-Questions? Feel free to ask in [our Discord](https://discord.gg/coder) or email ben@coder.com!
+Questions? Feel free to ask in [our Discord](https://discord.gg/coder) or email
+ben@coder.com!
## Changelog
### BREAKING CHANGES
-- RBAC: The default [Member role](https://coder.com/docs/v2/latest/admin/users) can no longer see a list of all users in a Coder deployment. The Template Admin role and above can still use the `Users` page in dashboard and query users via the API (#8650) (@Emyrk)
-- Kubernetes (Helm): The [default ServiceAccount](https://github.com/coder/coder/blob/8d0e8f45e0fb3802d777a396b4c027ab9788e1b8/helm/values.yaml#L67-L82) for Coder can provision `Deployments` on the cluster. (#8704) (@ericpaulsen)
- - This can be disabled by a [Helm value](https://github.com/coder/coder/blob/8d0e8f45e0fb3802d777a396b4c027ab9788e1b8/helm/values.yaml#L78)
- - Our [Kubernetes example template](https://github.com/coder/coder/tree/main/examples/templates/kubernetes) uses a `kubernetes_deployment` instead of `kubernetes_pod` since it works best with [log streaming](https://coder.com/docs/v2/latest/platforms/kubernetes/deployment-logs) in Coder.
+- RBAC: The default [Member role](https://coder.com/docs/v2/latest/admin/users)
+ can no longer see a list of all users in a Coder deployment. The Template
+ Admin role and above can still use the `Users` page in dashboard and query
+ users via the API (#8650) (@Emyrk)
+- Kubernetes (Helm): The
+ [default ServiceAccount](https://github.com/coder/coder/blob/8d0e8f45e0fb3802d777a396b4c027ab9788e1b8/helm/values.yaml#L67-L82)
+ for Coder can provision `Deployments` on the cluster. (#8704) (@ericpaulsen)
+ - This can be disabled by a
+ [Helm value](https://github.com/coder/coder/blob/8d0e8f45e0fb3802d777a396b4c027ab9788e1b8/helm/values.yaml#L78)
+ - Our
+ [Kubernetes example template](https://github.com/coder/coder/tree/main/examples/templates/kubernetes)
+ uses a `kubernetes_deployment` instead of `kubernetes_pod` since it works
+ best with
+ [log streaming](https://coder.com/docs/v2/latest/platforms/kubernetes/deployment-logs)
+ in Coder.
### Features
-- Template insights: Admins can see daily active users, user latency, and popular IDEs (#8722) (@BrunoQuaresma)
+- Template insights: Admins can see daily active users, user latency, and
+ popular IDEs (#8722) (@BrunoQuaresma)

-- [Kubernetes log streaming](https://coder.com/docs/v2/latest/platforms/kubernetes/deployment-logs): Stream Kubernetes event logs to the Coder agent logs to reveal Kuernetes-level issues such as ResourceQuota limitations, invalid images, etc.
+- [Kubernetes log streaming](https://coder.com/docs/v2/latest/platforms/kubernetes/deployment-logs):
+ Stream Kubernetes event logs to the Coder agent logs to reveal Kuernetes-level
+ issues such as ResourceQuota limitations, invalid images, etc.

-- [OIDC Role Sync](https://coder.com/docs/v2/latest/admin/auth#group-sync-enterprise) (Enterprise): Sync roles from your OIDC provider to Coder roles (e.g. `Template Admin`) (#8595) (@Emyrk)
-- Users can convert their accounts from username/password authentication to SSO by linking their account (#8742) (@Emyrk)
+- [OIDC Role Sync](https://coder.com/docs/v2/latest/admin/auth#group-sync-enterprise)
+ (Enterprise): Sync roles from your OIDC provider to Coder roles (e.g.
+ `Template Admin`) (#8595) (@Emyrk)
+- Users can convert their accounts from username/password authentication to SSO
+ by linking their account (#8742) (@Emyrk)

-- CLI: Added `--var` shorthand for `--variable` in `coder templates ` CLI (#8710) (@ammario)
-- Accounts are marked as dormant after 90 days of inactivity and do not consume a license seat. When the user logs in again, their account status is reinstated. (#8644) (@mtojek)
-- Groups can have a non-unique display name that takes priority in the dashboard (#8740) (@Emyrk)
-- Dotfiles: Coder checks if dotfiles install script is executable (#8588) (@BRAVO68WEB)
-- CLI: Added `--var` shorthand for `--variable` in `coder templates ` CLI (#8710) (@ammario)
-- Sever logs: Added fine-grained [filtering](https://coder.com/docs/v2/latest/cli/server#-l---log-filter) with Regex (#8748) (@ammario)
-- d3991fac2 feat(coderd): add parameter insights to template insights (#8656) (@mafredri)
-- Agent metadata: In cases where Coder does not receive metadata in time, we render the previous "stale" value. Stale values are grey versus the typical green color. (#8745) (@BrunoQuaresma)
-- [Open in Coder](https://coder.com/docs/v2/latest/templates/open-in-coder): Generate a link that automatically creates a workspace on behalf of the user, skipping the "Create Workspace" form (#8651) (@BrunoQuaresma)
- - e85b88ca9 feat(site): add restart button when workspace is unhealthy (#8765) (@BrunoQuaresma)
+- CLI: Added `--var` shorthand for `--variable` in
+ `coder templates ` CLI (#8710) (@ammario)
+- Accounts are marked as dormant after 90 days of inactivity and do not consume
+ a license seat. When the user logs in again, their account status is
+ reinstated. (#8644) (@mtojek)
+- Groups can have a non-unique display name that takes priority in the dashboard
+ (#8740) (@Emyrk)
+- Dotfiles: Coder checks if dotfiles install script is executable (#8588)
+ (@BRAVO68WEB)
+- CLI: Added `--var` shorthand for `--variable` in
+ `coder templates ` CLI (#8710) (@ammario)
+- Sever logs: Added fine-grained
+ [filtering](https://coder.com/docs/v2/latest/cli/server#-l---log-filter) with
+ Regex (#8748) (@ammario)
+- d3991fac2 feat(coderd): add parameter insights to template insights (#8656)
+ (@mafredri)
+- Agent metadata: In cases where Coder does not receive metadata in time, we
+ render the previous "stale" value. Stale values are grey versus the typical
+ green color. (#8745) (@BrunoQuaresma)
+- [Open in Coder](https://coder.com/docs/v2/latest/templates/open-in-coder):
+ Generate a link that automatically creates a workspace on behalf of the user,
+ skipping the "Create Workspace" form (#8651) (@BrunoQuaresma)
+ -
+ e85b88ca9 feat(site): add restart button when workspace is unhealthy (#8765)
+ (@BrunoQuaresma)
### Bug fixes
- Do not wait for devcontainer template volume claim bound (#8539) (@Tirzono)
-- Prevent repetition of template IDs in `template_usage_by_day` (#8693) (@mtojek)
+- Prevent repetition of template IDs in `template_usage_by_day` (#8693)
+ (@mtojek)
- Unify parameter validation errors (#8738) (@mtojek)
- Request trial after password is validated (#8750) (@kylecarbs)
- Fix `coder stat mem` calculation for cgroup v1 workspaces (#8762) (@sreya)
@@ -62,15 +108,18 @@ Questions? Feel free to ask in [our Discord](https://discord.gg/coder) or email
- Fix tailnet netcheck issues (#8802) (@deansheather)
- Avoid infinite loop in agent derp-map (#8848) (@deansheather)
- Avoid agent runLoop exiting due to ws ping (#8852) (@deansheather)
-- Add read call to derp-map endpoint to avoid ws ping timeout (#8859) (@deansheather)
+- Add read call to derp-map endpoint to avoid ws ping timeout (#8859)
+ (@deansheather)
- Show current DERP name correctly in vscode (#8856) (@deansheather)
- Apply log-filter to debug logs only (#8751) (@ammario)
- Correctly print deprecated warnings (#8771) (@ammario)
- De-duplicate logs (#8686) (@ammario)
- Always dial agents with `WorkspaceAgentIP` (#8760) (@coadler)
- Ensure creating a SCIM user is idempotent (#8730) (@coadler)
-- Send build parameters over the confirmation dialog on restart (#8660) (@BrunoQuaresma)
-- Fix error 'Reduce of empty array with no initial value' (#8700) (@BrunoQuaresma)
+- Send build parameters over the confirmation dialog on restart (#8660)
+ (@BrunoQuaresma)
+- Fix error 'Reduce of empty array with no initial value' (#8700)
+ (@BrunoQuaresma)
- Fix latency values (#8749) (@BrunoQuaresma)
- Fix metadata value changing width all the time (#8780) (@BrunoQuaresma)
- Show error when user exists (#8864) (@BrunoQuaresma)
@@ -80,14 +129,17 @@ Questions? Feel free to ask in [our Discord](https://discord.gg/coder) or email
### Documentation
- Explain JFrog integration 🐸 (#8682) (@ammario)
-- Allow multiple Coder deployments to use single GitHub OAuth app (#8786) (@matifali)
+- Allow multiple Coder deployments to use single GitHub OAuth app (#8786)
+ (@matifali)
- Remove Microsoft VS Code Server docs (#8845) (@ericpaulsen)
### Reverts
-- Make [pgCoordinator](https://github.com/coder/coder/pull/8044) experimental again (#8797) (@coadler)
+- Make [pgCoordinator](https://github.com/coder/coder/pull/8044) experimental
+ again (#8797) (@coadler)
-Compare: [`v0.27.0...v2.0.0`](https://github.com/coder/coder/compare/v0.27.0...v2.0.0)
+Compare:
+[`v0.27.0...v2.0.0`](https://github.com/coder/coder/compare/v0.27.0...v2.0.0)
## Container image
@@ -95,4 +147,6 @@ Compare: [`v0.27.0...v2.0.0`](https://github.com/coder/coder/compare/v0.27.0...v
## Install/upgrade
-Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or [upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a release asset below.
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v2.0.2.md b/docs/changelogs/v2.0.2.md
new file mode 100644
index 0000000000000..78134f7ef309e
--- /dev/null
+++ b/docs/changelogs/v2.0.2.md
@@ -0,0 +1,61 @@
+## Changelog
+
+### Features
+
+- [External provisioners](https://coder.com/docs/v2/latest/admin/provisioners)
+ updates
+ - Added
+ [PSK authentication](https://coder.com/docs/v2/latest/admin/provisioners#authentication)
+ method (#8877) (@spikecurtis)
+ - Provisioner daemons can be deployed
+ [via Helm](https://github.com/coder/coder/tree/main/helm/provisioner)
+ (#8939) (@spikecurtis)
+- Added login type (OIDC, GitHub, or built-in, or none) to users page (#8912)
+ (@Emyrk)
+- Groups can be
+ [automatically created](https://coder.com/docs/v2/latest/admin/auth#user-not-being-assigned--group-does-not-exist)
+ from OIDC group sync (#8884) (@Emyrk)
+- Parameter values can be specified via the
+ [command line](https://coder.com/docs/v2/latest/cli/create#--parameter) during
+ workspace creation/updates (#8898) (@mtojek)
+- Added date range picker for the template insights page (#8976)
+ (@BrunoQuaresma)
+- We now publish preview
+ [container images](https://github.com/coder/coder/pkgs/container/coder-preview)
+ on every commit to `main`. Only use these images for testing. They are
+ automatically deleted after 7 days.
+- Coder is
+ [officially listed JetBrains Gateway](https://coder.com/blog/self-hosted-remote-development-in-jetbrains-ides-now-available-to-coder-users).
+
+### Bug fixes
+
+- Don't close other web terminal or `coder_app` sessions during a terminal close
+ (#8917)
+- Properly refresh OIDC tokens (#8950) (@Emyrk)
+- Added backoff to validate fresh git auth tokens (#8956) (@kylecarbs)
+- Make preferred region the first in list (#9014) (@matifali)
+- `coder stat`: clistat: accept positional arg for stat disk cmd (#8911)
+- Prompt for confirmation during `coder delete ` (#8579)
+- Ensure SCIM create user can unsuspend (#8916)
+- Set correct Prometheus port in Helm notes (#8888)
+- Show user avatar on group page (#8997) (@BrunoQuaresma)
+- Make deployment stats bar scrollable on smaller viewports (#8996)
+ (@BrunoQuaresma)
+- Add horizontal scroll to template viewer (#8998) (@BrunoQuaresma)
+- Persist search parameters when user has to authenticate (#9005)
+ (@BrunoQuaresma)
+- Set default color and display error on appearance form (#9004)
+ (@BrunoQuaresma)
+
+Compare:
+[`v2.0.1...v2.0.2`](https://github.com/coder/coder/compare/v2.0.1...v2.0.2)
+
+## Container image
+
+- `docker pull ghcr.io/coder/coder:v2.0.2`
+
+## Install/upgrade
+
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v2.1.0.md b/docs/changelogs/v2.1.0.md
new file mode 100644
index 0000000000000..bf7af3379aefb
--- /dev/null
+++ b/docs/changelogs/v2.1.0.md
@@ -0,0 +1,76 @@
+## Changelog
+
+### Important changes
+
+- We removed `jq` from our base image. In the unlikely case you use `jq` for
+ fetching Coder's database secret or other values, you'll need to build your
+ own Coder image. Click
+ [here](https://gist.github.com/bpmct/05cfb671d1d468ae3be46e93173a02ea) to
+ learn more. (#8979) (@ericpaulsen)
+
+### Features
+
+- You can manually add OIDC or GitHub users (#9000) (@Emyrk)
+ 
+ > Use this with the
+ > [CODER_OIDC_ALLOW_SIGNUPS](https://coder.com/docs/v2/latest/cli/server#--oidc-allow-signups)
+ > flag to manually onboard users before opening the floodgates to every user
+ > in your identity provider!
+- CLI: The
+ [--header-command](https://coder.com/docs/v2/latest/cli#--header-command) flag
+ can leverage external services to provide dynamic headers to authenticate to a
+ Coder deployment behind an application proxy or VPN (#9059) (@code-asher)
+- OIDC: Add support for Azure OIDC PKI auth instead of client secret (#9054)
+ (@Emyrk)
+- Helm chart updates:
+ - Add terminationGracePeriodSeconds to provisioner chart (#9048)
+ (@spikecurtis)
+ - Add support for NodePort service type (#8993) (@ffais)
+ - Published
+ [external provisioner chart](https://coder.com/docs/v2/latest/admin/provisioners#example-running-an-external-provisioner-with-helm)
+ to release and docs (#9050) (@spikecurtis)
+- Exposed everyone group through UI. You can now set
+ [quotas](https://coder.com/docs/v2/latest/admin/quotas) for the `Everyone`
+ group. (#9117) (@sreya)
+- Workspace build errors are shown as a tooltip (#9029) (@BrunoQuaresma)
+- Add build log history to the build log page (#9150) (@BrunoQuaresma)
+ 
+
+### Bug fixes
+
+- Correct GitHub oauth2 callback url (#9052) (@Emyrk)
+- Remove duplication from language of query param error (#9069) (@kylecarbs)
+- Remove unnecessary newlines from the end of cli output (#9068) (@kylecarbs)
+- Change dashboard route `/settings/deployment` to `/deployment` (#9070)
+ (@kylecarbs)
+- Use screen for reconnecting terminal sessions on Linux if available (#8640)
+ (@code-asher)
+- Catch missing output with reconnecting PTY (#9094) (@code-asher)
+- Fix deadlock on tailnet close (#9079) (@spikecurtis)
+- Rename group GET request (#9097) (@ericpaulsen)
+- Change oauth convert oidc cookie to SameSite=Lax (#9129) (@Emyrk)
+- Make PGCoordinator close connections when unhealthy (#9125) (@spikecurtis)
+- Don't navigate away from editor after publishing (#9153) (@aslilac)
+- /workspaces should work even if missing template perms (#9152) (@Emyrk)
+- Redirect to login upon authentication error (#9134) (@aslilac)
+- Avoid showing disabled fields in group settings page (#9154) (@ammario)
+- Disable wireguard trimming (#9098) (@coadler)
+
+### Documentation
+
+- Add
+ [offline docs](https://www.jetbrains.com/help/idea/fully-offline-mode.html)
+ for JetBrains Gateway (#9039) (@ericpaulsen)
+- Add `coder login` to CI docs (#9038) (@ericpaulsen)
+- Expand [JFrog platform](https://coder.com/docs/v2/latest/platforms/jfrog) and
+ example template (#9073) (@matifali)
+
+## Container image
+
+- `docker pull ghcr.io/coder/coder:v2.1.0`
+
+## Install/upgrade
+
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v2.1.1.md b/docs/changelogs/v2.1.1.md
new file mode 100644
index 0000000000000..34b97de7ba2a6
--- /dev/null
+++ b/docs/changelogs/v2.1.1.md
@@ -0,0 +1,49 @@
+## Changelog
+
+### Features
+
+- Add `last_used` search params to workspaces. This can be used to find inactive
+ workspaces (#9230) (@Emyrk)
+ 
+ > You can use `last_used_before` and `last_used_after` in the workspaces
+ > search with [RFC3339Nano](RFC3339Nano) datetimes
+- Add `daily_cost`` to `coder ls` to show
+ [quota](https://coder.com/docs/v2/latest/admin/quotas) consumption (#9200)
+ (@ammario)
+- Added `coder_app` usage to template insights (#9138) (@mafredri)
+ 
+- Added documentation for
+ [workspace process logging](http://localhost:3000/docs/v2/latest/templates/process-logging).
+ This enterprise feature can be used to log all system-level processes in
+ workspaces. (#9002) (@deansheather)
+
+### Bug fixes
+
+- Avoid temporary license banner when Coder is upgraded via Helm + button to
+ refresh license entitlements (#9155) (@Emyrk)
+- Parameters in the page "Create workspace" will show the display name as the
+ primary field (#9158) (@aslilac)
+ 
+- Fix race in PGCoord at startup (#9144) (@spikecurtis)
+- Do not install strace on OSX (#9167) (@mtojek)
+- Use proper link to workspace proxies page (#9183) (@bpmct)
+- Correctly assess quota for stopped resources (#9201) (@ammario)
+- Add workspace_proxy type to auditlog friendly strings (#9194) (@Emyrk)
+- Always show add user button (#9229) (@aslilac)
+- Correctly reject quota-violating builds (#9233) (@ammario)
+- Log correct script timeout for startup script (#9190) (@mafredri)
+- Remove prompt for immutable parameters on start and restart (#9173) (@mtojek)
+- Server logs: apply filter to log message as well as name (#9232) (@ammario)
+
+Compare:
+[`v2.1.0...v2.1.1`](https://github.com/coder/coder/compare/v2.1.0...v2.1.1)
+
+## Container image
+
+- `docker pull ghcr.io/coder/coder:v2.1.1`
+
+## Install/upgrade
+
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v2.1.2.md b/docs/changelogs/v2.1.2.md
new file mode 100644
index 0000000000000..c4676154f1729
--- /dev/null
+++ b/docs/changelogs/v2.1.2.md
@@ -0,0 +1,32 @@
+## Changelog
+
+### Features
+
+- Users page: Add descriptions for each auth method to the selection menu
+ (#9252) (@aslilac)
+
+### Bug fixes
+
+- Pull agent metadata even when rate is high (#9251) (@ammario)
+- Disable setup page once setup has been completed (#9198) (@aslilac)
+- Rewrite onlyDataResources (#9263) (@mtojek)
+- Prompt when parameter options are incompatible (#9247) (@mtojek)
+- Resolve deadlock when fetching everyone group for in-memory db (#9277)
+ (@kylecarbs)
+- Do not ask for immutables on update (#9266) (@mtojek)
+- Parallelize queries to improve template insights performance (#9275)
+ (@mafredri)
+- Fix init race and close flush (#9248) (@mafredri)
+
+Compare:
+[`v2.1.1...v2.1.2`](https://github.com/coder/coder/compare/v2.1.1...v2.1.2)
+
+## Container image
+
+- `docker pull ghcr.io/coder/coder:v2.1.2`
+
+## Install/upgrade
+
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v2.1.3.md b/docs/changelogs/v2.1.3.md
new file mode 100644
index 0000000000000..ecd7c85582d82
--- /dev/null
+++ b/docs/changelogs/v2.1.3.md
@@ -0,0 +1,31 @@
+## Changelog
+
+### Bug fixes
+
+- Prevent oidc refresh being ignored (#9293) (@coryb)
+- Use stable sorting for insights and improve test coverage (#9250) (@mafredri)
+- Rewrite template insights query for speed and fix intervals (#9300)
+ (@mafredri)
+- Optimize template app insights query for speed and decrease intervals (#9302)
+ (@mafredri)
+- Upgrade cdr.dev/slog to fix isTTY race (#9305) (@mafredri)
+- Fix vertical scroll in the bottom bar (#9270) (@BrunoQuaresma)
+
+### Documentation
+
+- Explain
+ [incompatibility in parameter options](https://coder.com/docs/v2/latest/templates/parameters#incompatibility-in-parameter-options-for-workspace-builds)
+ for workspace builds (#9297) (@mtojek)
+
+Compare:
+[`v2.1.2...v2.1.3`](https://github.com/coder/coder/compare/v2.1.2...v2.1.3)
+
+## Container image
+
+- `docker pull ghcr.io/coder/coder:v2.1.3`
+
+## Install/upgrade
+
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/changelogs/v2.1.4.md b/docs/changelogs/v2.1.4.md
new file mode 100644
index 0000000000000..f2abe83d2fc10
--- /dev/null
+++ b/docs/changelogs/v2.1.4.md
@@ -0,0 +1,41 @@
+## Changelog
+
+### Features
+
+- Add `template_active_version_id` to workspaces (#9226) (@kylecarbs)
+- Show entity name in DeleteDialog (#9347) (@ammario)
+- Improve template publishing flow (#9346) (@aslilac)
+
+### Bug fixes
+
+- Fixed 2 bugs contributing to a memory leak in `coderd` (#9364):
+ - Allow `workspaceAgentLogs` follow to return on non-latest-build (#9382)
+ (@mafredri)
+ - Avoid derp-map updates endpoint leak (#9390) (@deansheather)
+- Send updated workspace data after ws connection (#9392) (@BrunoQuaresma)
+- Fix `coder template pull` on Windows (#9327) (@spikecurtis)
+- Truncate websocket close error (#9360) (@kylecarbs)
+- Add `--max-ttl`` to template create (#9319) (@ammario)
+- Remove rate limits from agent metadata (#9308) (@ammario)
+- Use `websocketNetConn` in `workspaceProxyCoordinate` to bind context (#9395)
+ (@mafredri)
+- Fox default ephemeral parameter value on parameters page (#9314)
+ (@BrunoQuaresma)
+- Render variable width unicode characters in terminal (#9259) (@ammario)
+- Use WebGL renderer for terminal (#9320) (@ammario)
+- 80425c32b fix(site): workaround: reload page every 3sec (#9387) (@mtojek)
+- Make right panel scrollable on template editor (#9344) (@BrunoQuaresma)
+- Use more reasonable restart limit for systemd service (#9355) (@bpmct)
+
+Compare:
+[`v2.1.3...v2.1.4`](https://github.com/coder/coder/compare/v2.1.3...v2.1.4)
+
+## Container image
+
+- `docker pull ghcr.io/coder/coder:v2.1.4`
+
+## Install/upgrade
+
+Refer to our docs to [install](https://coder.com/docs/v2/latest/install) or
+[upgrade](https://coder.com/docs/v2/latest/admin/upgrade) Coder, or use a
+release asset below.
diff --git a/docs/cli.md b/docs/cli.md
index 1a0c40a22fca6..c9ffdc7c46421 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -94,7 +94,18 @@ Path to the global `coder` config directory.
| Type | string-array |
| Environment | $CODER_HEADER |
-Additional HTTP headers added to all requests. Provide as key=value. Can be specified multiple times.
+Additional HTTP headers added to all requests. Provide as key=value. Can be
+specified multiple times.
+
+### --header-command
+
+| | |
+| ----------- | ---------------------------------- |
+| Type | string |
+| Environment | $CODER_HEADER_COMMAND |
+
+An external command that outputs additional HTTP headers added to all requests.
+The command must output each header as `key=value` on its own line.
### --no-feature-warning
@@ -121,7 +132,8 @@ Suppress warning when client and server versions do not match.
| Type | string |
| Environment | $CODER_SESSION_TOKEN |
-Specify an authentication token. For security reasons setting CODER_SESSION_TOKEN is preferred.
+Specify an authentication token. For security reasons setting
+CODER_SESSION_TOKEN is preferred.
### --url
diff --git a/docs/cli/create.md b/docs/cli/create.md
index 3f7ff099b16c8..8c36ea44a3dd7 100644
--- a/docs/cli/create.md
+++ b/docs/cli/create.md
@@ -20,6 +20,15 @@ coder create [flags] [name]
## Options
+### --parameter
+
+| | |
+| ----------- | ---------------------------------- |
+| Type | string-array |
+| Environment | $CODER_RICH_PARAMETER |
+
+Rich parameter value in the format "name=value".
+
### --rich-parameter-file
| | |
diff --git a/docs/cli/list.md b/docs/cli/list.md
index 7a1aa0defb052..b840a32acb151 100644
--- a/docs/cli/list.md
+++ b/docs/cli/list.md
@@ -31,7 +31,7 @@ Specifies whether all workspaces will be listed or not.
| Type | string-array |
| Default | workspace,template,status,healthy,last built,outdated,starts at,stops after |
-Columns to display in table output. Available columns: workspace, template, status, healthy, last built, outdated, starts at, stops after.
+Columns to display in table output. Available columns: workspace, template, status, healthy, last built, outdated, starts at, stops after, daily cost.
### -o, --output
diff --git a/docs/cli/provisionerd_start.md b/docs/cli/provisionerd_start.md
index 583e520389150..b129605933db3 100644
--- a/docs/cli/provisionerd_start.md
+++ b/docs/cli/provisionerd_start.md
@@ -42,6 +42,15 @@ How often to poll for provisioner jobs.
How much to jitter the poll interval by.
+### --psk
+
+| | |
+| ----------- | ------------------------------------------ |
+| Type | string |
+| Environment | $CODER_PROVISIONER_DAEMON_PSK |
+
+Pre-shared key to authenticate with Coder server.
+
### -t, --tag
| | |
diff --git a/docs/cli/restart.md b/docs/cli/restart.md
index 72daa5dec405d..d3b6010a92c2e 100644
--- a/docs/cli/restart.md
+++ b/docs/cli/restart.md
@@ -12,6 +12,15 @@ coder restart [flags]
## Options
+### --build-option
+
+| | |
+| ----------- | -------------------------------- |
+| Type | string-array |
+| Environment | $CODER_BUILD_OPTION |
+
+Build option value in the format "name=value".
+
### --build-options
| | |
diff --git a/docs/cli/server.md b/docs/cli/server.md
index 90c60d7392f00..49ba37d7a4236 100644
--- a/docs/cli/server.md
+++ b/docs/cli/server.md
@@ -118,6 +118,16 @@ Path to read a DERP mapping from. See: https://tailscale.com/kb/1118/custom-derp
URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custom-derp-servers/.
+### --derp-force-websockets
+
+| | |
+| ----------- | -------------------------------------------- |
+| Type | bool |
+| Environment | $CODER_DERP_FORCE_WEBSOCKETS |
+| YAML | networking.derp.forceWebSockets |
+
+Force clients and agents to always use WebSocket to connect to DERP relay servers. By default, DERP uses `Upgrade: derp`, which may cause issues with some reverse proxies. Clients may automatically fallback to WebSocket if they detect an issue with `Upgrade: derp`, but this does not work in all situations.
+
### --derp-server-enable
| | |
@@ -129,28 +139,6 @@ URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custo
Whether to enable or disable the embedded DERP relay server.
-### --derp-server-region-code
-
-| | |
-| ----------- | ------------------------------------------- |
-| Type | string |
-| Environment | $CODER_DERP_SERVER_REGION_CODE |
-| YAML | networking.derp.regionCode |
-| Default | coder |
-
-Region code to use for the embedded DERP server.
-
-### --derp-server-region-id
-
-| | |
-| ----------- | ----------------------------------------- |
-| Type | int |
-| Environment | $CODER_DERP_SERVER_REGION_ID |
-| YAML | networking.derp.regionID |
-| Default | 999 |
-
-Region ID to use for the embedded DERP server.
-
### --derp-server-region-name
| | |
@@ -174,14 +162,14 @@ An HTTP URL that is accessible by other replicas to relay DERP traffic. Required
### --derp-server-stun-addresses
-| | |
-| ----------- | ---------------------------------------------- |
-| Type | string-array |
-| Environment | $CODER_DERP_SERVER_STUN_ADDRESSES |
-| YAML | networking.derp.stunAddresses |
-| Default | stun.l.google.com:19302 |
+| | |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
+| Type | string-array |
+| Environment | $CODER_DERP_SERVER_STUN_ADDRESSES |
+| YAML | networking.derp.stunAddresses |
+| Default | stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302 |
-Addresses for STUN servers to establish P2P connections. Use special value 'disable' to turn off STUN.
+Addresses for STUN servers to establish P2P connections. It's recommended to have at least two STUN servers to give users the best chance of connecting P2P to workspaces. Each STUN server will get it's own DERP region, with region IDs starting at `--derp-server-region-id + 1`. Use special value 'disable' to turn off STUN completely.
### --default-quiet-hours-schedule
@@ -243,6 +231,17 @@ Disable automatic session expiry bumping due to activity. This forces all sessio
Specifies the custom docs URL.
+### --oidc-group-auto-create
+
+| | |
+| ----------- | ------------------------------------------ |
+| Type | bool |
+| Environment | $CODER_OIDC_GROUP_AUTO_CREATE |
+| YAML | oidc.enableGroupAutoCreate |
+| Default | false |
+
+Automatically creates missing groups from a user's groups claim.
+
### --enable-terraform-debug-mode
| | |
@@ -429,6 +428,16 @@ Whether new users can sign up with OIDC.
OIDC auth URL parameters to pass to the upstream provider.
+### --oidc-client-cert-file
+
+| | |
+| ----------- | ----------------------------------------- |
+| Type | string |
+| Environment | $CODER_OIDC_CLIENT_CERT_FILE |
+| YAML | oidc.oidcClientCertFile |
+
+Pem encoded certificate file to use for oauth2 PKI/JWT authorization. The public certificate that accompanies oidc-client-key-file. A standard x509 certificate is expected.
+
### --oidc-client-id
| | |
@@ -439,6 +448,16 @@ OIDC auth URL parameters to pass to the upstream provider.
Client ID to use for Login with OIDC.
+### --oidc-client-key-file
+
+| | |
+| ----------- | ---------------------------------------- |
+| Type | string |
+| Environment | $CODER_OIDC_CLIENT_KEY_FILE |
+| YAML | oidc.oidcClientKeyFile |
+
+Pem encoded RSA private key to use for oauth2 PKI/JWT authorization. This can be used instead of oidc-client-secret if your IDP supports it.
+
### --oidc-client-secret
| | |
@@ -521,6 +540,17 @@ Ignore the userinfo endpoint and only use the ID token for user information.
Issuer URL to use for Login with OIDC.
+### --oidc-group-regex-filter
+
+| | |
+| ----------- | ------------------------------------------- |
+| Type | regexp |
+| Environment | $CODER_OIDC_GROUP_REGEX_FILTER |
+| YAML | oidc.groupRegexFilter |
+| Default | .\* |
+
+If provided any group name not matching the regex is ignored. This allows for filtering out groups that are not needed. This filter is applied after the group mapping.
+
### --oidc-scopes
| | |
@@ -668,6 +698,16 @@ Collect database metrics (may increase charges for metrics storage).
Serve prometheus metrics on the address defined by prometheus address.
+### --provisioner-daemon-psk
+
+| | |
+| ----------- | ------------------------------------------ |
+| Type | string |
+| Environment | $CODER_PROVISIONER_DAEMON_PSK |
+| YAML | provisioning.daemonPSK |
+
+Pre-shared key to authenticate external provisioner daemons to Coder server.
+
### --provisioner-daemons
| | |
diff --git a/docs/cli/start.md b/docs/cli/start.md
index 8c3fd7f71276e..120edfde679eb 100644
--- a/docs/cli/start.md
+++ b/docs/cli/start.md
@@ -12,6 +12,15 @@ coder start [flags]
## Options
+### --build-option
+
+| | |
+| ----------- | -------------------------------- |
+| Type | string-array |
+| Environment | $CODER_BUILD_OPTION |
+
+Build option value in the format "name=value".
+
### --build-options
| | |
diff --git a/docs/cli/templates.md b/docs/cli/templates.md
index ed459f2d70d82..4426625363ed7 100644
--- a/docs/cli/templates.md
+++ b/docs/cli/templates.md
@@ -40,7 +40,6 @@ Templates are written in standard Terraform and describe the infrastructure for
| [edit](./templates_edit.md) | Edit the metadata of a template by name. |
| [init](./templates_init.md) | Get started with a templated template. |
| [list](./templates_list.md) | List all the templates available for the organization |
-| [plan](./templates_plan.md) | Plan a template push from the current directory |
| [pull](./templates_pull.md) | Download the latest version of a template to a path. |
| [push](./templates_push.md) | Push a new template version from the current directory or as specified by flag |
| [versions](./templates_versions.md) | Manage different versions of the specified template |
diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md
index 44081e8986120..2811e4a1ce021 100644
--- a/docs/cli/templates_create.md
+++ b/docs/cli/templates_create.md
@@ -19,7 +19,7 @@ coder templates create [flags] [name]
| Type | duration |
| Default | 24h |
-Specify a default TTL for workspaces created from this template.
+Specify a default TTL for workspaces created from this template. It is the default time before shutdown - workspaces created from this template default to this value. Maps to "Default autostop" in the UI.
### -d, --directory
@@ -37,7 +37,7 @@ Specify the directory to create from, use '-' to read tar from stdin.
| Type | duration |
| Default | 0h |
-Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).
+Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed "start" build before coder automatically schedules a "stop" build to cleanup.This licensed feature's default is 0h (off). Maps to "Failure cleanup"in the UI.
### --ignore-lockfile
@@ -55,7 +55,15 @@ Ignore warnings about not having a .terraform.lock.hcl file present in the templ
| Type | duration |
| Default | 0h |
-Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).
+Specify an inactivity TTL for workspaces created from this template. It is the amount of time the workspace is not used before it is be stopped and auto-locked. This includes across multiple builds (e.g. auto-starts and stops). This licensed feature's default is 0h (off). Maps to "Dormancy threshold" in the UI.
+
+### --max-ttl
+
+| | |
+| ---- | --------------------- |
+| Type | duration |
+
+Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.
### -m, --message
diff --git a/docs/cli/templates_edit.md b/docs/cli/templates_edit.md
index 2d25da15b7cc1..79f4ec0ba29f6 100644
--- a/docs/cli/templates_edit.md
+++ b/docs/cli/templates_edit.md
@@ -45,7 +45,7 @@ Allow users to cancel in-progress workspace jobs.
| ---- | --------------------- |
| Type | duration |
-Edit the template default time before shutdown - workspaces created from this template default to this value.
+Edit the template default time before shutdown - workspaces created from this template default to this value. Maps to "Default autostop" in the UI.
### --description
@@ -70,7 +70,7 @@ Edit the template display name.
| Type | duration |
| Default | 0h |
-Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).
+Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed "start" build before coder automatically schedules a "stop" build to cleanup.This licensed feature's default is 0h (off). Maps to "Failure cleanup" in the UI.
### --icon
@@ -87,7 +87,7 @@ Edit the template icon path.
| Type | duration |
| Default | 0h |
-Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).
+Specify an inactivity TTL for workspaces created from this template. It is the amount of time the workspace is not used before it is be stopped and auto-locked. This includes across multiple builds (e.g. auto-starts and stops). This licensed feature's default is 0h (off). Maps to "Dormancy threshold" in the UI.
### --max-ttl
@@ -95,7 +95,7 @@ Specify an inactivity TTL for workspaces created from this template. This licens
| ---- | --------------------- |
| Type | duration |
-Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.
+Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting, regardless of user activity. This is an enterprise-only feature. Maps to "Max lifetime" in the UI.
### --name
diff --git a/docs/cli/templates_plan.md b/docs/cli/templates_plan.md
deleted file mode 100644
index 06ed7d56c507d..0000000000000
--- a/docs/cli/templates_plan.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-# templates plan
-
-Plan a template push from the current directory
-
-## Usage
-
-```console
-coder templates plan
-```
diff --git a/docs/cli/update.md b/docs/cli/update.md
index 0b3d31a9755b8..b81172df6b9ca 100644
--- a/docs/cli/update.md
+++ b/docs/cli/update.md
@@ -26,6 +26,15 @@ Use --always-prompt to change the parameter values of the workspace.
Always prompt all parameters. Does not pull parameter values from existing workspace.
+### --build-option
+
+| | |
+| ----------- | -------------------------------- |
+| Type | string-array |
+| Environment | $CODER_BUILD_OPTION |
+
+Build option value in the format "name=value".
+
### --build-options
| | |
@@ -34,6 +43,15 @@ Always prompt all parameters. Does not pull parameter values from existing works
Prompt for one-time build options defined with ephemeral parameters.
+### --parameter
+
+| | |
+| ----------- | ---------------------------------- |
+| Type | string-array |
+| Environment | $CODER_RICH_PARAMETER |
+
+Rich parameter value in the format "name=value".
+
### --rich-parameter-file
| | |
diff --git a/docs/cli/users_create.md b/docs/cli/users_create.md
index 2eb78318ffa0a..b89ff2aeb6d45 100644
--- a/docs/cli/users_create.md
+++ b/docs/cli/users_create.md
@@ -10,21 +10,21 @@ coder users create [flags]
## Options
-### --disable-login
+### -e, --email
-| | |
-| ---- | ----------------- |
-| Type | bool |
+| | |
+| ---- | ------------------- |
+| Type | string |
-Disabling login for a user prevents the user from authenticating via password or IdP login. Authentication requires an API key/token generated by an admin. Be careful when using this flag as it can lock the user out of their account.
+Specifies an email address for the new user.
-### -e, --email
+### --login-type
| | |
| ---- | ------------------- |
| Type | string |
-Specifies an email address for the new user.
+Optionally specify the login type for the user. Valid values are: password, none, github, oidc. Using 'none' prevents the user from authenticating and requires an API key/token to be generated by an admin.
### -p, --password
diff --git a/docs/contributing/CODE_OF_CONDUCT.md b/docs/contributing/CODE_OF_CONDUCT.md
index 81db506b6ff38..5e40eb816bc17 100644
--- a/docs/contributing/CODE_OF_CONDUCT.md
+++ b/docs/contributing/CODE_OF_CONDUCT.md
@@ -5,9 +5,9 @@
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, sex characteristics, gender identity and expression,
-level of experience, education, socio-economic status, nationality, personal
-appearance, race, religion, or sexual identity and orientation.
+size, disability, ethnicity, sex characteristics, gender identity and
+expression, level of experience, education, socio-economic status, nationality,
+personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
@@ -37,11 +37,11 @@ Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
## Scope
@@ -55,11 +55,11 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team at opensource@coder.com. All
-complaints will be reviewed and investigated and will result in a response that
-is deemed necessary and appropriate to the circumstances. The project team is
-obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
+reported by contacting the project team at opensource@coder.com. All complaints
+will be reviewed and investigated and will result in a response that is deemed
+necessary and appropriate to the circumstances. The project team is obligated to
+maintain confidentiality with regard to the reporter of an incident. Further
+details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
@@ -67,8 +67,9 @@ members of the project's leadership.
## Attribution
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 1.4, available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
diff --git a/docs/contributing/SECURITY.md b/docs/contributing/SECURITY.md
index 649e87d772343..35dc53efd6934 100644
--- a/docs/contributing/SECURITY.md
+++ b/docs/contributing/SECURITY.md
@@ -1,4 +1,4 @@
# Security Policy
-If you find a vulnerability, **DO NOT FILE AN ISSUE**.
-Instead, send an email to security@coder.com.
+If you find a vulnerability, **DO NOT FILE AN ISSUE**. Instead, send an email to
+security@coder.com.
diff --git a/docs/contributing/documentation.md b/docs/contributing/documentation.md
index 94eba6a3b8068..0f4ba55877b9a 100644
--- a/docs/contributing/documentation.md
+++ b/docs/contributing/documentation.md
@@ -10,12 +10,12 @@ This style guide is primarily for use with authoring documentation.
- Use plural nouns and pronouns (_they_, _their_, or _them_), especially when
the specific number is uncertain (i.e., "Set up your environments" even though
you don't know if the user will have one or many environments)
-- When writing documentation titles, use the noun form, not the gerund form (e.g., "Environment
- Management" instead of "Managing Environments")
+- When writing documentation titles, use the noun form, not the gerund form
+ (e.g., "Environment Management" instead of "Managing Environments")
- Context matters when you decide whether to capitalize something or not. For
- example, ["A Job creates one or more
- Pods..."](https://kubernetes.io/docs/concepts/workloads/controllers/job/) is
- correct when writing about Kubernetes. However, in other contexts, neither
+ example,
+ ["A Job creates one or more Pods..."](https://kubernetes.io/docs/concepts/workloads/controllers/job/)
+ is correct when writing about Kubernetes. However, in other contexts, neither
_job_ nor _pods_ would be capitalized. Please follow the conventions set forth
by the relevant companies and open source communities.
@@ -79,8 +79,8 @@ For code that you want users to enter via a command-line interface, use
### Punctuation
-Do not use the ampersand (&) as a shorthand for _and_ unless you're referring to a
-UI element or the name of something that uses _&_.
+Do not use the ampersand (&) as a shorthand for _and_ unless you're referring to
+a UI element or the name of something that uses _&_.
You can use the symbol `~` in place of the word _approximately_.
@@ -91,13 +91,14 @@ and anything that has a name visible to the user, use bold font.
**Example:** On the **Environment Overview** page, click **Configure SSH**.
-Don't use code font for UI elements unless it is rendered based on previously entered
-text. For example, if you tell the user to provide the environment name as
-`myEnvironment`, then use both bold and cold font when referring to the name.
+Don't use code font for UI elements unless it is rendered based on previously
+entered text. For example, if you tell the user to provide the environment name
+as `myEnvironment`, then use both bold and cold font when referring to the name.
**Example**: Click **`myEnvironment`**.
-When writing out instructions that involve UI elements, both of the following options are acceptable:
+When writing out instructions that involve UI elements, both of the following
+options are acceptable:
- Go to **Manage** > **Users**.
- In the **Manage** menu, click **Users**.
@@ -111,13 +112,13 @@ Below summarizes the guidelines regarding how Coder terms should be used.
The only Coder-specific terms that should be capitalized are the names of
products (e.g., Coder).
-The exception is **code-server**, which is always lowercase. If it appears at the
-beginning of the sentence, rewrite the sentence to avoid this usage.
+The exception is **code-server**, which is always lowercase. If it appears at
+the beginning of the sentence, rewrite the sentence to avoid this usage.
### Uncapitalized terms
-In general, we do not capitalize the names of features (unless the situation calls for it,
-such as the word appearing at the beginning of a sentence):
+In general, we do not capitalize the names of features (unless the situation
+calls for it, such as the word appearing at the beginning of a sentence):
- account dormancy
- audit logs
diff --git a/docs/contributing/feature-stages.md b/docs/contributing/feature-stages.md
index 6ccb4e6edccb1..25b37bbc01863 100644
--- a/docs/contributing/feature-stages.md
+++ b/docs/contributing/feature-stages.md
@@ -4,13 +4,18 @@ Some Coder features are released as Alpha or Experimental.
## Alpha features
-Alpha features are enabled in all Coder deployments but the feature is subject to change, or even be removed. Breaking changes may not be documented in the changelog. In most cases, features will only stay in alpha for 1 month.
+Alpha features are enabled in all Coder deployments but the feature is subject
+to change, or even be removed. Breaking changes may not be documented in the
+changelog. In most cases, features will only stay in alpha for 1 month.
-We recommend using [GitHub issues](https://github.com/coder/coder/issues) to leave feedback and get support for alpha features.
+We recommend using [GitHub issues](https://github.com/coder/coder/issues) to
+leave feedback and get support for alpha features.
## Experimental features
-These features are disabled by default, and not recommended for use in production as they may cause performance or stability issues. In most cases, features will only stay in experimental for 1-2 weeks of internal testing.
+These features are disabled by default, and not recommended for use in
+production as they may cause performance or stability issues. In most cases,
+features will only stay in experimental for 1-2 weeks of internal testing.
```yaml
# Enable all experimental features
@@ -22,4 +27,5 @@ coder server --experiments=feature1,feature2
# Alternatively, use the `CODER_EXPERIMENTS` environment variable.
```
-For a list of all experiments, refer to the [codersdk reference](https://pkg.go.dev/github.com/coder/coder/codersdk#Experiment).
+For a list of all experiments, refer to the
+[codersdk reference](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#Experiment).
diff --git a/docs/contributing/frontend.md b/docs/contributing/frontend.md
index 03ebd41fa28a0..73dc27a1fd662 100644
--- a/docs/contributing/frontend.md
+++ b/docs/contributing/frontend.md
@@ -1,35 +1,51 @@
# Frontend
-This is a guide to help the Coder community and also Coder members contribute to our UI. It is ongoing work but we hope it provides some useful information to get started. If you have any questions or need help, please send us a message on our [Discord server](https://discord.com/invite/coder). We'll be happy to help you.
+This is a guide to help the Coder community and also Coder members contribute to
+our UI. It is ongoing work but we hope it provides some useful information to
+get started. If you have any questions or need help, please send us a message on
+our [Discord server](https://discord.com/invite/coder). We'll be happy to help
+you.
## Running the UI
You can run the UI and access the dashboard in two ways:
-- Build the UI pointing to an external Coder server: `CODER_HOST=https://mycoder.com yarn dev` inside of the `site` folder. This is helpful when you are building something in the UI and already have the data on your deployed server.
-- Build the entire Coder server + UI locally: `./scripts/develop.sh` in the root folder. It is useful when you have to contribute with features that are not deployed yet or when you have to work on both, frontend and backend.
+- Build the UI pointing to an external Coder server:
+ `CODER_HOST=https://mycoder.com pnpm dev` inside of the `site` folder. This is
+ helpful when you are building something in the UI and already have the data on
+ your deployed server.
+- Build the entire Coder server + UI locally: `./scripts/develop.sh` in the root
+ folder. It is useful when you have to contribute with features that are not
+ deployed yet or when you have to work on both, frontend and backend.
-In both cases, you can access the dashboard on `http://localhost:8080`. If you are running the `./scripts/develop.sh` you can log in using the default credentials: `admin@coder.com` and `SomeSecurePassword!`.
+In both cases, you can access the dashboard on `http://localhost:8080`. If you
+are running the `./scripts/develop.sh` you can log in using the default
+credentials: `admin@coder.com` and `SomeSecurePassword!`.
## Tech Stack
-All our dependencies are described in `site/package.json` but here are the most important ones:
+All our dependencies are described in `site/package.json` but here are the most
+important ones:
- [React](https://reactjs.org/) as framework
- [Typescript](https://www.typescriptlang.org/) to keep our sanity
- [Vite](https://vitejs.dev/) to build the project
-- [Material V4](https://v4.mui.com/) for UI components
+- [Material V5](https://mui.com/material-ui/getting-started/) for UI components
- [react-router](https://reactrouter.com/en/main) for routing
-- [TanStack Query v4](https://tanstack.com/query/v4/docs/react/overview) for fetching data
+- [TanStack Query v4](https://tanstack.com/query/v4/docs/react/overview) for
+ fetching data
- [XState](https://xstate.js.org/docs/) for handling complex state flows
- [axios](https://github.com/axios/axios) as fetching lib
- [Playwright](https://playwright.dev/) for E2E testing
- [Jest](https://jestjs.io/) for integration testing
-- [Storybook](https://storybook.js.org/) and [Chromatic](https://www.chromatic.com/) for visual testing
+- [Storybook](https://storybook.js.org/) and
+ [Chromatic](https://www.chromatic.com/) for visual testing
+- [PNPM](https://pnpm.io/) as package manager
## Structure
-All the code related to the UI is inside the `site` folder and we defined a few conventions to help people to navigate through it.
+All the code related to the UI is inside the `site` folder and we defined a few
+conventions to help people to navigate through it.
- **e2e** - E2E tests
- **src** - Source code
@@ -42,38 +58,69 @@ All the code related to the UI is inside the `site` folder and we defined a few
- **pages** - Page components
- **testHelpers** - Helper functions to help with integration tests
- **util** - Helper functions that can be used across the application
- - **xServices** - XState machines used to fetch data and handle complex scenarios
+ - **xServices** - XState machines used to fetch data and handle complex
+ scenarios
- **static** - UI static assets like images, fonts, icons, etc
## Routing
-We use [react-router](https://reactrouter.com/en/main) as our routing engine and adding a new route is very easy. If the new route needs to be authenticated, put it under the `` route and if it needs to live inside of the dashboard, put it under the `` route.
+We use [react-router](https://reactrouter.com/en/main) as our routing engine and
+adding a new route is very easy. If the new route needs to be authenticated, put
+it under the `` route and if it needs to live inside of the
+dashboard, put it under the `` route.
-The `RequireAuth` component handles all the authentication logic for the routes and the `DashboardLayout` wraps the route adding a navbar and passing down common dashboard data.
+The `RequireAuth` component handles all the authentication logic for the routes
+and the `DashboardLayout` wraps the route adding a navbar and passing down
+common dashboard data.
## Pages
-Pages are the top-level components of the app. The page component lives under the `src/pages` folder and each page should have its own folder so we can better group the views, tests, utility functions and so on. We use a structure where the page component is responsible for fetching all the data and passing it down to the view. We explain this decision a bit better in the next section.
+Pages are the top-level components of the app. The page component lives under
+the `src/pages` folder and each page should have its own folder so we can better
+group the views, tests, utility functions and so on. We use a structure where
+the page component is responsible for fetching all the data and passing it down
+to the view. We explain this decision a bit better in the next section.
-> ℹ️ Code that is only related to the page should live inside of the page folder but if at some point it is used in other pages or components, you should consider moving it to the `src` level in the `utils`, `hooks` or `components` folder.
+> ℹ️ Code that is only related to the page should live inside of the page folder
+> but if at some point it is used in other pages or components, you should
+> consider moving it to the `src` level in the `utils`, `hooks` or `components`
+> folder.
### States
-A page usually has at least three states: **loading**, **ready** or **success**, and **error** so remember to always handle these scenarios while you are coding a page. We also encourage you to add visual testing for these three states using the `*.stories.ts` file.
+A page usually has at least three states: **loading**, **ready** or **success**,
+and **error** so remember to always handle these scenarios while you are coding
+a page. We also encourage you to add visual testing for these three states using
+the `*.stories.ts` file.
## Fetching data
-We use [TanStack Query v4](https://tanstack.com/query/v4/docs/react/overview)(previously known as react-query) to fetch data from the API. We also use [XState](https://xstate.js.org/docs/) to handle complex flows with multiple states and transitions.
+We use
+[TanStack Query v4](https://tanstack.com/query/v4/docs/react/overview)(previously
+known as react-query) to fetch data from the API. We also use
+[XState](https://xstate.js.org/docs/) to handle complex flows with multiple
+states and transitions.
-> ℹ️ We recently changed how we are going to fetch data from the server so you will see a lot of fetches being made using XState machines but feel free to refactor it if you are already touching those files.
+> ℹ️ We recently changed how we are going to fetch data from the server so you
+> will see a lot of fetches being made using XState machines but feel free to
+> refactor it if you are already touching those files.
### Where to fetch data
-Finding the right place to fetch data in React apps is the one million dollar question but we decided to make it only in the page components and pass the props down to the views. This makes it easier to find where data is being loaded and easy to test using Storybook. So you will see components like `UsersPage` and `UsersPageView`.
+Finding the right place to fetch data in React apps is the one million dollar
+question but we decided to make it only in the page components and pass the
+props down to the views. This makes it easier to find where data is being loaded
+and easy to test using Storybook. So you will see components like `UsersPage`
+and `UsersPageView`.
### API
-We are using [axios](https://github.com/axios/axios) as our fetching library and writing the API functions in the `site/src/api/api.ts` files. We also have auto-generated types from our Go server on `site/src/api/typesGenerated.ts`. Usually, every endpoint has its own ` Request` and `Response` types but sometimes you need to pass extra parameters to make the call like the example below:
+We are using [axios](https://github.com/axios/axios) as our fetching library and
+writing the API functions in the `site/src/api/api.ts` files. We also have
+auto-generated types from our Go server on `site/src/api/typesGenerated.ts`.
+Usually, every endpoint has its own ` Request` and `Response` types but
+sometimes you need to pass extra parameters to make the call like the example
+below:
```ts
export const getAgentListeningPorts = async (
@@ -86,7 +133,8 @@ export const getAgentListeningPorts = async (
}
```
-Sometimes, a FE operation can have multiple API calls so it is ok to wrap it as a single function.
+Sometimes, a FE operation can have multiple API calls so it is ok to wrap it as
+a single function.
```ts
export const updateWorkspaceVersion = async (
@@ -97,45 +145,73 @@ export const updateWorkspaceVersion = async (
}
```
-If you need more granular errors or control, you may should consider keep them separated and use XState for that.
+If you need more granular errors or control, you may should consider keep them
+separated and use XState for that.
## Components
-We are using [Material V4](https://v4.mui.com/) in our UI and we don't have any short-term plans to update or even replace it. It still provides good value for us and changing it would cost too much work which is not valuable right now but of course, it can change in the future.
+We are using [Material V4](https://v4.mui.com/) in our UI and we don't have any
+short-term plans to update or even replace it. It still provides good value for
+us and changing it would cost too much work which is not valuable right now but
+of course, it can change in the future.
### Structure
-Each component gets its own folder. Make sure you add a test and Storybook stories for the component as well. By keeping these tidy, the codebase will remain easy to navigate, healthy and maintainable for all contributors.
+Each component gets its own folder. Make sure you add a test and Storybook
+stories for the component as well. By keeping these tidy, the codebase will
+remain easy to navigate, healthy and maintainable for all contributors.
### Accessibility
-We strive to keep our UI accessible. When using colors, avoid adding new elements with low color contrast. Always use labels on inputs, not just placeholders. These are important for screen-readers.
+We strive to keep our UI accessible. When using colors, avoid adding new
+elements with low color contrast. Always use labels on inputs, not just
+placeholders. These are important for screen-readers.
### Should I create a new component?
-As with most things in the world, it depends. If you are creating a new component to encapsulate some UI abstraction like `UsersTable` it is ok but you should always try to use the base components that are provided by the library or from the codebase so I recommend you to always do a quick search before creating a custom primitive component like dialogs, popovers, buttons, etc.
+As with most things in the world, it depends. If you are creating a new
+component to encapsulate some UI abstraction like `UsersTable` it is ok but you
+should always try to use the base components that are provided by the library or
+from the codebase so I recommend you to always do a quick search before creating
+a custom primitive component like dialogs, popovers, buttons, etc.
## Testing
-We use three types of testing in our app: **E2E**, **Integration** and **Visual Testing**.
+We use three types of testing in our app: **E2E**, **Integration** and **Visual
+Testing**.
### E2E (end-to-end)
-Are useful to test complete flows like "Create a user", "Import template", etc. For this one, we use [Playwright](https://playwright.dev/). If you only need to test if the page is being rendered correctly, you should probably consider using the **Visual Testing** approach.
+Are useful to test complete flows like "Create a user", "Import template", etc.
+For this one, we use [Playwright](https://playwright.dev/). If you only need to
+test if the page is being rendered correctly, you should probably consider using
+the **Visual Testing** approach.
-> ℹ️ For scenarios where you need to be authenticated, you can use `test.use({ storageState: getStatePath("authState") })`.
+> ℹ️ For scenarios where you need to be authenticated, you can use
+> `test.use({ storageState: getStatePath("authState") })`.
### Integration
-Test user interactions like "Click in a button shows a dialog", "Submit the form sends the correct data", etc. For this, we use [Jest](https://jestjs.io/) and [react-testing-library](https://testing-library.com/docs/react-testing-library/intro/). If the test involves routing checks like redirects or maybe checking the info on another page, you should probably consider using the **E2E** approach.
+Test user interactions like "Click in a button shows a dialog", "Submit the form
+sends the correct data", etc. For this, we use [Jest](https://jestjs.io/) and
+[react-testing-library](https://testing-library.com/docs/react-testing-library/intro/).
+If the test involves routing checks like redirects or maybe checking the info on
+another page, you should probably consider using the **E2E** approach.
### Visual testing
-Test components without user interaction like testing if a page view is rendered correctly depending on some parameters, if the button is showing a spinner if the `loading` props are passing, etc. This should always be your first option since it is way easier to maintain. For this, we use [Storybook](https://storybook.js.org/) and [Chromatic](https://www.chromatic.com/).
+Test components without user interaction like testing if a page view is rendered
+correctly depending on some parameters, if the button is showing a spinner if
+the `loading` props are passing, etc. This should always be your first option
+since it is way easier to maintain. For this, we use
+[Storybook](https://storybook.js.org/) and
+[Chromatic](https://www.chromatic.com/).
### What should I test?
-Choosing what to test is not always easy since there are a lot of flows and a lot of things can happen but these are a few indicators that can help you with that:
+Choosing what to test is not always easy since there are a lot of flows and a
+lot of things can happen but these are a few indicators that can help you with
+that:
- Things that can block the user
- Reported bugs
@@ -143,18 +219,27 @@ Choosing what to test is not always easy since there are a lot of flows and a lo
### Tests getting too slow
-A few times you can notice tests can take a very long time to get done. Sometimes it is because the test itself is complex and runs a lot of stuff, and sometimes it is because of how we are querying things. In the next section, we are going to talk more about them.
+A few times you can notice tests can take a very long time to get done.
+Sometimes it is because the test itself is complex and runs a lot of stuff, and
+sometimes it is because of how we are querying things. In the next section, we
+are going to talk more about them.
#### Using `ByRole` queries
-One thing we figured out that was slowing down our tests was the use of `ByRole` queries because of how it calculates the role attribute for every element on the `screen`. You can read more about it on the links below:
+One thing we figured out that was slowing down our tests was the use of `ByRole`
+queries because of how it calculates the role attribute for every element on the
+`screen`. You can read more about it on the links below:
- https://stackoverflow.com/questions/69711888/react-testing-library-getbyrole-is-performing-extremely-slowly
- https://github.com/testing-library/dom-testing-library/issues/552#issuecomment-625172052
-Even with `ByRole` having performance issues we still want to use it but for that, we have to scope the "querying" area by using the `within` command. So instead of using `screen.getByRole("button")` directly we could do `within(form).getByRole("button")`.
+Even with `ByRole` having performance issues we still want to use it but for
+that, we have to scope the "querying" area by using the `within` command. So
+instead of using `screen.getByRole("button")` directly we could do
+`within(form).getByRole("button")`.
-❌ Not ideal. If the screen has a hundred or thousand elements it can be VERY slow.
+❌ Not ideal. If the screen has a hundred or thousand elements it can be VERY
+slow.
```tsx
user.click(screen.getByRole("button"))
@@ -169,7 +254,9 @@ user.click(within(form).getByRole("button"))
#### `jest.spyOn` with the API is not working
-For some unknown reason, we figured out the `jest.spyOn` is not able to mock the API function when they are passed directly into the services XState machine configuration.
+For some unknown reason, we figured out the `jest.spyOn` is not able to mock the
+API function when they are passed directly into the services XState machine
+configuration.
❌ Does not work
diff --git a/docs/dotfiles.md b/docs/dotfiles.md
index af87e116bb853..7ce12f5b226b6 100644
--- a/docs/dotfiles.md
+++ b/docs/dotfiles.md
@@ -36,13 +36,13 @@ resource "coder_agent" "main" {
## Persistent Home
-Sometimes you want to support personalization without
-requiring dotfiles.
+Sometimes you want to support personalization without requiring dotfiles.
In such cases:
- Mount a persistent volume to the `/home` directory
-- Set the `startup_script` to call a `~/personalize` script that the user can edit
+- Set the `startup_script` to call a `~/personalize` script that the user can
+ edit
```hcl
resource "coder_agent" "main" {
@@ -63,7 +63,8 @@ sudo apt install -y neovim fish cargo
## Setup script support
-User can setup their dotfiles by creating one of the following script files in their dotfiles repo:
+User can setup their dotfiles by creating one of the following script files in
+their dotfiles repo:
- `install.sh`
- `install`
@@ -74,9 +75,13 @@ User can setup their dotfiles by creating one of the following script files in t
- `setup`
- `script/setup`
-If any of the above files are found (in the specified order), Coder will try to execute the first match. After the first match is found, other files will be ignored.
+If any of the above files are found (in the specified order), Coder will try to
+execute the first match. After the first match is found, other files will be
+ignored.
-The setup script must be executable, otherwise the dotfiles setup will fail. If you encounter this issue, you can fix it by making the script executable using the following commands:
+The setup script must be executable, otherwise the dotfiles setup will fail. If
+you encounter this issue, you can fix it by making the script executable using
+the following commands:
```shell
cd
diff --git a/docs/enterprise.md b/docs/enterprise.md
index f2be0c759642f..60b4af4be19e0 100644
--- a/docs/enterprise.md
+++ b/docs/enterprise.md
@@ -1,8 +1,8 @@
# Enterprise Features
-Coder is free to use and includes some features that are only accessible with a paid license.
-[Contact Sales](https://coder.com/contact) for pricing or [get a free
-trial](https://coder.com/trial).
+Coder is free to use and includes some features that are only accessible with a
+paid license. [Contact Sales](https://coder.com/contact) for pricing or
+[get a free trial](https://coder.com/trial).
| Category | Feature | Open Source | Enterprise |
| --------------- | ------------------------------------------------------------------------------------ | :---------: | :--------: |
@@ -20,11 +20,20 @@ trial](https://coder.com/trial).
| Deployment | [Isolated Terraform Runners](./admin/provisioners.md) | ❌ | ✅ |
| Deployment | [Workspace Proxies](./admin/workspace-proxies.md) | ❌ | ✅ |
-> Previous plans to restrict OIDC and Git Auth features in OSS have been removed
-> as of 2023-01-11
-
## Adding your license key
+There are two ways to add an enterprise license to a Coder deployment: In the
+Coder UI or with the Coder CLI.
+
+### Coder UI
+
+Click Deployment, Licenses, Add a license then drag or select the license file
+with the `jwt` extension.
+
+
+
+### Coder CLI
+
### Requirements
- Your license key
diff --git a/docs/ides.md b/docs/ides.md
index d53e7e501a88d..c5aafcec4813a 100644
--- a/docs/ides.md
+++ b/docs/ides.md
@@ -18,25 +18,30 @@ support should work:
## Visual Studio Code
-Click `VS Code Desktop` in the dashboard to one-click enter a workspace. This automatically installs the [Coder Remote](https://github.com/coder/vscode-coder) extension, authenticates with Coder, and connects to the workspace.
+Click `VS Code Desktop` in the dashboard to one-click enter a workspace. This
+automatically installs the [Coder Remote](https://github.com/coder/vscode-coder)
+extension, authenticates with Coder, and connects to the workspace.

-You can set the default directory in which VS Code opens via the `dir` argument on
-the `coder_agent` resource in your workspace template. See the [Terraform documentation
-for more details](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#dir).
+You can set the default directory in which VS Code opens via the `dir` argument
+on the `coder_agent` resource in your workspace template. See the
+[Terraform documentation for more details](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#dir).
-> The `VS Code Desktop` button can be hidden by enabling [Browser-only connections](./networking/index.md#Browser-only).
+> The `VS Code Desktop` button can be hidden by enabling
+> [Browser-only connections](./networking/index.md#Browser-only).
### Manual Installation
-Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
+Launch VS Code Quick Open (Ctrl+P), paste the following command, and press
+enter.
```text
ext install coder.coder-remote
```
-Alternatively, manually install the VSIX from the [latest release](https://github.com/coder/vscode-coder/releases/latest).
+Alternatively, manually install the VSIX from the
+[latest release](https://github.com/coder/vscode-coder/releases/latest).
## SSH configuration
@@ -45,7 +50,7 @@ Alternatively, manually install the VSIX from the [latest release](https://githu
To access Coder via SSH, run the following in the terminal:
-```console
+```shell
coder config-ssh
```
@@ -74,8 +79,8 @@ Setting up Gateway also involves picking a project directory, so if you have not
already done so, you may wish to open a terminal on your Coder workspace and
check out a copy of the project you intend to work on.
-After installing Gateway on your local system, [follow these steps to create a
-Connection and connect to your Coder workspace.](./ides/gateway.md)
+After installing Gateway on your local system,
+[follow these steps to create a Connection and connect to your Coder workspace.](./ides/gateway.md)
| Version | Status | Notes |
| --------- | ------- | -------------------------------------------------------- |
@@ -85,7 +90,8 @@ Connection and connect to your Coder workspace.](./ides/gateway.md)
## Web IDEs (Jupyter, code-server, JetBrains Projector)
-Web IDEs (code-server, JetBrains Projector, VNC, etc.) are defined in the template. See [IDEs](./ides/web-ides.md).
+Web IDEs (code-server, JetBrains Projector, VNC, etc.) are defined in the
+template. See [IDEs](./ides/web-ides.md).
## Up next
diff --git a/docs/ides/emacs-tramp.md b/docs/ides/emacs-tramp.md
index 0ae6d0aef7db7..9a33bd0141716 100644
--- a/docs/ides/emacs-tramp.md
+++ b/docs/ides/emacs-tramp.md
@@ -1,6 +1,7 @@
# Emacs TRAMP
-[Emacs TRAMP](https://www.emacswiki.org/emacs/TrampMode) is a method of running editing operations on a remote server.
+[Emacs TRAMP](https://www.emacswiki.org/emacs/TrampMode) is a method of running
+editing operations on a remote server.
## Connecting To A Workspace
@@ -10,13 +11,17 @@ To connect to your workspace first run:
coder config-ssh
```
-Then you can connect to your workspace by its name in the format: `coder.`.
+Then you can connect to your workspace by its name in the format:
+`coder.`.
-In Emacs type `C-x d` and then input: `/-:coder.:` and hit enter. This will open up Dired on the workspace's home directory.
+In Emacs type `C-x d` and then input: `/-:coder.:` and hit
+enter. This will open up Dired on the workspace's home directory.
### Using SSH
-By default Emacs TRAMP is setup to use SCP to access files on the Coder workspace instance. However you might want to use SSH if you have a jumpbox or some other complex network setup.
+By default Emacs TRAMP is setup to use SCP to access files on the Coder
+workspace instance. However you might want to use SSH if you have a jumpbox or
+some other complex network setup.
To do so set the following in your Emacs `init.el` file:
@@ -24,13 +29,17 @@ To do so set the following in your Emacs `init.el` file:
(setq tramp-default-method "ssh")
```
-Then when you access the workspace instance via `/-:coder.` Emacs will use SSH. Setting `tramp-default-method` will also tell `ansi-term` mode the correct way to access the remote when directory tracking.
+Then when you access the workspace instance via `/-:coder.`
+Emacs will use SSH. Setting `tramp-default-method` will also tell `ansi-term`
+mode the correct way to access the remote when directory tracking.
## Directory Tracking
### ansi-term
-If you run your terminal in Emacs via `ansi-term` then you might run into a problem where while SSH-ed into a workspace Emacs will not change its `default-directory` to open files in the directory your shell is in.
+If you run your terminal in Emacs via `ansi-term` then you might run into a
+problem where while SSH-ed into a workspace Emacs will not change its
+`default-directory` to open files in the directory your shell is in.
To fix this:
@@ -49,7 +58,8 @@ To fix this:
}
```
-2. Next in the shell profile file on the workspace (ex., `~/.bashrc` for Bash and `~/.zshrc` for Zsh) add the following:
+2. Next in the shell profile file on the workspace (ex., `~/.bashrc` for Bash
+ and `~/.zshrc` for Zsh) add the following:
```bash
ansi_term_announce_host() {
@@ -77,17 +87,24 @@ To fix this:
ansi_term_announce
```
- Ansi Term expects the terminal running inside of it to send escape codes to inform Emacs of the hostname, user, and working directory. The above code sends these escape codes and associated data whenever the terminal logs in and whenever the directory changes.
+ Ansi Term expects the terminal running inside of it to send escape codes to
+ inform Emacs of the hostname, user, and working directory. The above code
+ sends these escape codes and associated data whenever the terminal logs in
+ and whenever the directory changes.
### eshell
-The `eshell` mode will perform directory tracking by default, no additional configuration is needed.
+The `eshell` mode will perform directory tracking by default, no additional
+configuration is needed.
## Language Servers (Code Completion)
-If you use [`lsp-mode`](https://emacs-lsp.github.io/lsp-mode) for code intelligence and completion some additional configuration is required.
+If you use [`lsp-mode`](https://emacs-lsp.github.io/lsp-mode) for code
+intelligence and completion some additional configuration is required.
-In your Emacs `init.el` file you must register a LSP client and tell `lsp-mode` how to find it on the remote machine using the `lsp-register-client` function. For each LSP server you want to use in your workspace add the following:
+In your Emacs `init.el` file you must register a LSP client and tell `lsp-mode`
+how to find it on the remote machine using the `lsp-register-client` function.
+For each LSP server you want to use in your workspace add the following:
```lisp
(lsp-register-client (make-lsp-client :new-connection (lsp-tramp-connection "")
@@ -96,13 +113,27 @@ In your Emacs `init.el` file you must register a LSP client and tell `lsp-mode`
:server-id '))
```
-This tells `lsp-mode` to look for a language server binary named `` for use in `` on a machine named `coder.`. Be sure to replace the values between angle brackets:
-
-- `` : The name of the language server binary, without any path components. For example to use the Deno Javascript language server use the value `deno`.
-- ``: The name of the Emacs major mode for which the language server should be used. For example to enable the language server for Javascript development use the value `web-mode`.
-- ``: This is just the name that `lsp-mode` will use to refer to this language server. If you are ever looking for output buffers or files they may have this name in them.
-
-Calling the `lsp-register-client` function will tell `lsp-mode` the name of the LSP server binary. However this binary must be accessible via the path. If the language server binary is not in the path you must modify `tramp-remote-path` so that `lsp-mode` knows in what directories to look for the LSP server. To do this use TRAMP's connection profiles functionality. These connection profiles let you customize variables depending on what machine you are connected to. Add the following to your `init.el`:
+This tells `lsp-mode` to look for a language server binary named
+`` for use in `` on a machine named
+`coder.`. Be sure to replace the values between angle brackets:
+
+- `` : The name of the language server binary, without any
+ path components. For example to use the Deno Javascript language server use
+ the value `deno`.
+- ``: The name of the Emacs major mode for which the language
+ server should be used. For example to enable the language server for
+ Javascript development use the value `web-mode`.
+- ``: This is just the name that `lsp-mode` will use to
+ refer to this language server. If you are ever looking for output buffers or
+ files they may have this name in them.
+
+Calling the `lsp-register-client` function will tell `lsp-mode` the name of the
+LSP server binary. However this binary must be accessible via the path. If the
+language server binary is not in the path you must modify `tramp-remote-path` so
+that `lsp-mode` knows in what directories to look for the LSP server. To do this
+use TRAMP's connection profiles functionality. These connection profiles let you
+customize variables depending on what machine you are connected to. Add the
+following to your `init.el`:
```lisp
(connection-local-set-profile-variables 'remote-path-lsp-servers
@@ -110,9 +141,20 @@ Calling the `lsp-register-client` function will tell `lsp-mode` the name of the
(connection-local-set-profiles '(:machine "coder.") 'remote-path-lsp-servers)
```
-The `connection-local-set-profile-variables` function creates a new connection profile by the name `remote-path-lsp-servers`. The `connection-local-set-profiles` then indicates this `remote-path-lsp-servers` connection profile should be used when connecting to a server named `coder.`. Be sure to replace `` with the directory in which a LSP server is present.
-
-TRAMP and `lsp-mode` are fickle friends, sometimes there is weird behavior. If you find that language servers are hanging in the `starting` state then [it might be helpful](https://github.com/emacs-lsp/lsp-mode/issues/2709#issuecomment-800868919) to set the `lsp-log-io` variable to `t`.
-
-More details on configuring `lsp-mode` for TRAMP can be found [in the `lsp-mode` documentation](https://emacs-lsp.github.io/lsp-mode/page/remote/).
-The [TRAMP `tramp-remote-path` documentation](https://www.gnu.org/software/emacs/manual/html_node/tramp/Remote-programs.html#Remote-programs) contains more examples and details of connection profiles.
+The `connection-local-set-profile-variables` function creates a new connection
+profile by the name `remote-path-lsp-servers`. The
+`connection-local-set-profiles` then indicates this `remote-path-lsp-servers`
+connection profile should be used when connecting to a server named
+`coder.`. Be sure to replace `` with the directory
+in which a LSP server is present.
+
+TRAMP and `lsp-mode` are fickle friends, sometimes there is weird behavior. If
+you find that language servers are hanging in the `starting` state then
+[it might be helpful](https://github.com/emacs-lsp/lsp-mode/issues/2709#issuecomment-800868919)
+to set the `lsp-log-io` variable to `t`.
+
+More details on configuring `lsp-mode` for TRAMP can be found
+[in the `lsp-mode` documentation](https://emacs-lsp.github.io/lsp-mode/page/remote/).
+The
+[TRAMP `tramp-remote-path` documentation](https://www.gnu.org/software/emacs/manual/html_node/tramp/Remote-programs.html#Remote-programs)
+contains more examples and details of connection profiles.
diff --git a/docs/ides/gateway.md b/docs/ides/gateway.md
index cb5b62be53f3f..eb27158a45b2b 100644
--- a/docs/ides/gateway.md
+++ b/docs/ides/gateway.md
@@ -1,43 +1,73 @@
# JetBrains Gateway
-JetBrains Gateway is a compact desktop app that allows you to work remotely with a JetBrains IDE without even downloading one. [See JetBrains' website to learn about and Gateway.](https://www.jetbrains.com/remote-development/gateway/)
+JetBrains Gateway is a compact desktop app that allows you to work remotely with
+a JetBrains IDE without even downloading one.
+[See JetBrains' website to learn about and Gateway.](https://www.jetbrains.com/remote-development/gateway/)
-Gateway can connect to a Coder workspace by using Coder's Gateway plugin or manually setting up an SSH connection.
+Gateway can connect to a Coder workspace by using Coder's Gateway plugin or
+manually setting up an SSH connection.
## Using Coder's JetBrains Gateway Plugin
-> If you experience problems, please [create a GitHub issue](https://github.com/coder/coder/issues) or share in [our Discord channel](https://discord.gg/coder).
+> If you experience problems, please
+> [create a GitHub issue](https://github.com/coder/coder/issues) or share in
+> [our Discord channel](https://discord.gg/coder).
1. [Install Gateway](https://www.jetbrains.com/help/idea/jetbrains-gateway.html)
-1. Open Gateway and click the gear icon at the bottom left and then "Settings"
-1. In the Marketplace tab within Plugins, type Coder and then click "Install" and "OK"
- 
-1. Click the new "Coder" icon on the Gateway home screen
+1. Open Gateway and click the Coder icon to install the Coder plugin.
+1. Click the "Coder" icon under Install More Providers at the bottom of the
+ Gateway home screen
+1. Click "Connect to Coder" at the top of the Gateway home screen to launch the
+ plugin
+

-1. Enter your Coder deployment's Access Url and click "Connect" then paste the Session Token and click "OK"
+
+1. Enter your Coder deployment's Access Url and click "Connect" then paste the
+ Session Token and click "OK"
+

-1. Click the "+" icon to open a browser and go to the templates page in your Coder deployment to create a workspace
-1. If a workspace already exists but is stopped, click the green arrow to start the workspace
+
+1. Click the "+" icon to open a browser and go to the templates page in your
+ Coder deployment to create a workspace
+
+1. If a workspace already exists but is stopped, click the green arrow to start
+ the workspace
+
1. Once the workspace status says Running, click "Select IDE and Project"
+

-1. Select the JetBrains IDE for your project and the project directory then click "Start IDE and connect"
+
+1. Select the JetBrains IDE for your project and the project directory then
+ click "Start IDE and connect"

+

-> Note the JetBrains IDE is remotely installed into `~/.cache/JetBrains/RemoteDev/dist`
+> Note the JetBrains IDE is remotely installed into
+> `~/.cache/JetBrains/RemoteDev/dist`
+
+### Update a Coder plugin version
+
+1. Click the gear icon at the bottom left of the Gateway home screen and then
+ "Settings"
+
+1. In the Marketplace tab within Plugins, type Coder and if a newer plugin
+ release is available, click "Update" and "OK"
+
+ 
### Configuring the Gateway plugin to use internal certificates
-When attempting to connect to a Coder deployment that uses internally signed certificates,
-you may receive the following error in Gateway:
+When attempting to connect to a Coder deployment that uses internally signed
+certificates, you may receive the following error in Gateway:
```console
Failed to configure connection to https://coder.internal.enterprise/: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
```
-To resolve this issue, you will need to add Coder's certificate to the Java trust store
-present on your local machine. Here is the default location of the trust store for
-each OS:
+To resolve this issue, you will need to add Coder's certificate to the Java
+trust store present on your local machine. Here is the default location of the
+trust store for each OS:
```console
# Linux
@@ -52,8 +82,8 @@ C:\Program Files (x86)\\jre\lib\security\cacerts
%USERPROFILE%\AppData\Local\JetBrains\Toolbox\bin\jre\lib\security\cacerts # Path for Toolbox installation
```
-To add the certificate to the keystore, you can use the `keytool` utility that ships
-with Java:
+To add the certificate to the keystore, you can use the `keytool` utility that
+ships with Java:
```console
keytool -import -alias coder -file -keystore /path/to/trust/store
@@ -71,42 +101,67 @@ Windows example:
macOS example:
-```sh
+```shell
keytool -import -alias coder -file cacert.pem -keystore /Applications/JetBrains\ Gateway.app/Contents/jbr/Contents/Home/lib/security/cacerts
```
## Manually Configuring A JetBrains Gateway Connection
-> This is in lieu of using Coder's Gateway plugin which automatically performs these steps.
+> This is in lieu of using Coder's Gateway plugin which automatically performs
+> these steps.
1. [Install Gateway](https://www.jetbrains.com/help/idea/jetbrains-gateway.html)
+
1. [Configure the `coder` CLI](../ides.md#ssh-configuration)
+
1. Open Gateway, make sure "SSH" is selected under "Remote Development"
+
1. Click "New Connection"
+

+
1. In the resulting dialog, click the gear icon to the right of "Connection:"
+

+
1. Hit the "+" button to add a new SSH connection
+

1. For the Host, enter `coder.`
+
1. For the Port, enter `22` (this is ignored by Coder)
+
1. For the Username, enter your workspace username
-1. For the Authentication Type, select "OpenSSH config and authentication
- agent"
+
+1. For the Authentication Type, select "OpenSSH config and authentication agent"
+
1. Make sure the checkbox for "Parse config file ~/.ssh/config" is checked.
+
1. Click "Test Connection" to validate these settings.
+
1. Click "OK"
+

+
1. Select the connection you just added
- 
+
+ 
+
1. Click "Check Connection and Continue"
+

-1. Select the JetBrains IDE for your project and the project directory.
- SSH into your server to create a directory or check out code if you haven't already.
+
+1. Select the JetBrains IDE for your project and the project directory. SSH into
+ your server to create a directory or check out code if you haven't already.
+

- > Note the JetBrains IDE is remotely installed into `~/. cache/JetBrains/RemoteDev/dist`
+
+ > Note the JetBrains IDE is remotely installed into
+ > `~/. cache/JetBrains/RemoteDev/dist`
+
1. Click "Download and Start IDE" to connect.
+

## Using an existing JetBrains installation in the workspace
@@ -116,12 +171,20 @@ are air-gapped, and cannot reach jetbrains.com), run the following script in the
JetBrains IDE directory to point the default Gateway directory to the IDE
directory. This step must be done before configuring Gateway.
-```sh
+```shell
cd /opt/idea/bin
./remote-dev-server.sh registerBackendLocationForGateway
```
-> Gateway only works with paid versions of JetBrains IDEs so the script will not be located in the `bin` directory of JetBrains Community editions.
+> Gateway only works with paid versions of JetBrains IDEs so the script will not
+> be located in the `bin` directory of JetBrains Community editions.
[Here is the JetBrains article](https://www.jetbrains.com/help/idea/remote-development-troubleshooting.html#setup:~:text=Can%20I%20point%20Remote%20Development%20to%20an%20existing%20IDE%20on%20my%20remote%20server%3F%20Is%20it%20possible%20to%20install%20IDE%20manually%3F)
explaining this IDE specification.
+
+## JetBrains Gateway in an offline environment
+
+In networks that restrict access to the internet, you will need to leverage the
+JetBrains Client Installer to download and save the IDE clients locally. Please
+see the
+[JetBrains documentation for more information](https://www.jetbrains.com/help/idea/fully-offline-mode.html).
diff --git a/docs/ides/remote-desktops.md b/docs/ides/remote-desktops.md
index f7496a1d5620c..51ffe4e264cd6 100644
--- a/docs/ides/remote-desktops.md
+++ b/docs/ides/remote-desktops.md
@@ -1,6 +1,7 @@
# Remote Desktops
-> Built-in remote desktop is on the roadmap ([#2106](https://github.com/coder/coder/issues/2106)).
+> Built-in remote desktop is on the roadmap
+> ([#2106](https://github.com/coder/coder/issues/2106)).
## VNC Desktop
@@ -13,10 +14,13 @@ Workspace requirements:
- VNC server (e.g. [tigervnc](https://tigervnc.org/))
- VNC client (e.g. [novnc](https://novnc.com/info.html))
-Installation instructions vary depending on your workspace's operating
-system, platform, and build system.
+Installation instructions vary depending on your workspace's operating system,
+platform, and build system.
-As a starting point, see the [desktop-container](https://github.com/bpmct/coder-templates/tree/main/desktop-container) community template. It builds and provisions a Dockerized workspace with the following software:
+As a starting point, see the
+[desktop-container](https://github.com/bpmct/coder-templates/tree/main/desktop-container)
+community template. It builds and provisions a Dockerized workspace with the
+following software:
- Ubuntu 20.04
- TigerVNC server
@@ -25,9 +29,13 @@ As a starting point, see the [desktop-container](https://github.com/bpmct/coder-
## RDP Desktop
-To use RDP with Coder, you'll need to install an [RDP client](https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-clients) on your local machine, and enable RDP on your workspace.
+To use RDP with Coder, you'll need to install an
+[RDP client](https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-clients)
+on your local machine, and enable RDP on your workspace.
-As a starting point, see the [gcp-windows-rdp](https://github.com/matifali/coder-templates/tree/main/gcp-windows-rdp) community template. It builds and provisions a Windows Server workspace on GCP.
+As a starting point, see the
+[gcp-windows-rdp](https://github.com/matifali/coder-templates/tree/main/gcp-windows-rdp)
+community template. It builds and provisions a Windows Server workspace on GCP.
Use the following command to forward the RDP port to your local machine:
diff --git a/docs/ides/web-ides.md b/docs/ides/web-ides.md
index 1b5df48e1e589..e1e7cecadf761 100644
--- a/docs/ides/web-ides.md
+++ b/docs/ides/web-ides.md
@@ -11,8 +11,8 @@ It's common to also let developers to connect via web IDEs.
In Coder, web IDEs are defined as
[coder_app](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app)
-resources in the template. With our generic model, any web application can
-be used as a Coder application. For example:
+resources in the template. With our generic model, any web application can be
+used as a Coder application. For example:
```hcl
# Add button to open Portainer in the workspace dashboard
@@ -36,7 +36,9 @@ resource "coder_app" "portainer" {

-[code-server](https://github.com/coder/coder) is our supported method of running VS Code in the web browser. A simple way to install code-server in Linux/macOS workspaces is via the Coder agent in your template:
+[code-server](https://github.com/coder/coder) is our supported method of running
+VS Code in the web browser. A simple way to install code-server in Linux/macOS
+workspaces is via the Coder agent in your template:
```console
# edit your template
@@ -62,7 +64,9 @@ resource "coder_agent" "main" {
}
```
-For advanced use, we recommend installing code-server in your VM snapshot or container image. Here's a Dockerfile which leverages some special [code-server features](https://coder.com/docs/code-server/):
+For advanced use, we recommend installing code-server in your VM snapshot or
+container image. Here's a Dockerfile which leverages some special
+[code-server features](https://coder.com/docs/code-server/):
```Dockerfile
FROM codercom/enterprise-base:ubuntu
@@ -79,7 +83,8 @@ RUN code-server --install-extension eamodio.gitlens
# or use a process manager like supervisord
```
-You'll also need to specify a `coder_app` resource related to the agent. This is how code-server is displayed on the workspace page.
+You'll also need to specify a `coder_app` resource related to the agent. This is
+how code-server is displayed on the workspace page.
```hcl
resource "coder_app" "code-server" {
diff --git a/docs/images/add-license-ui.png b/docs/images/add-license-ui.png
new file mode 100644
index 0000000000000..03ff419d15a59
Binary files /dev/null and b/docs/images/add-license-ui.png differ
diff --git a/docs/images/deploy-pr-manually.png b/docs/images/deploy-pr-manually.png
new file mode 100644
index 0000000000000..718d00c65d1cc
Binary files /dev/null and b/docs/images/deploy-pr-manually.png differ
diff --git a/docs/images/gateway/plugin-connect-to-coder.png b/docs/images/gateway/plugin-connect-to-coder.png
index 11cecf4ad69fe..295efa7897386 100644
Binary files a/docs/images/gateway/plugin-connect-to-coder.png and b/docs/images/gateway/plugin-connect-to-coder.png differ
diff --git a/docs/images/gateway/plugin-ide-list.png b/docs/images/gateway/plugin-ide-list.png
index 40b9f2f71bcc8..0f767abfd0dd1 100644
Binary files a/docs/images/gateway/plugin-ide-list.png and b/docs/images/gateway/plugin-ide-list.png differ
diff --git a/docs/images/gateway/plugin-select-ide.png b/docs/images/gateway/plugin-select-ide.png
index b138b7b2a41d5..bc4e6cb1ee03b 100644
Binary files a/docs/images/gateway/plugin-select-ide.png and b/docs/images/gateway/plugin-select-ide.png differ
diff --git a/docs/images/gateway/plugin-session-token.png b/docs/images/gateway/plugin-session-token.png
index c233fd0c27759..b13d2fe12828e 100644
Binary files a/docs/images/gateway/plugin-session-token.png and b/docs/images/gateway/plugin-session-token.png differ
diff --git a/docs/images/gateway/plugin-settings-marketplace.png b/docs/images/gateway/plugin-settings-marketplace.png
index 58ccf8155992e..7973fa99811a3 100644
Binary files a/docs/images/gateway/plugin-settings-marketplace.png and b/docs/images/gateway/plugin-settings-marketplace.png differ
diff --git a/docs/images/oidc-sequence-diagram.svg b/docs/images/oidc-sequence-diagram.svg
new file mode 100644
index 0000000000000..72600e5193201
--- /dev/null
+++ b/docs/images/oidc-sequence-diagram.svg
@@ -0,0 +1,15136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/images/pr-deploy-manual.png b/docs/images/pr-deploy-manual.png
deleted file mode 100644
index eab92cc2249e7..0000000000000
Binary files a/docs/images/pr-deploy-manual.png and /dev/null differ
diff --git a/docs/install/binary.md b/docs/install/binary.md
index 33e2bbc1c5f4d..8e646816945c5 100644
--- a/docs/install/binary.md
+++ b/docs/install/binary.md
@@ -1,15 +1,22 @@
-Coder publishes self-contained .zip and .tar.gz archives in [GitHub releases](https://github.com/coder/coder/releases/latest). The archives bundle `coder` binary.
+Coder publishes self-contained .zip and .tar.gz archives in
+[GitHub releases](https://github.com/coder/coder/releases/latest). The archives
+bundle `coder` binary.
-1. Download the [release archive](https://github.com/coder/coder/releases/latest) appropriate for your operating system
+1. Download the
+ [release archive](https://github.com/coder/coder/releases/latest) appropriate
+ for your operating system
-1. Unzip the folder you just downloaded, and move the `coder` executable to a location that's on your `PATH`
+1. Unzip the folder you just downloaded, and move the `coder` executable to a
+ location that's on your `PATH`
```console
# ex. macOS and Linux
mv coder /usr/local/bin
```
- > Windows users: see [this guide](https://answers.microsoft.com/en-us/windows/forum/all/adding-path-variable/97300613-20cb-4d85-8d0e-cc9d3549ba23) for adding folders to `PATH`.
+ > Windows users: see
+ > [this guide](https://answers.microsoft.com/en-us/windows/forum/all/adding-path-variable/97300613-20cb-4d85-8d0e-cc9d3549ba23)
+ > for adding folders to `PATH`.
1. Start a Coder server
@@ -21,9 +28,9 @@ Coder publishes self-contained .zip and .tar.gz archives in [GitHub releases](ht
coder server --postgres-url --access-url
```
- > Set `CODER_ACCESS_URL` to the external URL that users and workspaces will use to
- > connect to Coder. This is not required if you are using the tunnel. Learn more
- > about Coder's [configuration options](../admin/configure.md).
+ > Set `CODER_ACCESS_URL` to the external URL that users and workspaces will
+ > use to connect to Coder. This is not required if you are using the tunnel.
+ > Learn more about Coder's [configuration options](../admin/configure.md).
1. Visit the Coder URL in the logs to set up your first account, or use the CLI.
diff --git a/docs/install/database.md b/docs/install/database.md
index e88daa3b0ea7a..482ff22320053 100644
--- a/docs/install/database.md
+++ b/docs/install/database.md
@@ -1,11 +1,12 @@
## Recommendation
-For production deployments, we recommend using an external [PostgreSQL](https://www.postgresql.org/) database (version 13 or higher).
+For production deployments, we recommend using an external
+[PostgreSQL](https://www.postgresql.org/) database (version 13 or higher).
## Basic configuration
-Before starting the Coder server, prepare the database server by creating a role and a database.
-Remember that the role must have access to the created database.
+Before starting the Coder server, prepare the database server by creating a role
+and a database. Remember that the role must have access to the created database.
With `psql`:
@@ -19,8 +20,9 @@ With `psql -U coder`:
CREATE DATABASE coder;
```
-Coder configuration is defined via [environment variables](../admin/configure.md).
-The database client requires the connection string provided via the `CODER_PG_CONNECTION_URL` variable.
+Coder configuration is defined via
+[environment variables](../admin/configure.md). The database client requires the
+connection string provided via the `CODER_PG_CONNECTION_URL` variable.
```console
export CODER_PG_CONNECTION_URL="postgres://coder:secret42@localhost/coder?sslmode=disable"
@@ -28,7 +30,9 @@ export CODER_PG_CONNECTION_URL="postgres://coder:secret42@localhost/coder?sslmod
## Custom schema
-For installations with elevated security requirements, it's advised to use a separate [schema](https://www.postgresql.org/docs/current/ddl-schemas.html) instead of the public one.
+For installations with elevated security requirements, it's advised to use a
+separate [schema](https://www.postgresql.org/docs/current/ddl-schemas.html)
+instead of the public one.
With `psql -U coder`:
@@ -53,8 +57,10 @@ In this case the database client requires the modified connection string:
export CODER_PG_CONNECTION_URL="postgres://coder:secret42@localhost/coder?sslmode=disable&search_path=myschema"
```
-The `search_path` parameter determines the order of schemas in which they are visited while looking for a specific table.
-The first schema named in the search path is called the current schema. By default `search_path` defines the following schemas:
+The `search_path` parameter determines the order of schemas in which they are
+visited while looking for a specific table. The first schema named in the search
+path is called the current schema. By default `search_path` defines the
+following schemas:
```sql
SHOW search_path;
@@ -64,7 +70,8 @@ search_path
"$user", public
```
-Using the `search_path` in the connection string corresponds to the following `psql` command:
+Using the `search_path` in the connection string corresponds to the following
+`psql` command:
```sql
ALTER ROLE coder SET search_path = myschema;
@@ -74,8 +81,9 @@ ALTER ROLE coder SET search_path = myschema;
### Coder server fails startup with "current_schema: converting NULL to string is unsupported"
-Please make sure that the schema selected in the connection string `...&search_path=myschema` exists
-and the role has granted permissions to access it. The schema should be present on this listing:
+Please make sure that the schema selected in the connection string
+`...&search_path=myschema` exists and the role has granted permissions to access
+it. The schema should be present on this listing:
```console
psql -U coder -c '\dn'
diff --git a/docs/install/docker.md b/docs/install/docker.md
index 038146b30085b..e3b2196f941f7 100644
--- a/docs/install/docker.md
+++ b/docs/install/docker.md
@@ -1,15 +1,18 @@
-You can install and run Coder using the official Docker images published on [GitHub Container Registry](https://github.com/coder/coder/pkgs/container/coder).
+You can install and run Coder using the official Docker images published on
+[GitHub Container Registry](https://github.com/coder/coder/pkgs/container/coder).
## Requirements
-Docker is required. See the [official installation documentation](https://docs.docker.com/install/).
+Docker is required. See the
+[official installation documentation](https://docs.docker.com/install/).
-> Note that the below steps are only supported on a Linux distribution. If on macOS, please [run Coder via the standalone binary](./binary.md).
+> Note that the below steps are only supported on a Linux distribution. If on
+> macOS, please [run Coder via the standalone binary](./binary.md).
## Run Coder with the built-in database (quick)
-For proof-of-concept deployments, you can run a complete Coder instance with
-the following command.
+For proof-of-concept deployments, you can run a complete Coder instance with the
+following command.
```console
export CODER_DATA=$HOME/.config/coderv2-docker
@@ -27,8 +30,8 @@ ensure Coder has permissions to manage Docker via `docker.sock`. If the host
systems `/var/run/docker.sock` is not group writeable or does not belong to the
`docker` group, the above may not work as-is.
-Coder configuration is defined via environment variables.
-Learn more about Coder's [configuration options](../admin/configure.md).
+Coder configuration is defined via environment variables. Learn more about
+Coder's [configuration options](../admin/configure.md).
## Run Coder with access URL and external PostgreSQL (recommended)
@@ -44,13 +47,14 @@ docker run --rm -it \
ghcr.io/coder/coder:latest
```
-Coder configuration is defined via environment variables.
-Learn more about Coder's [configuration options](../admin/configure.md).
+Coder configuration is defined via environment variables. Learn more about
+Coder's [configuration options](../admin/configure.md).
## Run Coder with docker-compose
-Coder's publishes a [docker-compose example](https://github.com/coder/coder/blob/main/docker-compose.yaml) which includes
-an PostgreSQL container and volume.
+Coder's publishes a
+[docker-compose example](https://github.com/coder/coder/blob/main/docker-compose.yaml)
+which includes an PostgreSQL container and volume.
1. Install [Docker Compose](https://docs.docker.com/compose/install/)
@@ -62,9 +66,11 @@ an PostgreSQL container and volume.
3. Start Coder with `docker-compose up`:
- In order to use cloud-based templates (e.g. Kubernetes, AWS), you must have an external URL that users and workspaces will use to connect to Coder.
+ In order to use cloud-based templates (e.g. Kubernetes, AWS), you must have
+ an external URL that users and workspaces will use to connect to Coder.
- For proof-of-concept deployments, you can use [Coder's tunnel](../admin/configure.md#tunnel):
+ For proof-of-concept deployments, you can use
+ [Coder's tunnel](../admin/configure.md#tunnel):
```console
cd coder
@@ -72,7 +78,8 @@ an PostgreSQL container and volume.
docker-compose up
```
- For production deployments, we recommend setting an [access URL](../admin/configure.md#access-url):
+ For production deployments, we recommend setting an
+ [access URL](../admin/configure.md#access-url):
```console
cd coder
@@ -80,19 +87,24 @@ an PostgreSQL container and volume.
CODER_ACCESS_URL=https://coder.example.com docker-compose up
```
-4. Visit the web ui via the configured url. You can add `/login` to the base url to create the first user via the ui.
+4. Visit the web ui via the configured url. You can add `/login` to the base url
+ to create the first user via the ui.
-5. Follow the on-screen instructions log in and create your first template and workspace
+5. Follow the on-screen instructions log in and create your first template and
+ workspace
## Troubleshooting
### Docker-based workspace is stuck in "Connecting..."
-Ensure you have an externally-reachable `CODER_ACCESS_URL` set. See [troubleshooting templates](../templates/index.md#troubleshooting-templates) for more steps.
+Ensure you have an externally-reachable `CODER_ACCESS_URL` set. See
+[troubleshooting templates](../templates/index.md#troubleshooting-templates) for
+more steps.
### Permission denied while trying to connect to the Docker daemon socket
-See Docker's official documentation to [Manage Docker as a non-root user](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user)
+See Docker's official documentation to
+[Manage Docker as a non-root user](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user)
## Next steps
diff --git a/docs/install/install.sh.md b/docs/install/install.sh.md
index 4acd34288e3d9..7b278676a9fae 100644
--- a/docs/install/install.sh.md
+++ b/docs/install/install.sh.md
@@ -1,4 +1,6 @@
-The easiest way to install Coder is to use our [install script](https://github.com/coder/coder/blob/main/install.sh) for Linux and macOS.
+The easiest way to install Coder is to use our
+[install script](https://github.com/coder/coder/blob/main/install.sh) for Linux
+and macOS.
To install, run:
@@ -12,15 +14,18 @@ You can preview what occurs during the install process:
curl -fsSL https://coder.com/install.sh | sh -s -- --dry-run
```
-You can modify the installation process by including flags. Run the help command for reference:
+You can modify the installation process by including flags. Run the help command
+for reference:
```bash
curl -fsSL https://coder.com/install.sh | sh -s -- --help
```
-After installing, use the in-terminal instructions to start the Coder server manually via `coder server` or as a system package.
+After installing, use the in-terminal instructions to start the Coder server
+manually via `coder server` or as a system package.
-By default, the Coder server runs on `http://127.0.0.1:3000` and uses a [public tunnel](../admin/configure.md#tunnel) for workspace connections.
+By default, the Coder server runs on `http://127.0.0.1:3000` and uses a
+[public tunnel](../admin/configure.md#tunnel) for workspace connections.
## Next steps
diff --git a/docs/install/kubernetes.md b/docs/install/kubernetes.md
index 191ef1aba0338..6e07f68fd57c6 100644
--- a/docs/install/kubernetes.md
+++ b/docs/install/kubernetes.md
@@ -1,20 +1,18 @@
## Requirements
-Before proceeding, please ensure that you have a Kubernetes cluster running K8s 1.19+ and have Helm 3.5+ installed.
+Before proceeding, please ensure that you have a Kubernetes cluster running K8s
+1.19+ and have Helm 3.5+ installed.
-You'll also want to install the [latest version of Coder](https://github.com/coder/coder/releases/latest) locally in order
-to log in and manage templates.
+You'll also want to install the
+[latest version of Coder](https://github.com/coder/coder/releases/latest)
+locally in order to log in and manage templates.
## Install Coder with Helm
-> **Warning**: Helm support is new and not yet complete. There may be changes
-> to the Helm chart between releases which require manual values updates. Please
-> file an issue if you run into any issues.
-
1. Create a namespace for Coder, such as `coder`:
```console
- $ kubectl create namespace coder
+ kubectl create namespace coder
```
1. Create a PostgreSQL deployment. Coder does not manage a database server for
@@ -25,12 +23,13 @@ to log in and manage templates.
[AWS](https://aws.amazon.com/rds/postgresql/),
[Azure](https://docs.microsoft.com/en-us/azure/postgresql/), or
[DigitalOcean](https://www.digitalocean.com/products/managed-databases-postgresql),
- you can use the managed PostgreSQL offerings they provide. Make sure that
- the PostgreSQL service is running and accessible from your cluster. It
- should be in the same network, same project, etc.
+ you can use the managed PostgreSQL offerings they provide. Make sure that the
+ PostgreSQL service is running and accessible from your cluster. It should be
+ in the same network, same project, etc.
You can install Postgres manually on your cluster using the
- [Bitnami PostgreSQL Helm chart](https://github.com/bitnami/charts/tree/master/bitnami/postgresql#readme). There are some
+ [Bitnami PostgreSQL Helm chart](https://github.com/bitnami/charts/tree/master/bitnami/postgresql#readme).
+ There are some
[helpful guides](https://phoenixnap.com/kb/postgresql-kubernetes) on the
internet that explain sensible configurations for this chart. Example:
@@ -53,15 +52,8 @@ to log in and manage templates.
> Ensure you set up periodic backups so you don't lose data.
- You can use
- [Postgres operator](https://github.com/zalando/postgres-operator) to
- manage PostgreSQL deployments on your Kubernetes cluster.
-
-1. Add the Coder Helm repo:
-
- ```console
- helm repo add coder-v2 https://helm.coder.com/v2
- ```
+ You can use [Postgres operator](https://github.com/zalando/postgres-operator)
+ to manage PostgreSQL deployments on your Kubernetes cluster.
1. Create a secret with the database URL:
@@ -72,6 +64,12 @@ to log in and manage templates.
--from-literal=url="postgres://coder:coder@coder-db-postgresql.coder.svc.cluster.local:5432/coder?sslmode=disable"
```
+1. Add the Coder Helm repo:
+
+ ```console
+ helm repo add coder-v2 https://helm.coder.com/v2
+ ```
+
1. Create a `values.yaml` with the configuration settings you'd like for your
deployment. For example:
@@ -109,38 +107,71 @@ to log in and manage templates.
> You can view our
> [Helm README](https://github.com/coder/coder/blob/main/helm#readme) for
> details on the values that are available, or you can view the
- > [values.yaml](https://github.com/coder/coder/blob/main/helm/values.yaml)
+ > [values.yaml](https://github.com/coder/coder/blob/main/helm/coder/values.yaml)
> file directly.
- If you are deploying Coder on AWS EKS and service is set to `LoadBalancer`, AWS will default to the Classic load balancer. The load balancer external IP will be stuck in a pending status unless sessionAffinity is set to None.
+1. Run the following command to install the chart in your cluster.
- ```yaml
- coder:
- service:
- type: LoadBalancer
- sessionAffinity: None
+ ```console
+ helm install coder coder-v2/coder \
+ --namespace coder \
+ --values values.yaml
```
+ You can watch Coder start up by running `kubectl get pods -n coder`. Once
+ Coder has started, the `coder-*` pods should enter the `Running` state.
+
+1. Log in to Coder
+
+ Use `kubectl get svc -n coder` to get the IP address of the LoadBalancer.
+ Visit this in the browser to set up your first account.
+
+ If you do not have a domain, you should set `CODER_ACCESS_URL` to this URL in
+ the Helm chart and upgrade Coder (see below). This allows workspaces to
+ connect to the proper Coder URL.
+
+## Upgrading Coder via Helm
+
+To upgrade Coder in the future or change values, you can run the following
+command:
+
+```console
+helm repo update
+helm upgrade coder coder-v2/coder \
+ --namespace coder \
+ -f values.yaml
+```
+
## Load balancing considerations
### AWS
-AWS however recommends a Network load balancer in lieu of the Classic load balancer. Use the following `values.yaml` settings to request a Network load balancer:
+If you are deploying Coder on AWS EKS and service is set to `LoadBalancer`, AWS
+will default to the Classic load balancer. The load balancer external IP will be
+stuck in a pending status unless sessionAffinity is set to None.
+
+```yaml
+coder:
+ service:
+ type: LoadBalancer
+ sessionAffinity: None
+```
+
+AWS recommends a Network load balancer in lieu of the Classic load balancer. Use
+the following `values.yaml` settings to request a Network load balancer:
```yaml
coder:
- service:
- externalTrafficPolicy: Local
- sessionAffinity: None
- annotations: {
- service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
- }
+ service:
+ externalTrafficPolicy: Local
+ sessionAffinity: None
+ annotations: { service.beta.kubernetes.io/aws-load-balancer-type: "nlb" }
```
By default, Coder will set the `externalTrafficPolicy` to `Cluster` which will
-mask client IP addresses in the Audit log. To preserve the source IP, you can either
-set this value to `Local`, or pass through the client IP via the X-Forwarded-For
-header. To configure the latter, set the following environment
+mask client IP addresses in the Audit log. To preserve the source IP, you can
+either set this value to `Local`, or pass through the client IP via the
+X-Forwarded-For header. To configure the latter, set the following environment
variables:
```yaml
@@ -152,39 +183,23 @@ coder:
value: 10.0.0.1/8 # this will be the CIDR range of your Load Balancer IP address
```
-1. Run the following command to install the chart in your cluster.
-
- ```console
- helm install coder coder-v2/coder \
- --namespace coder \
- --values values.yaml
- ```
-
- You can watch Coder start up by running `kubectl get pods -n coder`. Once Coder has
- started, the `coder-*` pods should enter the `Running` state.
-
-1. Log in to Coder
-
- Use `kubectl get svc -n coder` to get the IP address of the
- LoadBalancer. Visit this in the browser to set up your first account.
-
- If you do not have a domain, you should set `CODER_ACCESS_URL`
- to this URL in the Helm chart and upgrade Coder (see below).
- This allows workspaces to connect to the proper Coder URL.
-
### Azure
-In certain enterprise environments, the [Azure Application Gateway](https://learn.microsoft.com/en-us/azure/application-gateway/ingress-controller-overview) was needed. The Application Gateway supports:
+In certain enterprise environments, the
+[Azure Application Gateway](https://learn.microsoft.com/en-us/azure/application-gateway/ingress-controller-overview)
+was needed. The Application Gateway supports:
- Websocket traffic (required for workspace connections)
- TLS termination
## PostgreSQL Certificates
-Your organization may require connecting to the database instance over SSL. To supply
-Coder with the appropriate certificates, and have it connect over SSL, follow the steps below:
+Your organization may require connecting to the database instance over SSL. To
+supply Coder with the appropriate certificates, and have it connect over SSL,
+follow the steps below:
-1. Create the certificate as a secret in your Kubernetes cluster, if not already present:
+1. Create the certificate as a secret in your Kubernetes cluster, if not already
+ present:
```console
$ kubectl create secret tls postgres-certs -n coder --key="postgres.key" --cert="postgres.crt"
@@ -210,32 +225,24 @@ coder:
postgres://:@databasehost:/?sslmode=require&sslcert=$HOME/.postgresql/postgres.crt&sslkey=$HOME/.postgresql/postgres.key"
```
-> More information on connecting to PostgreSQL databases using certificates can be found [here](https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-CLIENTCERT).
-
-## Upgrading Coder via Helm
-
-To upgrade Coder in the future or change values,
-you can run the following command:
-
-```console
-helm repo update
-helm upgrade coder coder-v2/coder \
- --namespace coder \
- -f values.yaml
-```
+> More information on connecting to PostgreSQL databases using certificates can
+> be found
+> [here](https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-CLIENTCERT).
## Troubleshooting
-You can view Coder's logs by getting the pod name from `kubectl get pods` and then running `kubectl logs `. You can also
-view these logs in your
+You can view Coder's logs by getting the pod name from `kubectl get pods` and
+then running `kubectl logs `. You can also view these logs in your
Cloud's log management system if you are using managed Kubernetes.
### Kubernetes-based workspace is stuck in "Connecting..."
-Ensure you have an externally-reachable `CODER_ACCESS_URL` set in your helm chart. If you do not have a domain set up,
-this should be the IP address of Coder's LoadBalancer (`kubectl get svc -n coder`).
+Ensure you have an externally-reachable `CODER_ACCESS_URL` set in your helm
+chart. If you do not have a domain set up, this should be the IP address of
+Coder's LoadBalancer (`kubectl get svc -n coder`).
-See [troubleshooting templates](../templates/index.md#troubleshooting-templates) for more steps.
+See [troubleshooting templates](../templates/index.md#troubleshooting-templates)
+for more steps.
## Next steps
diff --git a/docs/install/offline.md b/docs/install/offline.md
index 1081843c24643..e7798670c18d5 100644
--- a/docs/install/offline.md
+++ b/docs/install/offline.md
@@ -1,8 +1,10 @@
# Offline Deployments
-All Coder features are supported in offline / behind firewalls / in air-gapped environments. However, some changes to your configuration are necessary.
+All Coder features are supported in offline / behind firewalls / in air-gapped
+environments. However, some changes to your configuration are necessary.
-> This is a general comparison. Keep reading for a full tutorial running Coder offline with Kubernetes or Docker.
+> This is a general comparison. Keep reading for a full tutorial running Coder
+> offline with Kubernetes or Docker.
| | Public deployments | Offline deployments |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -16,16 +18,23 @@ All Coder features are supported in offline / behind firewalls / in air-gapped e
## Offline container images
-The following instructions walk you through how to build a custom Coder server image for Docker or Kubernetes
+The following instructions walk you through how to build a custom Coder server
+image for Docker or Kubernetes
-First, build and push a container image extending our official image with the following:
+First, build and push a container image extending our official image with the
+following:
-- CLI config (.tfrc) for Terraform referring to [external mirror](https://www.terraform.io/cli/config/config-file#explicit-installation-method-configuration)
+- CLI config (.tfrc) for Terraform referring to
+ [external mirror](https://www.terraform.io/cli/config/config-file#explicit-installation-method-configuration)
- [Terraform Providers](https://registry.terraform.io) for templates
- - These could also be specified via a volume mount (Docker) or [network mirror](https://www.terraform.io/internals/provider-network-mirror-protocol). See below for details.
+ - These could also be specified via a volume mount (Docker) or
+ [network mirror](https://www.terraform.io/internals/provider-network-mirror-protocol).
+ See below for details.
-> Note: Coder includes the latest [supported version](https://github.com/coder/coder/blob/main/provisioner/terraform/install.go#L23-L24) of Terraform in the official Docker images.
-> If you need to bundle a different version of terraform, you can do so by customizing the image.
+> Note: Coder includes the latest
+> [supported version](https://github.com/coder/coder/blob/main/provisioner/terraform/install.go#L23-L24)
+> of Terraform in the official Docker images. If you need to bundle a different
+> version of terraform, you can do so by customizing the image.
Here's an example Dockerfile:
@@ -104,7 +113,9 @@ ENV TF_CLI_CONFIG_FILE=/opt/terraform/config.tfrc
```
> If you are bundling Terraform providers into your Coder image, be sure the
-> provider version matches any templates or [example templates](https://github.com/coder/coder/tree/main/examples/templates) you intend to use.
+> provider version matches any templates or
+> [example templates](https://github.com/coder/coder/tree/main/examples/templates)
+> you intend to use.
```hcl
# filesystem-mirror-example.tfrc
@@ -126,7 +137,10 @@ provider_installation {
## Run offline via Docker
-Follow our [docker-compose](./docker.md#run-coder-with-docker-compose) documentation and modify the docker-compose file to specify your custom Coder image. Additionally, you can add a volume mount to add providers to the filesystem mirror without re-building the image.
+Follow our [docker-compose](./docker.md#run-coder-with-docker-compose)
+documentation and modify the docker-compose file to specify your custom Coder
+image. Additionally, you can add a volume mount to add providers to the
+filesystem mirror without re-building the image.
First, make a create an empty plugins directory:
@@ -158,11 +172,17 @@ services:
# ...
```
-> The [terraform providers mirror](https://www.terraform.io/cli/commands/providers/mirror) command can be used to download the required plugins for a Coder template. This can be uploaded into the `plugins` directory on your offline server.
+> The
+> [terraform providers mirror](https://www.terraform.io/cli/commands/providers/mirror)
+> command can be used to download the required plugins for a Coder template.
+> This can be uploaded into the `plugins` directory on your offline server.
## Run offline via Kubernetes
-We publish the Helm chart for download on [GitHub Releases](https://github.com/coder/coder/releases/latest). Follow our [Kubernetes](./kubernetes.md) documentation and modify the Helm values to specify your custom Coder image.
+We publish the Helm chart for download on
+[GitHub Releases](https://github.com/coder/coder/releases/latest). Follow our
+[Kubernetes](./kubernetes.md) documentation and modify the Helm values to
+specify your custom Coder image.
```yaml
# values.yaml
@@ -188,12 +208,31 @@ coder:
## Offline docs
-Coder also provides offline documentation in case you want to host it on your own server. The docs are exported as static files that you can host on any web server, as demonstrated in the example below:
+Coder also provides offline documentation in case you want to host it on your
+own server. The docs are exported as static files that you can host on any web
+server, as demonstrated in the example below:
-1. Go to the release page. In this case, we want to use the [latest version](https://github.com/coder/coder/releases/latest).
-2. Download the documentation files from the "Assets" section. It is named as `coder_docs_.tgz`.
+1. Go to the release page. In this case, we want to use the
+ [latest version](https://github.com/coder/coder/releases/latest).
+2. Download the documentation files from the "Assets" section. It is named as
+ `coder_docs_.tgz`.
3. Extract the file and move its contents to your server folder.
-4. If you are using NodeJS, you can execute the following command: `cd docs && npx http-server .`
-5. Set the [CODER_DOCS_URL](../cli/server#--docs-url) environment variable to use the URL of your hosted docs. This way, the Coder UI will reference the documentation from your specified URL.
+4. If you are using NodeJS, you can execute the following command:
+ `cd docs && npx http-server .`
+5. Set the [CODER_DOCS_URL](../cli/server.md#--docs-url) environment variable to
+ use the URL of your hosted docs. This way, the Coder UI will reference the
+ documentation from your specified URL.
-With these steps, you'll have the Coder documentation hosted on your server and accessible for your team to use.
+With these steps, you'll have the Coder documentation hosted on your server and
+accessible for your team to use.
+
+## Firewall exceptions
+
+In restricted internet networks, Coder may require connection to internet.
+Ensure that the following web addresses are accessible from the machine where
+Coder is installed.
+
+- code-server.dev (install via AUR)
+- open-vsx.org (optional if someone would use code-server)
+- registry.terraform.io (to create and push template)
+- v2-licensor.coder.com (developing Coder in Coder)
diff --git a/docs/install/openshift.md b/docs/install/openshift.md
index dd0ab0a569601..7d7440978da24 100644
--- a/docs/install/openshift.md
+++ b/docs/install/openshift.md
@@ -2,10 +2,11 @@
Before proceeding, please ensure that you have an OpenShift cluster running K8s
1.19+ (OpenShift 4.7+) and have Helm 3.5+ installed. In addition, you'll need to
-install the OpenShift CLI (`oc`) to authenticate to your cluster and create OpenShift
-resources.
+install the OpenShift CLI (`oc`) to authenticate to your cluster and create
+OpenShift resources.
-You'll also want to install the [latest version of Coder](https://github.com/coder/coder/releases/latest)
+You'll also want to install the
+[latest version of Coder](https://github.com/coder/coder/releases/latest)
locally in order to log in and manage templates.
## Install Coder with OpenShift
@@ -26,11 +27,12 @@ oc new-project coder
### 2. Configure SecurityContext values
-Depending upon your configured Security Context Constraints (SCC), you'll need to modify
-some or all of the following `securityContext` values from the default values:
+Depending upon your configured Security Context Constraints (SCC), you'll need
+to modify some or all of the following `securityContext` values from the default
+values:
-The below values are modified from Coder defaults and allow the Coder deployment to run
-under the SCC `restricted-v2`.
+The below values are modified from Coder defaults and allow the Coder deployment
+to run under the SCC `restricted-v2`.
> Note: `readOnlyRootFilesystem: true` is not technically required under
> `restricted-v2`, but is often mandated in OpenShift environments.
@@ -45,8 +47,8 @@ coder:
seccompProfile: RuntimeDefault # Unchanged from default
```
-- For `runAsUser` / `runAsGroup`, you can retrieve the correct values for project UID and project GID with the following
- command:
+- For `runAsUser` / `runAsGroup`, you can retrieve the correct values for
+ project UID and project GID with the following command:
```console
oc get project coder -o json | jq -r '.metadata.annotations'
@@ -56,12 +58,12 @@ coder:
}
```
- Alternatively, you can set these values to `null` to allow OpenShift to automatically select
- the correct value for the project.
+ Alternatively, you can set these values to `null` to allow OpenShift to
+ automatically select the correct value for the project.
- For `readOnlyRootFilesystem`, consult the SCC under which Coder needs to run.
- In the below example, the `restricted-v2` SCC does not require a read-only root filesystem,
- while `restricted-custom` does:
+ In the below example, the `restricted-v2` SCC does not require a read-only
+ root filesystem, while `restricted-custom` does:
```console
oc get scc -o wide
@@ -70,34 +72,34 @@ coder:
restricted-v2 false ["NET_BIND_SERVICE"] MustRunAs MustRunAsRange MustRunAs RunAsAny false ["configMap","downwardAPI","emptyDir","ephemeral","persistentVolumeClaim","projected","secret"]
```
- If you are unsure, we recommend setting `readOnlyRootFilesystem` to `true` in an OpenShift
- environment.
+ If you are unsure, we recommend setting `readOnlyRootFilesystem` to `true` in
+ an OpenShift environment.
-- For `seccompProfile`: in some environments, you may need to set this to `null` to allow OpenShift
- to pick its preferred value.
+- For `seccompProfile`: in some environments, you may need to set this to `null`
+ to allow OpenShift to pick its preferred value.
### 3. Configure the Coder service, connection URLs, and cache values
-To establish a connection to PostgreSQL, set the `CODER_PG_CONNECTION_URL` value.
-[See our Helm documentation](./kubernetes.md) on configuring the PostgreSQL connection
-URL as a secret. Additionally, if accessing Coder over a hostname, set the `CODER_ACCESS_URL`
-value.
+To establish a connection to PostgreSQL, set the `CODER_PG_CONNECTION_URL`
+value. [See our Helm documentation](./kubernetes.md) on configuring the
+PostgreSQL connection URL as a secret. Additionally, if accessing Coder over a
+hostname, set the `CODER_ACCESS_URL` value.
By default, Coder creates the cache directory in `/home/coder/.cache`. Given the
-OpenShift-provided UID and `readOnlyRootFS` security context constraint, the Coder
-container does not have permission to write to this directory.
+OpenShift-provided UID and `readOnlyRootFS` security context constraint, the
+Coder container does not have permission to write to this directory.
-To fix this, you can mount a temporary volume in the pod and set
-the `CODER_CACHE_DIRECTORY` environment variable to that location.
-In the below example, we mount this under `/tmp` and set the cache location to
-`/tmp/coder`. This enables Coder to run with `readOnlyRootFilesystem: true`.
+To fix this, you can mount a temporary volume in the pod and set the
+`CODER_CACHE_DIRECTORY` environment variable to that location. In the below
+example, we mount this under `/tmp` and set the cache location to `/tmp/coder`.
+This enables Coder to run with `readOnlyRootFilesystem: true`.
> Note: Depending on the number of templates and provisioners you use, you may
-> need to increase the size of the volume, as the `coder` pod will be automatically
-> restarted when this volume fills up.
+> need to increase the size of the volume, as the `coder` pod will be
+> automatically restarted when this volume fills up.
-Additionally, create the Coder service as a `ClusterIP`. In the next step,
-you will create an OpenShift route that points to the service HTTP target port.
+Additionally, create the Coder service as a `ClusterIP`. In the next step, you
+will create an OpenShift route that points to the service HTTP target port.
```yaml
coder:
@@ -128,8 +130,8 @@ coder:
readOnly: false
```
-> Note: OpenShift provides a Developer Catalog offering you can use to
-> install PostgreSQL into your cluster.
+> Note: OpenShift provides a Developer Catalog offering you can use to install
+> PostgreSQL into your cluster.
### 4. Create the OpenShift route
@@ -165,8 +167,8 @@ oc apply -f route.yaml
### 5. Install Coder
-You can now install Coder using the values you've set from the above steps. To do
-so, run the series of `helm` commands below:
+You can now install Coder using the values you've set from the above steps. To
+do so, run the series of `helm` commands below:
```console
helm repo add coder-v2 https://helm.coder.com/v2
@@ -176,8 +178,8 @@ helm install coder coder-v2/coder \
--values values.yaml
```
-> Note: If the Helm installation fails with a Kubernetes RBAC error, check the permissions
-> of your OpenShift user using the `oc auth can-i` command.
+> Note: If the Helm installation fails with a Kubernetes RBAC error, check the
+> permissions of your OpenShift user using the `oc auth can-i` command.
>
> The below permissions are the minimum required:
>
@@ -212,9 +214,9 @@ helm install coder coder-v2/coder \
### 6. Create an OpenShift-compatible image
-While the deployment is spinning up, we will need to create some images that
-are compatible with OpenShift. These images can then be run without modifying
-the Security Context Constraints (SCCs) in OpenShift.
+While the deployment is spinning up, we will need to create some images that are
+compatible with OpenShift. These images can then be run without modifying the
+Security Context Constraints (SCCs) in OpenShift.
1. Determine the UID range for the project:
@@ -230,15 +232,18 @@ the Security Context Constraints (SCCs) in OpenShift.
}
```
- Note the `uid-range` and `supplemental-groups`. In this case, the project `coder`
- has been allocated 10,000 UIDs and GIDs, both starting at `1000680000`.
+ Note the `uid-range` and `supplemental-groups`. In this case, the project
+ `coder` has been allocated 10,000 UIDs and GIDs, both starting at
+ `1000680000`.
In this example, we will pick both UID and GID `1000680000`.
1. Create a `BuildConfig` referencing the source image you want to customize.
- This will automatically kick off a `Build` that will remain pending until step 3.
+ This will automatically kick off a `Build` that will remain pending until
+ step 3.
- > For more information, please consult the [OpenShift Documentation](https://docs.openshift.com/container-platform/4.12/cicd/builds/understanding-buildconfigs.html).
+ > For more information, please consult the
+ > [OpenShift Documentation](https://docs.openshift.com/container-platform/4.12/cicd/builds/understanding-buildconfigs.html).
```console
oc create -f - < Set `CODER_ACCESS_URL` to the external URL that users and workspaces will use to
- > connect to Coder. This is not required if you are using the tunnel. Learn more
- > about Coder's [configuration options](../admin/configure.md).
+ > Set `CODER_ACCESS_URL` to the external URL that users and workspaces will
+ > use to connect to Coder. This is not required if you are using the tunnel.
+ > Learn more about Coder's [configuration options](../admin/configure.md).
1. Visit the Coder URL in the logs to set up your first account, or use the CLI:
diff --git a/docs/install/uninstall.md b/docs/install/uninstall.md
index ae99930da8563..c6c5056f1e557 100644
--- a/docs/install/uninstall.md
+++ b/docs/install/uninstall.md
@@ -30,7 +30,8 @@ Alpine:
sudo apk del coder
```
-If you installed Coder manually or used the install script on an unsupported operating system, you can remove the binary directly:
+If you installed Coder manually or used the install script on an unsupported
+operating system, you can remove the binary directly:
```console
sudo rm /usr/local/bin/coder
@@ -45,9 +46,8 @@ sudo rm /etc/coder.d/coder.env
## Coder settings and the optional built-in PostgreSQL database
> There is a `postgres` directory within the `coderv2` directory that has the
-> database engine and database. If you want to reuse the database, consider
-> not performing the following step or copying the directory to another
-> location.
+> database engine and database. If you want to reuse the database, consider not
+> performing the following step or copying the directory to another location.
### macOS
diff --git a/docs/install/windows.md b/docs/install/windows.md
index 86de80da7755a..d4eb53e6cf2d4 100644
--- a/docs/install/windows.md
+++ b/docs/install/windows.md
@@ -1,8 +1,12 @@
# Windows
-Use the Windows installer to download the CLI and add Coder to `PATH`. Alternatively, you can install Coder on Windows via a [standalone binary](./binary.md).
+Use the Windows installer to download the CLI and add Coder to `PATH`.
+Alternatively, you can install Coder on Windows via a
+[standalone binary](./binary.md).
-1. Download the Windows installer from [GitHub releases](https://github.com/coder/coder/releases/latest) or from `winget`
+1. Download the Windows installer from
+ [GitHub releases](https://github.com/coder/coder/releases/latest) or from
+ `winget`
```powershell
winget install Coder.Coder
@@ -22,9 +26,9 @@ Use the Windows installer to download the CLI and add Coder to `PATH`. Alternati
coder server --postgres-url --access-url
```
- > Set `CODER_ACCESS_URL` to the external URL that users and workspaces will use to
- > connect to Coder. This is not required if you are using the tunnel. Learn more
- > about Coder's [configuration options](../admin/configure.md).
+ > Set `CODER_ACCESS_URL` to the external URL that users and workspaces will
+ > use to connect to Coder. This is not required if you are using the tunnel.
+ > Learn more about Coder's [configuration options](../admin/configure.md).
4. Visit the Coder URL in the logs to set up your first account, or use the CLI.
diff --git a/docs/manifest.json b/docs/manifest.json
index d6c59b477f053..8c18bb26ad9f5 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -196,6 +196,12 @@
"title": "Terraform Modules",
"description": "Reuse code across Coder templates",
"path": "./templates/modules.md"
+ },
+ {
+ "title": "Process Logging",
+ "description": "Audit commands in workspaces with exectrace",
+ "path": "./templates/process-logging.md",
+ "state": "enterprise"
}
]
},
@@ -788,11 +794,6 @@
"description": "List all the templates available for the organization",
"path": "cli/templates_list.md"
},
- {
- "title": "templates plan",
- "description": "Plan a template push from the current directory",
- "path": "cli/templates_plan.md"
- },
{
"title": "templates pull",
"description": "Download the latest version of a template to a path.",
diff --git a/docs/networking/index.md b/docs/networking/index.md
index c83d8289eb358..f5d94b10b70e6 100644
--- a/docs/networking/index.md
+++ b/docs/networking/index.md
@@ -1,29 +1,30 @@
# Networking
-Coder's network topology has three types of nodes:
-workspaces, coder servers, and users.
+Coder's network topology has three types of nodes: workspaces, coder servers,
+and users.
The coder server must have an inbound address reachable by users and workspaces,
but otherwise, all topologies _just work_ with Coder.
When possible, we establish direct connections between users and workspaces.
Direct connections are as fast as connecting to the workspace outside of Coder.
-When NAT traversal fails, connections are relayed through the coder server.
-All user <-> workspace connections are end-to-end encrypted.
+When NAT traversal fails, connections are relayed through the coder server. All
+user <-> workspace connections are end-to-end encrypted.
[Tailscale's open source](https://tailscale.com) backs our networking logic.
## coder server
-Workspaces connect to the coder server via the server's external address,
-set via [`ACCESS_URL`](../admin/configure.md#access-url). There must not be a
-NAT between workspaces and coder server.
+Workspaces connect to the coder server via the server's external address, set
+via [`ACCESS_URL`](../admin/configure.md#access-url). There must not be a NAT
+between workspaces and coder server.
Users connect to the coder server's dashboard and API through its `ACCESS_URL`
as well. There must not be a NAT between users and the coder server.
Template admins can overwrite the site-wide access URL at the template level by
-leveraging the `url` argument when [defining the Coder provider](https://registry.terraform.io/providers/coder/coder/latest/docs#url):
+leveraging the `url` argument when
+[defining the Coder provider](https://registry.terraform.io/providers/coder/coder/latest/docs#url):
```terraform
provider "coder" {
@@ -31,16 +32,17 @@ provider "coder" {
}
```
-This is useful when debugging connectivity issues between the workspace agent and
-the Coder server.
+This is useful when debugging connectivity issues between the workspace agent
+and the Coder server.
## Web Apps
The coder servers relays dashboard-initiated connections between the user and
-the workspace. Web terminal <-> workspace connections are an exception and may be direct.
+the workspace. Web terminal <-> workspace connections are an exception and may
+be direct.
-In general, [port forwarded](./port-forwarding.md) web apps are
-faster than dashboard-accessed web apps.
+In general, [port forwarded](./port-forwarding.md) web apps are faster than
+dashboard-accessed web apps.
## 🌎 Geo-distribution
@@ -50,16 +52,20 @@ Direct connections are a straight line between the user and workspace, so there
is no special geo-distribution configuration. To speed up direct connections,
move the user and workspace closer together.
-If a direct connection is not available (e.g. client or server is behind NAT), Coder
-will use a relayed connection. By default, [Coder uses Google's public STUN server](../cli/server.md#--derp-server-stun-addresses), but
-this can be disabled or changed for [offline deployments](../install/offline.md).
+If a direct connection is not available (e.g. client or server is behind NAT),
+Coder will use a relayed connection. By default,
+[Coder uses Google's public STUN server](../cli/server.md#--derp-server-stun-addresses),
+but this can be disabled or changed for
+[offline deployments](../install/offline.md).
### Relayed connections
-By default, your Coder server also runs a built-in DERP relay which can be used for both public and [offline deployments](../install/offline.md).
+By default, your Coder server also runs a built-in DERP relay which can be used
+for both public and [offline deployments](../install/offline.md).
However, Tailscale has graciously allowed us to use
-[their global DERP relays](https://tailscale.com/kb/1118/custom-derp-servers/#what-are-derp-servers). You can launch `coder server` with Tailscale's DERPs like so:
+[their global DERP relays](https://tailscale.com/kb/1118/custom-derp-servers/#what-are-derp-servers).
+You can launch `coder server` with Tailscale's DERPs like so:
```bash
$ coder server --derp-config-url https://controlplane.tailscale.com/derpmap/default
@@ -67,7 +73,9 @@ $ coder server --derp-config-url https://controlplane.tailscale.com/derpmap/defa
#### Custom Relays
-If you want lower latency than what Tailscale offers or want additional DERP relays for offline deployments, you may run custom DERP servers. Refer to [Tailscale's documentation](https://tailscale.com/kb/1118/custom-derp-servers/#why-run-your-own-derp-server)
+If you want lower latency than what Tailscale offers or want additional DERP
+relays for offline deployments, you may run custom DERP servers. Refer to
+[Tailscale's documentation](https://tailscale.com/kb/1118/custom-derp-servers/#why-run-your-own-derp-server)
to learn how to set them up.
After you have custom DERP servers, you can launch Coder with them like so:
@@ -109,7 +117,8 @@ Some Coder deployments require that all access is through the browser to comply
with security policies. In these cases, pass the `--browser-only` flag to
`coder server` or set `CODER_BROWSER_ONLY=true`.
-With browser-only connections, developers can only connect to their workspaces via the web terminal and [web IDEs](../ides/web-ides.md).
+With browser-only connections, developers can only connect to their workspaces
+via the web terminal and [web IDEs](../ides/web-ides.md).
## Troubleshooting
@@ -128,8 +137,8 @@ pong from my-workspace proxied via DERP(Denver) in 90ms
2023-06-21 17:50:22.504 [debu] wgengine: wg: [v2] Device closed
```
-The `coder speedtest ` command measures user <-> workspace throughput.
-E.g.:
+The `coder speedtest ` command measures user <-> workspace
+throughput. E.g.:
```
$ coder speedtest dev
diff --git a/docs/networking/port-forwarding.md b/docs/networking/port-forwarding.md
index 9e8f075d5e06c..eb233050361d6 100644
--- a/docs/networking/port-forwarding.md
+++ b/docs/networking/port-forwarding.md
@@ -1,8 +1,8 @@
# Port Forwarding
Port forwarding lets developers securely access processes on their Coder
-workspace from a local machine. A common use case is testing web
-applications in a browser.
+workspace from a local machine. A common use case is testing web applications in
+a browser.
There are three ways to forward ports in Coder:
@@ -14,9 +14,9 @@ The `coder port-forward` command is generally more performant.
## The `coder port-forward` command
-This command can be used to forward TCP or UDP ports from the remote
-workspace so they can be accessed locally. Both the TCP and UDP command
-line flags (`--tcp` and `--udp`) can be given once or multiple times.
+This command can be used to forward TCP or UDP ports from the remote workspace
+so they can be accessed locally. Both the TCP and UDP command line flags
+(`--tcp` and `--udp`) can be given once or multiple times.
The supported syntax variations for the `--tcp` and `--udp` flag are:
@@ -33,8 +33,8 @@ Forward the remote TCP port `8080` to local port `8000`:
coder port-forward myworkspace --tcp 8000:8080
```
-Forward the remote TCP port `3000` and all ports from `9990` to `9999`
-to their respective local ports.
+Forward the remote TCP port `3000` and all ports from `9990` to `9999` to their
+respective local ports.
```console
coder port-forward myworkspace --tcp 3000,9990-9999
@@ -46,20 +46,27 @@ For more examples, see `coder port-forward --help`.
> To enable port forwarding via the dashboard, Coder must be configured with a
> [wildcard access URL](../admin/configure.md#wildcard-access-url). If an access
-> URL is not specified, Coder will create [a publicly accessible URL](../admin/configure.md#tunnel)
-> to reverse proxy the deployment, and port forwarding will work. There is a
-> known limitation where if the port forwarding URL length is greater than 63
-> characters, port forwarding will not work.
+> URL is not specified, Coder will create
+> [a publicly accessible URL](../admin/configure.md#tunnel) to reverse proxy the
+> deployment, and port forwarding will work. There is a known limitation where
+> if the port forwarding URL length is greater than 63 characters, port
+> forwarding will not work.
### From an arbitrary port
-One way to port forward in the dashboard is to use the "Port forward" button to specify an arbitrary port. Coder will also detect if processes are running, and will list them below the port picklist to click an open the running processes in the browser.
+One way to port forward in the dashboard is to use the "Port forward" button to
+specify an arbitrary port. Coder will also detect if processes are running, and
+will list them below the port picklist to click an open the running processes in
+the browser.

### From an coder_app resource
-Another way to port forward is to configure a `coder_app` resource in the workspace's template. This approach shows a visual application icon in the dashboard. See the following `coder_app` example for a Node React app and note the `subdomain` and `share` settings:
+Another way to port forward is to configure a `coder_app` resource in the
+workspace's template. This approach shows a visual application icon in the
+dashboard. See the following `coder_app` example for a Node React app and note
+the `subdomain` and `share` settings:
```hcl
# node app
@@ -80,7 +87,9 @@ resource "coder_app" "node-react-app" {
}
```
-Valid `share` values include `owner` - private to the user, `authenticated` - accessible by any user authenticated to the Coder deployment, and `public` - accessible by users outside of the Coder deployment.
+Valid `share` values include `owner` - private to the user, `authenticated` -
+accessible by any user authenticated to the Coder deployment, and `public` -
+accessible by users outside of the Coder deployment.

@@ -100,7 +109,12 @@ must include credentials (set `credentials: "include"` if using `fetch`) or the
requests cannot be authenticated and you will see an error resembling the
following:
-> Access to fetch at 'https://coder.example.com/api/v2/applications/auth-redirect' from origin 'https://8000--dev--user--apps.coder.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
+> Access to fetch at
+> 'https://coder.example.com/api/v2/applications/auth-redirect' from origin
+> 'https://8000--dev--user--apps.coder.example.com' has been blocked by CORS
+> policy: No 'Access-Control-Allow-Origin' header is present on the requested
+> resource. If an opaque response serves your needs, set the request's mode to
+> 'no-cors' to fetch the resource with CORS disabled.
#### Headers
@@ -197,11 +211,12 @@ configurable by either admins or users.
## SSH
-First, [configure SSH](../ides.md#ssh-configuration) on your
-local machine. Then, use `ssh` to forward like so:
+First, [configure SSH](../ides.md#ssh-configuration) on your local machine.
+Then, use `ssh` to forward like so:
```console
ssh -L 8080:localhost:8000 coder.myworkspace
```
-You can read more on SSH port forwarding [here](https://www.ssh.com/academy/ssh/tunneling/example).
+You can read more on SSH port forwarding
+[here](https://www.ssh.com/academy/ssh/tunneling/example).
diff --git a/docs/platforms/aws.md b/docs/platforms/aws.md
index c74f65b7b2a0d..7ec5bdeca4531 100644
--- a/docs/platforms/aws.md
+++ b/docs/platforms/aws.md
@@ -1,6 +1,10 @@
# Amazon Web Services
-This guide is designed to get you up and running with a Coder proof-of-concept VM on AWS EC2 using a [Coder-provided AMI](https://github.com/coder/packages). If you are familiar with EC2 however, you can use our [install script](../install/install.sh.md) to run Coder on any popular Linux distribution.
+This guide is designed to get you up and running with a Coder proof-of-concept
+VM on AWS EC2 using a [Coder-provided AMI](https://github.com/coder/packages).
+If you are familiar with EC2 however, you can use our
+[install script](../install/install.sh.md) to run Coder on any popular Linux
+distribution.
## Requirements
@@ -8,34 +12,48 @@ This guide assumes your AWS account has `AmazonEC2FullAccess` permissions.
## Launch a Coder instance from the from AWS Marketplace
-We publish an Ubuntu 22.04 AMI with Coder and Docker pre-installed. Search for `Coder` in the EC2 "Launch an Instance" screen or [launch directly from the marketplace](https://aws.amazon.com/marketplace/pp/prodview-5gxjyur2vc7rg).
+We publish an Ubuntu 22.04 AMI with Coder and Docker pre-installed. Search for
+`Coder` in the EC2 "Launch an Instance" screen or
+[launch directly from the marketplace](https://aws.amazon.com/marketplace/pp/prodview-5gxjyur2vc7rg).

-Be sure to keep the default firewall (SecurityGroup) options checked so you can connect over HTTP, HTTPS, and SSH.
+Be sure to keep the default firewall (SecurityGroup) options checked so you can
+connect over HTTP, HTTPS, and SSH.

-We recommend keeping the default instance type (`t2.xlarge`, 4 cores and 16 GB memory) if you plan on provisioning Docker containers as workspaces on this EC2 instance. Keep in mind this platforms is intended for proof-of-concept deployments and you should adjust your infrastructure when preparing for production use. See: [Scaling Coder](../admin/scale.md)
+We recommend keeping the default instance type (`t2.xlarge`, 4 cores and 16 GB
+memory) if you plan on provisioning Docker containers as workspaces on this EC2
+instance. Keep in mind this platforms is intended for proof-of-concept
+deployments and you should adjust your infrastructure when preparing for
+production use. See: [Scaling Coder](../admin/scale.md)
-Be sure to add a keypair so that you can connect over SSH to further [configure Coder](../admin/configure.md).
+Be sure to add a keypair so that you can connect over SSH to further
+[configure Coder](../admin/configure.md).
-After launching the instance, wait 30 seconds and navigate to the public IPv4 address. You should be redirected to a public tunnel URL.
+After launching the instance, wait 30 seconds and navigate to the public IPv4
+address. You should be redirected to a public tunnel URL.
Your browser does not support the video tag.
-That's all! Use the UI to create your first user, template, and workspace. We recommend starting with a Docker template since the instance has Docker pre-installed.
+That's all! Use the UI to create your first user, template, and workspace. We
+recommend starting with a Docker template since the instance has Docker
+pre-installed.

## Configuring Coder server
-Coder is primarily configured by server-side flags and environment variables. Given you created or added key-pairs when launching the instance, you can [configure your Coder deployment](../admin/configure.md) by logging in via SSH or using the console:
+Coder is primarily configured by server-side flags and environment variables.
+Given you created or added key-pairs when launching the instance, you can
+[configure your Coder deployment](../admin/configure.md) by logging in via SSH
+or using the console:
-```sh
+```shell
ssh ubuntu@
sudo vim /etc/coder.d/coder.env # edit config
sudo service coder restart # restart Coder
@@ -43,15 +61,22 @@ sudo service coder restart # restart Coder
## Give developers EC2 workspaces (optional)
-Instead of running containers on the Coder instance, you can offer developers full EC2 instances with the [aws-linux](https://github.com/coder/coder/tree/main/examples/templates/aws-linux) template.
+Instead of running containers on the Coder instance, you can offer developers
+full EC2 instances with the
+[aws-linux](https://github.com/coder/coder/tree/main/examples/templates/aws-linux)
+template.
-Before you add the AWS template from the dashboard or CLI, you'll need to modify the instance IAM role.
+Before you add the AWS template from the dashboard or CLI, you'll need to modify
+the instance IAM role.

-You must create or select a role that has `EC2FullAccess` permissions or a limited [Coder-specific permissions policy](https://github.com/coder/coder/tree/main/examples/templates/aws-linux#required-permissions--policy).
+You must create or select a role that has `EC2FullAccess` permissions or a
+limited
+[Coder-specific permissions policy](https://github.com/coder/coder/tree/main/examples/templates/aws-linux#required-permissions--policy).
-From there, you can import the AWS starter template in the dashboard and begin creating VM-based workspaces.
+From there, you can import the AWS starter template in the dashboard and begin
+creating VM-based workspaces.

diff --git a/docs/platforms/azure.md b/docs/platforms/azure.md
index 318b1d8b4ceb2..72fab874d3322 100644
--- a/docs/platforms/azure.md
+++ b/docs/platforms/azure.md
@@ -9,23 +9,33 @@ This guide assumes you have full administrator privileges on Azure.
## Create An Azure VM
-From the Azure Portal, navigate to the Virtual Machines Dashboard. Click Create, and select creating a new Azure Virtual machine .
+From the Azure Portal, navigate to the Virtual Machines Dashboard. Click Create,
+and select creating a new Azure Virtual machine .
-This will bring you to the `Create a virtual machine` page. Select the subscription group of your choice, or create one if necessary.
+This will bring you to the `Create a virtual machine` page. Select the
+subscription group of your choice, or create one if necessary.
-Next, name the VM something relevant to this project using the naming convention of your choice. Change the region to something more appropriate for your current location. For this tutorial, we will use the base selection of the Ubuntu Gen2 Image and keep the rest of the base settings for this image the same.
+Next, name the VM something relevant to this project using the naming convention
+of your choice. Change the region to something more appropriate for your current
+location. For this tutorial, we will use the base selection of the Ubuntu Gen2
+Image and keep the rest of the base settings for this image the same.
-Up next, under `Inbound port rules` modify the Select `inbound ports` to also take in `HTTPS` and `HTTP`.
+Up next, under `Inbound port rules` modify the Select `inbound ports` to also
+take in `HTTPS` and `HTTP`.
-The set up for the image is complete at this stage. Click `Review and Create` - review the information and click `Create`. A popup will appear asking you to download the key pair for the server. Click `Download private key and create resource` and place it into a folder of your choice on your local system.
+The set up for the image is complete at this stage. Click `Review and Create` -
+review the information and click `Create`. A popup will appear asking you to
+download the key pair for the server. Click
+`Download private key and create resource` and place it into a folder of your
+choice on your local system.
@@ -33,74 +43,96 @@ Click `Return to create a virtual machine`. Your VM will start up!
-Click `Go to resource` in the virtual machine and copy the public IP address. You will need it to SSH into the virtual machine via your local machine.
+Click `Go to resource` in the virtual machine and copy the public IP address.
+You will need it to SSH into the virtual machine via your local machine.
-Follow [these instructions](https://learn.microsoft.com/en-us/azure/virtual-machines/linux-vm-connect?tabs=Linux) to SSH into the virtual machine. Once on the VM, you can run and install Coder using your method of choice. For the fastest install, we recommend running Coder as a system service.
+Follow
+[these instructions](https://learn.microsoft.com/en-us/azure/virtual-machines/linux-vm-connect?tabs=Linux)
+to SSH into the virtual machine. Once on the VM, you can run and install Coder
+using your method of choice. For the fastest install, we recommend running Coder
+as a system service.
## Install Coder
-For this instance, we will run Coder as a system service, however you can run Coder a multitude of different ways. You can learn more about those [here](https://coder.com/docs/coder-oss/latest/install).
+For this instance, we will run Coder as a system service, however you can run
+Coder a multitude of different ways. You can learn more about those
+[here](https://coder.com/docs/coder-oss/latest/install).
In the Azure VM instance, run the following command to install Coder
-```console
-curl -fsSL | sh
+```shell
+curl -fsSL https://coder.com/install.sh | sh
```
## Run Coder
Run the following command to start Coder as a system level service:
-```console
- sudo systemctl enable --now coder
+```shell
+sudo systemctl enable --now coder
```
The following command will get you information about the Coder launch service
-```console
- journalctl -u coder.service -b
+```shell
+journalctl -u coder.service -b
```
-This will return a series of logs related to running Coder as a system service. Embedded in the logs is the Coder Access URL.
+This will return a series of logs related to running Coder as a system service.
+Embedded in the logs is the Coder Access URL.
-Copy the URL and run the following command to create the first user, either on your local machine or in the instance terminal.
+Copy the URL and run the following command to create the first user, either on
+your local machine or in the instance terminal.
-```console
+```shell
coder login
```
-Fill out the prompts. Be sure to save use email and password as these are your admin username and password.
+Fill out the prompts. Be sure to save use email and password as these are your
+admin username and password.
-You can now access Coder on your local machine with the relevant `***.try.coder.app` URL and logging in with the username and password.
+You can now access Coder on your local machine with the relevant
+`***.try.coder.app` URL and logging in with the username and password.
## Creating and Uploading Your First Template
-First, run `coder template init` to create your first template. You’ll be given a list of possible templates to use. This tutorial will show you how to set up your Coder instance to create a Linux based machine on Azure.
+First, run `coder template init` to create your first template. You’ll be given
+a list of possible templates to use. This tutorial will show you how to set up
+your Coder instance to create a Linux based machine on Azure.
-Press `enter` to select `Develop in Linux on Azure` template. This will return the following:
+Press `enter` to select `Develop in Linux on Azure` template. This will return
+the following:
-To get started using the Azure template, install the Azure CLI by following the instructions [here](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt). Run `az login` and follow the instructions to configure the Azure command line.
+To get started using the Azure template, install the Azure CLI by following the
+instructions
+[here](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt).
+Run `az login` and follow the instructions to configure the Azure command line.
-Coder is running as a system service, which creates the system user `coder` for handling processes. The Coder user will require access to the Azure credentials to initialize the template.
+Coder is running as a system service, which creates the system user `coder` for
+handling processes. The Coder user will require access to the Azure credentials
+to initialize the template.
-Run the following commands to copy the Azure credentials and give the `coder` user access to them:
+Run the following commands to copy the Azure credentials and give the `coder`
+user access to them:
-```console
+```shell
sudo cp -r ~/.azure /home/coder/.azure
sudo chown -R coder:coder /home/coder/.azure/
```
-Navigate to the `./azure-linux` folder where you created your template and run the following command to put the template on your Coder instance.
+Navigate to the `./azure-linux` folder where you created your template and run
+the following command to put the template on your Coder instance.
-```console
+```shell
coder templates create
```
-Congrats! You can now navigate to your Coder dashboard and use this Linux on Azure template to create a new workspace!
+Congrats! You can now navigate to your Coder dashboard and use this Linux on
+Azure template to create a new workspace!
## Next Steps
diff --git a/docs/platforms/docker.md b/docs/platforms/docker.md
index 2a33c678830ec..7784e455da570 100644
--- a/docs/platforms/docker.md
+++ b/docs/platforms/docker.md
@@ -6,8 +6,8 @@ Coder with Docker has the following advantages:
- Workspace images are easily configured
- Workspaces share resources for burst operations
-> Note that the below steps are only supported on a Linux distribution.
-> If on macOS, please [run Coder via the standalone binary](../install//binary.md).
+> Note that the below steps are only supported on a Linux distribution. If on
+> macOS, please [run Coder via the standalone binary](../install//binary.md).
## Requirements
@@ -29,18 +29,23 @@ Coder with Docker has the following advantages:
ghcr.io/coder/coder:latest
```
- > This will use Coder's tunnel and built-in database. See our [Docker documentation](../install/docker.md) for other configuration options such as running on localhost, using docker-compose, and external PostgreSQL.
+ > This will use Coder's tunnel and built-in database. See our
+ > [Docker documentation](../install/docker.md) for other configuration
+ > options such as running on localhost, using docker-compose, and external
+ > PostgreSQL.
-1. In new terminal, [install Coder](../install/) in order to connect to your deployment through the CLI.
+1. In new terminal, [install Coder](../install/) in order to connect to your
+ deployment through the CLI.
```console
curl -L https://coder.com/install.sh | sh
```
-1. Run `coder login ` and follow the
- interactive instructions to create your user.
+1. Run `coder login ` and follow the interactive instructions to
+ create your user.
-1. Pull the "Docker" example template using the interactive `coder templates init`:
+1. Pull the "Docker" example template using the interactive
+ `coder templates init`:
```console
coder templates init
@@ -49,8 +54,7 @@ Coder with Docker has the following advantages:
1. Push up the template with `coder templates create`
-1. Open the dashboard in your browser to create your
- first workspace:
+1. Open the dashboard in your browser to create your first workspace:
@@ -74,18 +78,25 @@ Coder with Docker has the following advantages:
You can use a remote Docker host in 2 ways.
-1. Over SSH. See [here](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs#remote-hosts) for details.
-2. Over TCP. See [here](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs#certificate-information) for details.
+1. Over SSH. See
+ [here](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs#remote-hosts)
+ for details.
+2. Over TCP. See
+ [here](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs#certificate-information)
+ for details.
## Troubleshooting
### Docker-based workspace is stuck in "Connecting..."
-Ensure you have an externally-reachable `CODER_ACCESS_URL` set. See [troubleshooting templates](../templates/index.md#Troubleshooting) for more steps.
+Ensure you have an externally-reachable `CODER_ACCESS_URL` set. See
+[troubleshooting templates](../templates/index.md#Troubleshooting) for more
+steps.
### Permission denied while trying to connect to the Docker daemon socket
-See Docker's official documentation to [Manage Docker as a non-root user](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user).
+See Docker's official documentation to
+[Manage Docker as a non-root user](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user).
## Next Steps
diff --git a/docs/platforms/google-cloud-platform.md b/docs/platforms/google-cloud-platform.md
index 8f20820c65806..f48a69e5fd8a8 100644
--- a/docs/platforms/google-cloud-platform.md
+++ b/docs/platforms/google-cloud-platform.md
@@ -1,22 +1,31 @@
# Google Cloud Platform
-In this guide, you will learn how to deploy the Coder control plane instance and your first template.
+In this guide, you will learn how to deploy the Coder control plane instance and
+your first template.
## Requirements
-This guide assumes you have `roles/compute.instanceAdmin.v1` access to your Google Cloud Platform project.
+This guide assumes you have `roles/compute.instanceAdmin.v1` access to your
+Google Cloud Platform project.
## Setting Up your VM
-If this is the first time you’re creating a VM on this project, you will need to enable the `Compute Engine API`. On the Compute Engine API page, click `enable` and wait for the service to finish connecting.
+If this is the first time you’re creating a VM on this project, you will need to
+enable the `Compute Engine API`. On the Compute Engine API page, click `enable`
+and wait for the service to finish connecting.
-This will pull up the `Create an Instance` page - name the instance something relevant to this project, following your naming convention of choice. In addition, select a region and zone that is close to your physical location. For this instance, we will use the base suggested image.
+This will pull up the `Create an Instance` page - name the instance something
+relevant to this project, following your naming convention of choice. In
+addition, select a region and zone that is close to your physical location. For
+this instance, we will use the base suggested image.
-Under `Identity and API Access`, click `Allow full access to all Cloud APIs`. Scroll down to `Firewall` and click `Allow HTTPS traffic` and `Allow HTTP traffic`.
+Under `Identity and API Access`, click `Allow full access to all Cloud APIs`.
+Scroll down to `Firewall` and click `Allow HTTPS traffic` and
+`Allow HTTP traffic`.
@@ -26,7 +35,8 @@ Congrats you’ve created your VM instance!
## SSH-ing into the VM
-On the Compute Engine Dashboard, click on the VM for this project. Under `Details`, click `SSH` and select `Open in browser window`.
+On the Compute Engine Dashboard, click on the VM for this project. Under
+`Details`, click `SSH` and select `Open in browser window`.
@@ -42,9 +52,12 @@ curl -fsSL https://coder.com/install.sh | sh
## Run Coder
-For this tutorial, we will run Coder as a `systemd` service. You can run Coder in [a multitude of different ways](https://coder.com/docs/coder-oss/latest/install).
+For this tutorial, we will run Coder as a `systemd` service. You can run Coder
+in
+[a multitude of different ways](https://coder.com/docs/coder-oss/latest/install).
-First, edit the `coder.env` file to enable `CODER_TUNNEL` by setting the value to true with the following command:
+First, edit the `coder.env` file to enable `CODER_TUNNEL` by setting the value
+to true with the following command:
```console
sudo vim /etc/coder.d/coder.env
@@ -58,7 +71,8 @@ Exit vim and run the following command to start Coder as a system service:
sudo systemctl enable --now coder
```
-The following command shows the Coder service's logs, including the Access URL. The Access URL will be used to access the Coder control plane.
+The following command shows the Coder service's logs, including the Access URL.
+The Access URL will be used to access the Coder control plane.
```console
journalctl -u coder.service -b
@@ -66,7 +80,8 @@ journalctl -u coder.service -b
-In this instance, Coder can be accessed at the URL `https://fcca2f3bfc9d2e3bf1b9feb50e723448.pit-1.try.coder.app`.
+In this instance, Coder can be accessed at the URL
+`https://fcca2f3bfc9d2e3bf1b9feb50e723448.pit-1.try.coder.app`.
Copy the URL and run the following command to create the workspace admin:
@@ -74,11 +89,16 @@ Copy the URL and run the following command to create the workspace admin:
coder login
```
-Fill out the prompts and be sure to save use email and password. This is your admin login. Now, you can now access Coder from your local machine by navigating to the `***.try.coder.app` URL and logging in with that same username and password.
+Fill out the prompts and be sure to save use email and password. This is your
+admin login. Now, you can now access Coder from your local machine by navigating
+to the `***.try.coder.app` URL and logging in with that same username and
+password.
## Creating and Uploading your First Template
-First, run `coder template init` to create your first template. You’ll be given a list of prefabricated templates. This tutorial shows you how to create a Linux based template on GCP.
+First, run `coder template init` to create your first template. You’ll be given
+a list of prefabricated templates. This tutorial shows you how to create a Linux
+based template on GCP.
@@ -90,23 +110,34 @@ Run the following command:
coder templates create
```
-It will ask for your `project-id`, which you can find on the home page of your GCP Dashboard.
+It will ask for your `project-id`, which you can find on the home page of your
+GCP Dashboard.
-Given it’s your first time setting up Coder, it may give an error that will look like the following:
+Given it’s your first time setting up Coder, it may give an error that will look
+like the following:
-In the error message will be a link. In this case, the URL is `https://console.developes.google.com/apis/api/iam.googles.com/overview:?project=1073148106645`. Copy the respective URL from your error message, and visit it via your browser. It may ask you to enable `Identity and Access Management (IAM) API`.
+In the error message will be a link. In this case, the URL is
+`https://console.developes.google.com/apis/api/iam.googles.com/overview:?project=1073148106645`.
+Copy the respective URL from your error message, and visit it via your browser.
+It may ask you to enable `Identity and Access Management (IAM) API`.
Click `enable` and wait as the API initializes for your account.
-Once initialized, click create credentials in the upper right-hand corner. Select the `Compute Engine API` from the dropdown, and select `Application Data` under `What data will you be accessing?`. In addition, select `Yes, I’m using one or more` under `Are you planning on using this API with Compute Engine, Kubernetes Engine, App Engine, or Cloud Functions?`.
+Once initialized, click create credentials in the upper right-hand corner.
+Select the `Compute Engine API` from the dropdown, and select `Application Data`
+under `What data will you be accessing?`. In addition, select
+`Yes, I’m using one or more` under
+`Are you planning on using this API with Compute Engine, Kubernetes Engine, App Engine, or Cloud Functions?`.
Back in your GCP terminal, run the `coder templates create` one more time.
-Congrats! You can now create new Linux-based workspaces that use Google Cloud Platform. Go onto your Coder dashboard, build your workspace, and get started coding!
+Congrats! You can now create new Linux-based workspaces that use Google Cloud
+Platform. Go onto your Coder dashboard, build your workspace, and get started
+coding!
## Next Steps
diff --git a/docs/platforms/jfrog.md b/docs/platforms/jfrog.md
index 966d1472f6bd5..180c46192f014 100644
--- a/docs/platforms/jfrog.md
+++ b/docs/platforms/jfrog.md
@@ -1,18 +1,23 @@
# JFrog
-Use Coder and JFrog together to secure your development environments without disturbing your developers' existing workflows.
+Use Coder and JFrog together to secure your development environments without
+disturbing your developers' existing workflows.
This guide will demonstrate how to use JFrog Artifactory as a package registry
-within a workspace. We'll use Docker as the underlying compute. But, these concepts apply to any compute platform.
+within a workspace. We'll use Docker as the underlying compute. But, these
+concepts apply to any compute platform.
-The full example template can be found [here](https://github.com/coder/coder/tree/main/examples/templates/jfrog-docker).
+The full example template can be found
+[here](https://github.com/coder/coder/tree/main/examples/templates/jfrog/docker).
## Requirements
- A JFrog Artifactory instance
- An admin-level access token for Artifactory
-- 1:1 mapping of users in Coder to users in Artifactory by email address
-- An npm repository in Artifactory named "npm"
+- 1:1 mapping of users in Coder to users in Artifactory by email address and
+ username
+- Repositories configured in Artifactory for each package manager you want to
+ use
The admin-level access token is used to provision user tokens and is never exposed to
@@ -40,14 +45,14 @@ terraform {
}
artifactory = {
source = "registry.terraform.io/jfrog/artifactory"
- version = "6.22.3"
+ version = "~> 8.4.0"
}
}
}
-variable "jfrog_url" {
+variable "jfrog_host" {
type = string
- description = "The URL of the JFrog instance."
+ description = "JFrog instance hostname. e.g. YYY.jfrog.io"
}
variable "artifactory_access_token" {
@@ -57,15 +62,15 @@ variable "artifactory_access_token" {
# Configure the Artifactory provider
provider "artifactory" {
- url = "${var.jfrog_url}/artifactory"
+ url = "https://${var.jfrog_host}/artifactory"
access_token = "${var.artifactory_access_token}"
}
```
-When pushing the template, you can pass in the variables using the `-V` flag:
+When pushing the template, you can pass in the variables using the `--var` flag:
-```sh
-coder templates push --var 'jfrog_url=https://YYY.jfrog.io' --var 'artifactory_access_token=XXX'
+```shell
+coder templates push --var 'jfrog_host=YYY.jfrog.io' --var 'artifactory_access_token=XXX'
```
## Installing JFrog CLI
@@ -74,21 +79,40 @@ coder templates push --var 'jfrog_url=https://YYY.jfrog.io' --var 'artifactory_a
we'll focus on its ability to configure package managers, as that's the relevant
functionality for most developers.
-The generic method of installing the JFrog CLI is the following command:
+Most users should be able to install `jf` by running the following command:
-```sh
+```shell
curl -fL https://install-cli.jfrog.io | sh
```
Other methods are listed [here](https://jfrog.com/getcli/).
-In our Docker-based example, we install `jf` by adding these lines to our `Dockerfile`:
+In our Docker-based example, we install `jf` by adding these lines to our
+`Dockerfile`:
```Dockerfile
RUN curl -fL https://install-cli.jfrog.io | sh && chmod 755 $(which jf)
```
-and use this `coder_agent` block:
+## Configuring Coder workspace to use JFrog Artifactory repositories
+
+Create a `locals` block to store the Artifactory repository keys for each
+package manager you want to use in your workspace. For example, if you want to
+use artifactory repositories with keys `npm`, `pypi`, and `go`, you can create a
+`locals` block like this:
+
+```hcl
+locals {
+ artifactory_repository_keys = {
+ npm = "npm"
+ python = "pypi"
+ go = "go"
+ }
+}
+```
+
+To automatically configure `jf` CLI and Artifactory repositories for each user,
+add the following lines to your `startup_script` in the `coder_agent` block:
```hcl
resource "coder_agent" "main" {
@@ -107,24 +131,43 @@ resource "coder_agent" "main" {
export CI=true
jf c rm 0 || true
- echo ${artifactory_access_token.me.access_token} | \
- jf c add --access-token-stdin --url ${var.jfrog_url} 0
+ echo ${artifactory_scoped_token.me.access_token} | \
+ jf c add --access-token-stdin --url https://${var.jfrog_host} 0
+
+ # Configure the `npm` CLI to use the Artifactory "npm" repository.
+ cat << EOF > ~/.npmrc
+ email = ${data.coder_workspace.me.owner_email}
+ registry = https://${var.jfrog_host}/artifactory/api/npm/${local.artifactory_repository_keys["npm"]}
+ EOF
+ jf rt curl /api/npm/auth >> .npmrc
+
+ # Configure the `pip` to use the Artifactory "python" repository.
+ mkdir -p ~/.pip
+ cat << EOF > ~/.pip/pip.conf
+ [global]
+ index-url = https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/pypi/${local.artifactory_repository_keys["python"]}/simple
+ EOF
+
EOT
+ # Set GOPROXY to use the Artifactory "go" repository.
+ env = {
+ GOPROXY : "https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/go/${local.artifactory_repository_keys["go"]}"
+ }
}
```
-You can verify that `jf` is configured correctly in your workspace by
-running `jf c show`. It should display output like:
+You can verify that `jf` is configured correctly in your workspace by running
+`jf c show`. It should display output like:
```text
coder@jf:~$ jf c show
Server ID: 0
-JFrog Platform URL: https://cdr.jfrog.io/
-Artifactory URL: https://cdr.jfrog.io/artifactory/
-Distribution URL: https://cdr.jfrog.io/distribution/
-Xray URL: https://cdr.jfrog.io/xray/
-Mission Control URL: https://cdr.jfrog.io/mc/
-Pipelines URL: https://cdr.jfrog.io/pipelines/
+JFrog Platform URL: https://YYY.jfrog.io/
+Artifactory URL: https://YYY.jfrog.io/artifactory/
+Distribution URL: https://YYY.jfrog.io/distribution/
+Xray URL: https://YYY.jfrog.io/xray/
+Mission Control URL: https://YYY.jfrog.io/mc/
+Pipelines URL: https://YYY.jfrog.io/pipelines/
User: ammar@....com
Access token: ...
Default: true
@@ -132,16 +175,16 @@ Default: true
## Installing the JFrog VS Code Extension
-You can install the JFrog VS Code extension into workspaces automatically
-by inserting the following lines into your `startup_script`:
+You can install the JFrog VS Code extension into workspaces by inserting the
+following lines into your `startup_script`:
-```sh
- # Install the JFrog VS Code extension.
- # Find the latest version number at
- # https://open-vsx.org/extension/JFrog/jfrog-vscode-extension.
- JFROG_EXT_VERSION=2.4.1
- curl -o /tmp/jfrog.vsix -L "https://open-vsx.org/api/JFrog/jfrog-vscode-extension/$JFROG_EXT_VERSION/file/JFrog.jfrog-vscode-extension-$JFROG_EXT_VERSION.vsix"
- /tmp/code-server/bin/code-server --install-extension /tmp/jfrog.vsix
+```shell
+# Install the JFrog VS Code extension.
+# Find the latest version number at
+# https://open-vsx.org/extension/JFrog/jfrog-vscode-extension.
+JFROG_EXT_VERSION=2.4.1
+curl -o /tmp/jfrog.vsix -L "https://open-vsx.org/api/JFrog/jfrog-vscode-extension/$JFROG_EXT_VERSION/file/JFrog.jfrog-vscode-extension-$JFROG_EXT_VERSION.vsix"
+/tmp/code-server/bin/code-server --install-extension /tmp/jfrog.vsix
```
Note that this method will only work if your developers use code-server.
@@ -151,24 +194,59 @@ Note that this method will only work if your developers use code-server.
Add the following line to your `startup_script` to configure `npm` to use
Artifactory:
-```sh
+```shell
# Configure the `npm` CLI to use the Artifactory "npm" registry.
cat << EOF > ~/.npmrc
email = ${data.coder_workspace.me.owner_email}
- registry=${var.jfrog_url}/artifactory/api/npm/npm/
+ registry = https://${var.jfrog_host}/artifactory/api/npm/npm/
EOF
jf rt curl /api/npm/auth >> .npmrc
```
Now, your developers can run `npm install`, `npm audit`, etc. and transparently
use Artifactory as the package registry. You can verify that `npm` is configured
-correctly by running `npm install --loglevel=http react` and checking that
-npm is only hitting your Artifactory URL.
+correctly by running `npm install --loglevel=http react` and checking that npm
+is only hitting your Artifactory URL.
+
+## Configuring pip
+
+Add the following lines to your `startup_script` to configure `pip` to use
+Artifactory:
+
+```shell
+ mkdir -p ~/.pip
+ cat << EOF > ~/.pip/pip.conf
+ [global]
+ index-url = https://${data.coder_workspace.me.owner}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/pypi/pypi/simple
+ EOF
+```
+
+Now, your developers can run `pip install` and transparently use Artifactory as
+the package registry. You can verify that `pip` is configured correctly by
+running `pip install --verbose requests` and checking that pip is only hitting
+your Artifactory URL.
+
+## Configuring Go
+
+Add the following environment variable to your `coder_agent` block to configure
+`go` to use Artifactory:
+
+```hcl
+ env = {
+ GOPROXY : "https://${data.coder_workspace.me.owner}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/go/go"
+ }
+```
-You can apply the same concepts to Docker, Go, Maven, and other package managers
-supported by Artifactory.
+You can apply the same concepts to Docker, Maven, and other package managers
+supported by Artifactory. See the
+[JFrog documentation](https://jfrog.com/help/r/jfrog-artifactory-documentation/package-management)
+for more information.
## More reading
-- See the full example template [here](https://github.com/coder/coder/tree/main/examples/templates/jfrog-docker).
-- To serve extensions from your own VS Code Marketplace, check out [code-marketplace](https://github.com/coder/code-marketplace#artifactory-storage).
+- See the full example template
+ [here](https://github.com/coder/coder/tree/main/examples/templates/jfrog/docker).
+- To serve extensions from your own VS Code Marketplace, check out
+ [code-marketplace](https://github.com/coder/code-marketplace#artifactory-storage).
+- To store templates in Artifactory, check out our
+ [Artifactory modules](../templates/modules.md#artifactory) docs.
diff --git a/docs/platforms/kubernetes/additional-clusters.md b/docs/platforms/kubernetes/additional-clusters.md
index 47a193ad9abb8..f7646f5b5c3e6 100644
--- a/docs/platforms/kubernetes/additional-clusters.md
+++ b/docs/platforms/kubernetes/additional-clusters.md
@@ -1,15 +1,19 @@
# Additional clusters
-With Coder, you can deploy workspaces in additional Kubernetes clusters using different [authentication methods](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication) in the Terraform provider.
+With Coder, you can deploy workspaces in additional Kubernetes clusters using
+different
+[authentication methods](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication)
+in the Terraform provider.

## Option 1) Kubernetes contexts and kubeconfig
-First, create a kubeconfig file with [multiple contexts](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/).
+First, create a kubeconfig file with
+[multiple contexts](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/).
-```sh
-$ kubectl config get-contexts
+```shell
+kubectl config get-contexts
CURRENT NAME CLUSTER
workspaces-europe-west2-c workspaces-europe-west2-c
@@ -20,9 +24,10 @@ CURRENT NAME CLUSTER
If you deployed Coder on Kubernetes, you can attach a kubeconfig as a secret.
-This assumes Coder is deployed on the `coder` namespace and your kubeconfig file is in ~/.kube/config.
+This assumes Coder is deployed on the `coder` namespace and your kubeconfig file
+is in ~/.kube/config.
-```sh
+```shell
kubectl create secret generic kubeconfig-secret -n coder --from-file=~/.kube/config
```
@@ -41,15 +46,20 @@ coder:
readOnly: true
```
-[Upgrade Coder](../../install/kubernetes.md#upgrading-coder-via-helm) with these new values.
+[Upgrade Coder](../../install/kubernetes.md#upgrading-coder-via-helm) with these
+new values.
### VM control plane
-If you deployed Coder on a VM, copy the kubeconfig file to `/home/coder/.kube/config`.
+If you deployed Coder on a VM, copy the kubeconfig file to
+`/home/coder/.kube/config`.
### Create a Coder template
-You can start from our [example template](https://github.com/coder/coder/tree/main/examples/templates/kubernetes). From there, add [template parameters](../../templates/parameters.md) to allow developers to pick their desired cluster.
+You can start from our
+[example template](https://github.com/coder/coder/tree/main/examples/templates/kubernetes).
+From there, add [template parameters](../../templates/parameters.md) to allow
+developers to pick their desired cluster.
```hcl
# main.tf
@@ -79,17 +89,22 @@ provider "kubernetes" {
## Option 2) Kubernetes ServiceAccounts
-Alternatively, you can authenticate with remote clusters with ServiceAccount tokens. Coder can store these secrets on your behalf with [managed Terraform variables](../../templates/parameters.md#managed-terraform-variables).
+Alternatively, you can authenticate with remote clusters with ServiceAccount
+tokens. Coder can store these secrets on your behalf with
+[managed Terraform variables](../../templates/parameters.md#managed-terraform-variables).
-Alternatively, these could also be fetched from Kubernetes secrets or even [Hashicorp Vault](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/data-sources/generic_secret).
+Alternatively, these could also be fetched from Kubernetes secrets or even
+[Hashicorp Vault](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/data-sources/generic_secret).
-This guide assumes you have a `coder-workspaces` namespace on your remote cluster. Change the namespace accordingly.
+This guide assumes you have a `coder-workspaces` namespace on your remote
+cluster. Change the namespace accordingly.
### Create a ServiceAccount
-Run this command against your remote cluster to create a ServiceAccount, Role, RoleBinding, and token:
+Run this command against your remote cluster to create a ServiceAccount, Role,
+RoleBinding, and token:
-```sh
+```shell
kubectl apply -n coder-workspaces -f - < Note: This is only required for Coder versions < 0.28.0, as this will be the default value for Coder versions >= 0.28.0
+> Note: This is only required for Coder versions < 0.28.0, as this will be the
+> default value for Coder versions >= 0.28.0
## Installation
-Install the `coder-kubestream-logs` helm chart on the cluster where the deployment is running.
+Install the `coder-kubestream-logs` helm chart on the cluster where the
+deployment is running.
```shell
helm repo add coder-logstream-kube https://helm.coder.com/logstream-kube
@@ -34,7 +45,8 @@ helm install coder-logstream-kube coder-logstream-kube/coder-logstream-kube \
## Example logs
-Here is an example of the logs you can expect to see in the workspace startup logs:
+Here is an example of the logs you can expect to see in the workspace startup
+logs:
### Normal pod deployment
@@ -54,6 +66,13 @@ Here is an example of the logs you can expect to see in the workspace startup lo
## How it works
-Kubernetes provides an [informers](https://pkg.go.dev/k8s.io/client-go/informers) API that streams pod and event data from the API server.
+Kubernetes provides an
+[informers](https://pkg.go.dev/k8s.io/client-go/informers) API that streams pod
+and event data from the API server.
-coder-logstream-kube listens for pod creation events with containers that have the CODER_AGENT_TOKEN environment variable set. All pod events are streamed as logs to the Coder API using the agent token for authentication. For more details, see the [coder-logstream-kube](https://github.com/coder/coder-logstream-kube) repository.
+coder-logstream-kube listens for pod creation events with containers that have
+the CODER_AGENT_TOKEN environment variable set. All pod events are streamed as
+logs to the Coder API using the agent token for authentication. For more
+details, see the
+[coder-logstream-kube](https://github.com/coder/coder-logstream-kube)
+repository.
diff --git a/docs/platforms/kubernetes/index.md b/docs/platforms/kubernetes/index.md
index d52d10e4cddb8..9ad7dfd61879c 100644
--- a/docs/platforms/kubernetes/index.md
+++ b/docs/platforms/kubernetes/index.md
@@ -4,10 +4,12 @@ Coder's control plane and/or workspaces can be deployed on Kubernetes.
## Installation
-Refer to our [Helm install docs](../../install/kubernetes.md) to deploy Coder on Kubernetes. The default helm values will provision the following:
+Refer to our [Helm install docs](../../install/kubernetes.md) to deploy Coder on
+Kubernetes. The default helm values will provision the following:
- Coder control plane (as a `Deployment`)
-- ServiceAccount + Role + RoleBinding to provision pods + PVCS in the current namespace (used for Kubernetes workspaces)
+- ServiceAccount + Role + RoleBinding to provision pods + PVCS in the current
+ namespace (used for Kubernetes workspaces)
- LoadBalancer to access control plane
## Kubernetes templates
@@ -18,9 +20,11 @@ From the dashboard, import the Kubernetes starter template:
In the next screen, set the following template variables:
-- `use_kubeconfig`: `false` (The ServiceAccount will authorize Coder to create pods on your cluster)
+- `use_kubeconfig`: `false` (The ServiceAccount will authorize Coder to create
+ pods on your cluster)
- `namespace`: `coder` (or whatever namespace you deployed Coder on)

-> If you deployed Coder on another platform besides Kubernetes, you can set `use_kubeconfig: true` for Coder to read the config from your VM, for example.
+> If you deployed Coder on another platform besides Kubernetes, you can set
+> `use_kubeconfig: true` for Coder to read the config from your VM, for example.
diff --git a/docs/platforms/other.md b/docs/platforms/other.md
index 7cccea0968eb3..a01654cec04e4 100644
--- a/docs/platforms/other.md
+++ b/docs/platforms/other.md
@@ -1,6 +1,9 @@
# Other platforms
-Coder is highly extensible and is not limited to the platforms outlined in these docs. The control plane can be provisioned on any VM or container compute, and workspaces can include any Terraform resource. See our [architecture diagram](../about/architecture.md) for more details.
+Coder is highly extensible and is not limited to the platforms outlined in these
+docs. The control plane can be provisioned on any VM or container compute, and
+workspaces can include any Terraform resource. See our
+[architecture diagram](../about/architecture.md) for more details.
The following resources may help as you're deploying Coder.
diff --git a/docs/secrets.md b/docs/secrets.md
index 791fab5f1123d..c6057f146a190 100644
--- a/docs/secrets.md
+++ b/docs/secrets.md
@@ -9,26 +9,26 @@ Coder is open-minded about how you get your secrets into your workspaces.
## Wait a minute...
-Your first stab at secrets with Coder should be your local method.
-You can do everything you can locally and more with your Coder workspace, so
-whatever workflow and tools you already use to manage secrets may be brought
-over.
+Your first stab at secrets with Coder should be your local method. You can do
+everything you can locally and more with your Coder workspace, so whatever
+workflow and tools you already use to manage secrets may be brought over.
Often, this workflow is simply:
1. Give your users their secrets in advance
-1. Your users write them to a persistent file after
- they've built their workspace
+1. Your users write them to a persistent file after they've built their
+ workspace
-[Template parameters](./templates/parameters.md) are a dangerous way to accept secrets.
-We show parameters in cleartext around the product. Assume anyone with view
-access to a workspace can also see its parameters.
+[Template parameters](./templates/parameters.md) are a dangerous way to accept
+secrets. We show parameters in cleartext around the product. Assume anyone with
+view access to a workspace can also see its parameters.
## SSH Keys
-Coder generates SSH key pairs for each user. This can be used as an authentication mechanism for
-git providers or other tools. Within workspaces, git will attempt to use this key within workspaces
-via the `$GIT_SSH_COMMAND` environment variable.
+Coder generates SSH key pairs for each user. This can be used as an
+authentication mechanism for git providers or other tools. Within workspaces,
+git will attempt to use this key within workspaces via the `$GIT_SSH_COMMAND`
+environment variable.
Users can view their public key in their account settings:
@@ -40,8 +40,8 @@ Users can view their public key in their account settings:
## Dynamic Secrets
Dynamic secrets are attached to the workspace lifecycle and automatically
-injected into the workspace. With a little bit of up front template work,
-they make life simpler for both the end user and the security team.
+injected into the workspace. With a little bit of up front template work, they
+make life simpler for both the end user and the security team.
This method is limited to
[services with Terraform providers](https://registry.terraform.io/browse/providers),
@@ -64,14 +64,17 @@ resource "coder_agent" "main" {
}
```
-A catch-all variation of this approach is dynamically provisioning a cloud service account (e.g [GCP](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_service_account_key#private_key))
-for each workspace and then making the relevant secrets available via the cloud's secret management
-system.
+A catch-all variation of this approach is dynamically provisioning a cloud
+service account (e.g
+[GCP](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_service_account_key#private_key))
+for each workspace and then making the relevant secrets available via the
+cloud's secret management system.
## Displaying Secrets
While you can inject secrets into the workspace via environment variables, you
-can also show them in the Workspace UI with [`coder_metadata`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/metadata).
+can also show them in the Workspace UI with
+[`coder_metadata`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/metadata).

diff --git a/docs/security/0001_user_apikeys_invalidation.md b/docs/security/0001_user_apikeys_invalidation.md
index e47a5a89d72ba..c6f8fde3bd371 100644
--- a/docs/security/0001_user_apikeys_invalidation.md
+++ b/docs/security/0001_user_apikeys_invalidation.md
@@ -4,31 +4,47 @@
## Summary
-Coder identified an issue in [https://github.com/coder/coder](https://github.com/coder/coder) where API tokens belonging to a deleted user were not invalidated. A deleted user in possession of a valid and non-expired API token is still able to use the above token with their full suite of capabilities.
+Coder identified an issue in
+[https://github.com/coder/coder](https://github.com/coder/coder) where API
+tokens belonging to a deleted user were not invalidated. A deleted user in
+possession of a valid and non-expired API token is still able to use the above
+token with their full suite of capabilities.
## Impact: HIGH
-If exploited, an attacker could perform any action that the deleted user was authorized to perform.
+If exploited, an attacker could perform any action that the deleted user was
+authorized to perform.
## Exploitability: HIGH
-The CLI writes the API key to `~/.coderv2/session` by default, so any deleted user who previously logged in via the Coder CLI has the potential to exploit this. Note that there is a time window for exploitation; API tokens have a maximum lifetime after which they are no longer valid.
+The CLI writes the API key to `~/.coderv2/session` by default, so any deleted
+user who previously logged in via the Coder CLI has the potential to exploit
+this. Note that there is a time window for exploitation; API tokens have a
+maximum lifetime after which they are no longer valid.
-The issue only affects users who were active (not suspended) at the time they were deleted. Users who were first suspended and later deleted cannot exploit this issue.
+The issue only affects users who were active (not suspended) at the time they
+were deleted. Users who were first suspended and later deleted cannot exploit
+this issue.
## Affected Versions
All versions of Coder between v0.8.15 and v0.22.2 (inclusive) are affected.
-All customers are advised to upgrade to [v0.23.0](https://github.com/coder/coder/releases/tag/v0.23.0) as soon as possible.
+All customers are advised to upgrade to
+[v0.23.0](https://github.com/coder/coder/releases/tag/v0.23.0) as soon as
+possible.
## Details
-Coder incorrectly failed to invalidate API keys belonging to a user when they were deleted. When authenticating a user via their API key, Coder incorrectly failed to check whether the API key corresponds to a deleted user.
+Coder incorrectly failed to invalidate API keys belonging to a user when they
+were deleted. When authenticating a user via their API key, Coder incorrectly
+failed to check whether the API key corresponds to a deleted user.
## Indications of Compromise
-> 💡 Automated remediation steps in the upgrade purge all affected API keys. Either perform the following query before upgrade or run it on a backup of your database from before the upgrade.
+> 💡 Automated remediation steps in the upgrade purge all affected API keys.
+> Either perform the following query before upgrade or run it on a backup of
+> your database from before the upgrade.
Execute the following SQL query:
@@ -65,4 +81,7 @@ Otherwise, the following information will be reported:
- User API key ID
- Time the affected API key was last used
-> 💡 If your license includes the [Audit Logs](https://coder.com/docs/v2/latest/admin/audit-logs#filtering-logs) feature, you can then query all actions performed by the above users by using the filter `email:$USER_EMAIL`.
+> 💡 If your license includes the
+> [Audit Logs](https://coder.com/docs/v2/latest/admin/audit-logs#filtering-logs)
+> feature, you can then query all actions performed by the above users by using
+> the filter `email:$USER_EMAIL`.
diff --git a/docs/security/index.md b/docs/security/index.md
index 76d2d069e657e..1193f572dab75 100644
--- a/docs/security/index.md
+++ b/docs/security/index.md
@@ -1,12 +1,17 @@
# Security Advisories
-> If you discover a vulnerability in Coder, please do not hesitate to report it to us by following the instructions [here](https://github.com/coder/coder/blob/main/SECURITY.md).
+> If you discover a vulnerability in Coder, please do not hesitate to report it
+> to us by following the instructions
+> [here](https://github.com/coder/coder/blob/main/SECURITY.md).
-From time to time, Coder employees or other community members may discover vulnerabilities in the product.
+From time to time, Coder employees or other community members may discover
+vulnerabilities in the product.
-If a vulnerability requires an immediate upgrade to mitigate a potential security risk, we will add it to the below table.
+If a vulnerability requires an immediate upgrade to mitigate a potential
+security risk, we will add it to the below table.
-Click on the description links to view more details about each specific vulnerability.
+Click on the description links to view more details about each specific
+vulnerability.
---
diff --git a/docs/templates/README.md b/docs/templates/README.md
index fc338650b8ef6..9df47f3d8db0f 100644
--- a/docs/templates/README.md
+++ b/docs/templates/README.md
@@ -16,13 +16,13 @@ individuals can start their own Coder deployments.
From your local machine, download the CLI for your operating system from the
[releases](https://github.com/coder/coder/releases/latest) or run:
-```console
+```shell
curl -fsSL https://coder.com/install.sh | sh
```
To see the sub-commands for managing templates, run:
-```console
+```shell
coder templates --help
```
@@ -31,7 +31,7 @@ coder templates --help
Before you can create templates, you must first login to your Coder deployment
with the CLI.
-```console
+```shell
coder login https://coder.example.com # aka the URL to your coder instance
```
@@ -41,7 +41,7 @@ returning an API Key.
> Make a note of the API Key. You can re-use the API Key in future CLI logins or
> sessions.
-```console
+```shell
coder --token login https://coder.example.com/ # aka the URL to your coder instance
```
@@ -49,7 +49,7 @@ coder --token login https://coder.example.com/ # aka the URL to y
Before users can create workspaces, you'll need at least one template in Coder.
-```sh
+```shell
# create a local directory to store templates
mkdir -p $HOME/coder/templates
cd $HOME/coder/templates
@@ -74,7 +74,7 @@ coder templates create
To control cost, specify a maximum time to live flag for a template in hours or
minutes.
-```sh
+```shell
coder templates create my-template --default-ttl 4h
```
@@ -232,7 +232,7 @@ Alternatively, if you're willing to wait for longer start times from Coder, you
can set the `imagePullPolicy` to `Always` in your Terraform template; when set,
Coder will check `image:tag` on every build and update if necessary:
-```tf
+```hcl
resource "kubernetes_pod" "podName" {
spec {
container {
@@ -254,7 +254,7 @@ Using the UI, navigate to the template page, click on the menu, and select "Edit
Using the CLI, login to Coder and run the following command to edit a single
template:
-```console
+```shell
coder templates edit --description "This is my template"
```
@@ -263,20 +263,20 @@ Review editable template properties by running `coder templates edit -h`.
Alternatively, you can pull down the template as a tape archive (`.tar`) to your
current directory:
-```console
+```shell
coder templates pull file.tar
```
Then, extract it by running:
-```sh
+```shell
tar -xf file.tar
```
Make the changes to your template then run this command from the root of the
template folder:
-```console
+```shell
coder templates push
```
@@ -292,7 +292,7 @@ have any running workspaces associated to it.
Using the CLI, login to Coder and run the following command to delete a
template:
-```console
+```shell
coder templates delete
```
@@ -329,7 +329,7 @@ sets a few environment variables based on the username and email address of the
workspace's owner, so that you can make Git commits immediately without any
manual configuration:
-```tf
+```hcl
resource "coder_agent" "main" {
# ...
env = {
@@ -370,7 +370,7 @@ practices:
- The Coder agent logs are typically stored in `/tmp/coder-agent.log`
- The Coder agent startup script logs are typically stored in `/tmp/coder-startup-script.log`
- The Coder agent shutdown script logs are typically stored in `/tmp/coder-shutdown-script.log`
-- This can also happen if the websockets are not being forwarded correctly when running Coder behind a reverse proxy. [Read our reverse-proxy docs](https://coder.com/docs/v2/latest/admin/configure#tls--reverse-proxy)
+- This can also happen if the websockets are not being forwarded correctly when running Coder behind a reverse proxy. [Read our reverse-proxy docs](../admin/configure.md#tls--reverse-proxy)
### Agent does not become ready
diff --git a/docs/templates/agent-metadata.md b/docs/templates/agent-metadata.md
index 34a8c19a76760..7303e3fa46c89 100644
--- a/docs/templates/agent-metadata.md
+++ b/docs/templates/agent-metadata.md
@@ -2,19 +2,23 @@

-With Agent Metadata, template admins can expose operational metrics from
-their workspaces to their users. It is the dynamic complement of [Resource Metadata](./resource-metadata.md).
+With Agent Metadata, template admins can expose operational metrics from their
+workspaces to their users. It is the dynamic complement of
+[Resource Metadata](./resource-metadata.md).
-See the [Terraform reference](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#metadata).
+See the
+[Terraform reference](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#metadata).
## Examples
-All of these examples use [heredoc strings](https://developer.hashicorp.com/terraform/language/expressions/strings#heredoc-strings) for the script declaration. With heredoc strings, you
-can script without messy escape codes, just as if you were working in your terminal.
+All of these examples use
+[heredoc strings](https://developer.hashicorp.com/terraform/language/expressions/strings#heredoc-strings)
+for the script declaration. With heredoc strings, you can script without messy
+escape codes, just as if you were working in your terminal.
-Some of the below examples use the [`coder stat`](../cli/stat.md) command.
-This is useful for determining CPU/memory usage inside a container, which
-can be tricky otherwise.
+Some of the below examples use the [`coder stat`](../cli/stat.md) command. This
+is useful for determining CPU/memory usage inside a container, which can be
+tricky otherwise.
Here's a standard set of metadata snippets for Linux agents:
@@ -84,9 +88,9 @@ resource "coder_agent" "main" {
## Utilities
-[top](https://linux.die.net/man/1/top) is available in most Linux
-distributions and provides virtual memory, CPU and IO statistics. Running `top`
-produces output that looks like:
+[top](https://linux.die.net/man/1/top) is available in most Linux distributions
+and provides virtual memory, CPU and IO statistics. Running `top` produces
+output that looks like:
```text
%Cpu(s): 65.8 us, 4.4 sy, 0.0 ni, 29.3 id, 0.3 wa, 0.0 hi, 0.2 si, 0.0 st
@@ -95,8 +99,8 @@ MiB Swap: 0.0 total, 0.0 free, 0.0 used. 11021.3 avail Mem
```
[vmstat](https://linux.die.net/man/8/vmstat) is available in most Linux
-distributions and provides virtual memory, CPU and IO statistics. Running `vmstat`
-produces output that looks like:
+distributions and provides virtual memory, CPU and IO statistics. Running
+`vmstat` produces output that looks like:
```text
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
@@ -104,9 +108,9 @@ r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 19580 4781680 12133692 217646944 0 2 4 32 1 0 1 1 98 0 0
```
-[dstat](https://linux.die.net/man/1/dstat) is considerably more parseable
-than `vmstat` but often not included in base images. It is easily installed by
-most package managers under the name `dstat`. The output of running `dstat 1 1` looks
+[dstat](https://linux.die.net/man/1/dstat) is considerably more parseable than
+`vmstat` but often not included in base images. It is easily installed by most
+package managers under the name `dstat`. The output of running `dstat 1 1` looks
like:
```text
@@ -117,9 +121,9 @@ usr sys idl wai stl| read writ| recv send| in out | int csw
## DB Write Load
-Agent metadata can generate a significant write load and overwhelm your
-database if you're not careful. The approximate writes per second can be
-calculated using the formula:
+Agent metadata can generate a significant write load and overwhelm your database
+if you're not careful. The approximate writes per second can be calculated using
+the formula:
```text
(metadata_count * num_running_agents * 2) / metadata_avg_interval
@@ -133,5 +137,5 @@ For example, let's say you have
You can expect `(10 * 6 * 2) / 4` or 30 writes per second.
-One of the writes is to the `UNLOGGED` `workspace_agent_metadata` table and
-the other to the `NOTIFY` query that enables live stats streaming in the UI.
+One of the writes is to the `UNLOGGED` `workspace_agent_metadata` table and the
+other to the `NOTIFY` query that enables live stats streaming in the UI.
diff --git a/docs/templates/authentication.md b/docs/templates/authentication.md
index 4f25be1711b74..3597c83b26dfe 100644
--- a/docs/templates/authentication.md
+++ b/docs/templates/authentication.md
@@ -7,16 +7,19 @@
-Coder's provisioner process needs to authenticate with cloud provider APIs to provision
-workspaces. You can either pass credentials to the provisioner as parameters or execute Coder
-in an environment that is authenticated with the cloud provider.
+Coder's provisioner process needs to authenticate with cloud provider APIs to
+provision workspaces. You can either pass credentials to the provisioner as
+parameters or execute Coder in an environment that is authenticated with the
+cloud provider.
-We encourage the latter where supported. This approach simplifies the template, keeps cloud
-provider credentials out of Coder's database (making it a less valuable target for attackers),
-and is compatible with agent-based authentication schemes (that handle credential rotation
-and/or ensure the credentials are not written to disk).
+We encourage the latter where supported. This approach simplifies the template,
+keeps cloud provider credentials out of Coder's database (making it a less
+valuable target for attackers), and is compatible with agent-based
+authentication schemes (that handle credential rotation and/or ensure the
+credentials are not written to disk).
-Cloud providers for which the Terraform provider supports authenticated environments include
+Cloud providers for which the Terraform provider supports authenticated
+environments include
- [Google Cloud](https://registry.terraform.io/providers/hashicorp/google/latest/docs)
- [Amazon Web Services](https://registry.terraform.io/providers/hashicorp/aws/latest/docs)
@@ -24,11 +27,11 @@ Cloud providers for which the Terraform provider supports authenticated environm
- [Kubernetes](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs)
Additional providers may be supported; check the
-[documentation of the Terraform provider](https://registry.terraform.io/browse/providers) for
-details.
+[documentation of the Terraform provider](https://registry.terraform.io/browse/providers)
+for details.
-The way these generally work is via the credentials being available to Coder either in some
-well-known location on disk (e.g. `~/.aws/credentials` for AWS on posix systems), or via
-environment variables. It is usually sufficient to authenticate using the CLI or SDK for the
-cloud provider before running Coder for this to work, but check the Terraform provider
-documentation for details.
+The way these generally work is via the credentials being available to Coder
+either in some well-known location on disk (e.g. `~/.aws/credentials` for AWS on
+posix systems), or via environment variables. It is usually sufficient to
+authenticate using the CLI or SDK for the cloud provider before running Coder
+for this to work, but check the Terraform provider documentation for details.
diff --git a/docs/templates/change-management.md b/docs/templates/change-management.md
index a1add78ec64f8..6c4fecfa8da2f 100644
--- a/docs/templates/change-management.md
+++ b/docs/templates/change-management.md
@@ -1,6 +1,7 @@
# Template Change Management
-We recommend source controlling your templates as you would other code. [Install Coder](../install/) in CI/CD pipelines to push new template versions.
+We recommend source controlling your templates as you would other code.
+[Install Coder](../install/) in CI/CD pipelines to push new template versions.
```console
# Install the Coder CLI
@@ -20,12 +21,14 @@ export CODER_TEMPLATE_DIR=.coder/templates/kubernetes
export CODER_TEMPLATE_VERSION=$(git rev-parse --short HEAD)
# Push the new template version to Coder
+coder login --url $CODER_URL --token $CODER_SESSION_TOKEN
coder templates push --yes $CODER_TEMPLATE_NAME \
--directory $CODER_TEMPLATE_DIR \
--name=$CODER_TEMPLATE_VERSION # Version name is optional
```
-> Looking for an example? See how we push our development image
-> and template [via GitHub actions](https://github.com/coder/coder/blob/main/.github/workflows/dogfood.yaml).
+> Looking for an example? See how we push our development image and template
+> [via GitHub actions](https://github.com/coder/coder/blob/main/.github/workflows/dogfood.yaml).
-> To cap token lifetime on creation, [configure Coder server to set a shorter max token lifetime](../cli/server.md#--max-token-lifetime)
+> To cap token lifetime on creation,
+> [configure Coder server to set a shorter max token lifetime](../cli/server.md#--max-token-lifetime)
diff --git a/docs/templates/devcontainers.md b/docs/templates/devcontainers.md
index 3a92e79a90843..10a107ca451b0 100644
--- a/docs/templates/devcontainers.md
+++ b/docs/templates/devcontainers.md
@@ -1,20 +1,32 @@
# Devcontainers (alpha)
-[Devcontainers](https://containers.dev) are an open source specification for defining development environments. [envbuilder](https://github.com/coder/envbuilder) is an open source project by Coder that runs devcontainers via Coder templates and your underlying infrastructure.
+[Devcontainers](https://containers.dev) are an open source specification for
+defining development environments.
+[envbuilder](https://github.com/coder/envbuilder) is an open source project by
+Coder that runs devcontainers via Coder templates and your underlying
+infrastructure.
-There are several benefits to adding a devcontainer-compatible template to Coder:
+There are several benefits to adding a devcontainer-compatible template to
+Coder:
-- Drop-in migration from Codespaces (or any existing repositories that use devcontainers)
+- Drop-in migration from Codespaces (or any existing repositories that use
+ devcontainers)
- Easier to start projects from Coder (new workspace, pick starter devcontainer)
-- Developer teams can "bring their own image." No need for platform teams to manage complex images, registries, and CI pipelines.
+- Developer teams can "bring their own image." No need for platform teams to
+ manage complex images, registries, and CI pipelines.
## How it works
-- Coder admins add a devcontainer-compatible template to Coder (envbuilder can run on Docker or Kubernetes)
+- Coder admins add a devcontainer-compatible template to Coder (envbuilder can
+ run on Docker or Kubernetes)
-- Developers enter their repository URL as a [parameter](./parameters.md) when they create their workspace. [envbuilder](https://github.com/coder/envbuilder) clones the repo and builds a container from the `devcontainer.json` specified in the repo.
+- Developers enter their repository URL as a [parameter](./parameters.md) when
+ they create their workspace. [envbuilder](https://github.com/coder/envbuilder)
+ clones the repo and builds a container from the `devcontainer.json` specified
+ in the repo.
-- Developers can edit the `devcontainer.json` in their workspace to rebuild to iterate on their development environments.
+- Developers can edit the `devcontainer.json` in their workspace to rebuild to
+ iterate on their development environments.
## Example templates
@@ -23,16 +35,24 @@ There are several benefits to adding a devcontainer-compatible template to Coder

-[Parameters](./parameters.md) can be used to prompt the user for a repo URL when they are creating a workspace.
+[Parameters](./parameters.md) can be used to prompt the user for a repo URL when
+they are creating a workspace.
## Authentication
-You may need to authenticate to your container registry (e.g. Artifactory) or git provider (e.g. GitLab) to use envbuilder. Refer to the [envbuilder documentation](https://github.com/coder/envbuilder/) for more information.
+You may need to authenticate to your container registry (e.g. Artifactory) or
+git provider (e.g. GitLab) to use envbuilder. Refer to the
+[envbuilder documentation](https://github.com/coder/envbuilder/) for more
+information.
## Caching
-To improve build times, devcontainers can be cached. Refer to the [envbuilder documentation](https://github.com/coder/envbuilder/) for more information.
+To improve build times, devcontainers can be cached. Refer to the
+[envbuilder documentation](https://github.com/coder/envbuilder/) for more
+information.
## Other features & known issues
-Envbuilder is still under active development. Refer to the [envbuilder GitHub repo](https://github.com/coder/envbuilder/) for more information and to submit feature requests.
+Envbuilder is still under active development. Refer to the
+[envbuilder GitHub repo](https://github.com/coder/envbuilder/) for more
+information and to submit feature requests.
diff --git a/docs/templates/docker-in-workspaces.md b/docs/templates/docker-in-workspaces.md
index c0e0cf9bc351a..24357d771fcc6 100644
--- a/docs/templates/docker-in-workspaces.md
+++ b/docs/templates/docker-in-workspaces.md
@@ -11,11 +11,21 @@ There are a few ways to run Docker within container-based Coder workspaces.
## Sysbox container runtime
-The [Sysbox](https://github.com/nestybox/sysbox) container runtime allows unprivileged users to run system-level applications, such as Docker, securely from the workspace containers. Sysbox requires a [compatible Linux distribution](https://github.com/nestybox/sysbox/blob/master/docs/distro-compat.md) to implement these security features. Sysbox can also be used to run systemd inside Coder workspaces. See [Systemd in Docker](#systemd-in-docker).
+The [Sysbox](https://github.com/nestybox/sysbox) container runtime allows
+unprivileged users to run system-level applications, such as Docker, securely
+from the workspace containers. Sysbox requires a
+[compatible Linux distribution](https://github.com/nestybox/sysbox/blob/master/docs/distro-compat.md)
+to implement these security features. Sysbox can also be used to run systemd
+inside Coder workspaces. See [Systemd in Docker](#systemd-in-docker).
+
+The Sysbox container runtime is not compatible with our
+[workspace process logging](./process-logging.md) feature. Envbox is compatible
+with process logging, however.
### Use Sysbox in Docker-based templates
-After [installing Sysbox](https://github.com/nestybox/sysbox#installation) on the Coder host, modify your template to use the sysbox-runc runtime:
+After [installing Sysbox](https://github.com/nestybox/sysbox#installation) on
+the Coder host, modify your template to use the sysbox-runc runtime:
```hcl
resource "docker_container" "workspace" {
@@ -44,7 +54,10 @@ resource "coder_agent" "main" {
### Use Sysbox in Kubernetes-based templates
-After [installing Sysbox on Kubernetes](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/install-k8s.md), modify your template to use the sysbox-runc RuntimeClass. This requires the Kubernetes Terraform provider version 2.16.0 or greater.
+After
+[installing Sysbox on Kubernetes](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/install-k8s.md),
+modify your template to use the sysbox-runc RuntimeClass. This requires the
+Kubernetes Terraform provider version 2.16.0 or greater.
```hcl
terraform {
@@ -109,15 +122,20 @@ resource "kubernetes_pod" "dev" {
}
```
-> Sysbox CE (Community Edition) supports a maximum of 16 pods (workspaces) per node on Kubernetes. See the [Sysbox documentation](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/install-k8s.md#limitations) for more details.
+> Sysbox CE (Community Edition) supports a maximum of 16 pods (workspaces) per
+> node on Kubernetes. See the
+> [Sysbox documentation](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/install-k8s.md#limitations)
+> for more details.
## Envbox
-[Envbox](https://github.com/coder/envbox) is an image developed and maintained by Coder that bundles the sysbox runtime. It works
-by starting an outer container that manages the various sysbox daemons and spawns an unprivileged
-inner container that acts as the user's workspace. The inner container is able to run system-level
-software similar to a regular virtual machine (e.g. `systemd`, `dockerd`, etc). Envbox offers the
-following benefits over running sysbox directly on the nodes:
+[Envbox](https://github.com/coder/envbox) is an image developed and maintained
+by Coder that bundles the sysbox runtime. It works by starting an outer
+container that manages the various sysbox daemons and spawns an unprivileged
+inner container that acts as the user's workspace. The inner container is able
+to run system-level software similar to a regular virtual machine (e.g.
+`systemd`, `dockerd`, etc). Envbox offers the following benefits over running
+sysbox directly on the nodes:
- No custom runtime installation or management on your Kubernetes nodes.
- No limit to the number of pods that run envbox.
@@ -125,27 +143,37 @@ following benefits over running sysbox directly on the nodes:
Some drawbacks include:
- The outer container must be run as privileged
- - Note: the inner container is _not_ privileged. For more information on the security of sysbox
- containers see sysbox's [official documentation](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/security.md).
-- Initial workspace startup is slower than running `sysbox-runc` directly on the nodes. This is due
- to `envbox` having to pull the image to its own Docker cache on its initial startup. Once the image
- is cached in `envbox`, startup performance is similar.
-
-Envbox requires the same kernel requirements as running sysbox directly on the nodes. Refer
-to sysbox's [compatibility matrix](https://github.com/nestybox/sysbox/blob/master/docs/distro-compat.md#sysbox-distro-compatibility) to ensure your nodes are compliant.
-
-To get started with `envbox` check out the [starter template](https://github.com/coder/coder/tree/main/examples/templates/envbox) or visit the [repo](https://github.com/coder/envbox).
+ - Note: the inner container is _not_ privileged. For more information on the
+ security of sysbox containers see sysbox's
+ [official documentation](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/security.md).
+- Initial workspace startup is slower than running `sysbox-runc` directly on the
+ nodes. This is due to `envbox` having to pull the image to its own Docker
+ cache on its initial startup. Once the image is cached in `envbox`, startup
+ performance is similar.
+
+Envbox requires the same kernel requirements as running sysbox directly on the
+nodes. Refer to sysbox's
+[compatibility matrix](https://github.com/nestybox/sysbox/blob/master/docs/distro-compat.md#sysbox-distro-compatibility)
+to ensure your nodes are compliant.
+
+To get started with `envbox` check out the
+[starter template](https://github.com/coder/coder/tree/main/examples/templates/envbox)
+or visit the [repo](https://github.com/coder/envbox).
### Authenticating with a Private Registry
-Authenticating with a private container registry can be done by referencing the credentials
-via the `CODER_IMAGE_PULL_SECRET` environment variable. It is encouraged to populate this
-[environment variable](https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-container-environment-variables-using-secret-data) by using a Kubernetes [secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials).
+Authenticating with a private container registry can be done by referencing the
+credentials via the `CODER_IMAGE_PULL_SECRET` environment variable. It is
+encouraged to populate this
+[environment variable](https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-container-environment-variables-using-secret-data)
+by using a Kubernetes
+[secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials).
-Refer to your container registry documentation to understand how to best create this secret.
+Refer to your container registry documentation to understand how to best create
+this secret.
-The following shows a minimal example using a the JSON API key from a GCP service account to pull
-a private image:
+The following shows a minimal example using a the JSON API key from a GCP
+service account to pull a private image:
```bash
# Create the secret
@@ -170,17 +198,22 @@ env {
## Rootless podman
-[Podman](https://docs.podman.io/en/latest/) is Docker alternative that is compatible with OCI containers specification. which can run rootless inside Kubernetes pods. No custom RuntimeClass is required.
+[Podman](https://docs.podman.io/en/latest/) is Docker alternative that is
+compatible with OCI containers specification. which can run rootless inside
+Kubernetes pods. No custom RuntimeClass is required.
-Prior to completing the steps below, please review the following Podman documentation:
+Prior to completing the steps below, please review the following Podman
+documentation:
- [Basic setup and use of Podman in a rootless environment](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md)
- [Shortcomings of Rootless Podman](https://github.com/containers/podman/blob/main/rootless.md#shortcomings-of-rootless-podman)
-1. Enable [smart-device-manager](https://gitlab.com/arm-research/smarter/smarter-device-manager#enabling-access) to securely expose a FUSE devices to pods.
+1. Enable
+ [smart-device-manager](https://gitlab.com/arm-research/smarter/smarter-device-manager#enabling-access)
+ to securely expose a FUSE devices to pods.
- ```sh
+ ```shell
cat < ⚠️ **Warning**: If you are using a managed Kubernetes distribution (e.g. AKS, EKS, GKE), be sure to set node labels via your cloud provider. Otherwise, your nodes may drop the labels and break podman functionality.
+ > ⚠️ **Warning**: If you are using a managed Kubernetes distribution (e.g.
+ > AKS, EKS, GKE), be sure to set node labels via your cloud provider.
+ > Otherwise, your nodes may drop the labels and break podman functionality.
-3. For systems running SELinux (typically Fedora-, CentOS-, and Red Hat-based systems), you may need to disable SELinux or set it to permissive mode.
+3. For systems running SELinux (typically Fedora-, CentOS-, and Red Hat-based
+ systems), you may need to disable SELinux or set it to permissive mode.
-4. Import our [kubernetes-with-podman](https://github.com/coder/coder/tree/main/examples/templates/kubernetes-with-podman) example template, or make your own.
+4. Import our
+ [kubernetes-with-podman](https://github.com/coder/coder/tree/main/examples/templates/kubernetes-with-podman)
+ example template, or make your own.
- ```sh
+ ```shell
echo "kubernetes-with-podman" | coder templates init
cd ./kubernetes-with-podman
coder templates create
```
- > For more information around the requirements of rootless podman pods, see: [How to run Podman inside of Kubernetes](https://www.redhat.com/sysadmin/podman-inside-kubernetes)
+ > For more information around the requirements of rootless podman pods, see:
+ > [How to run Podman inside of Kubernetes](https://www.redhat.com/sysadmin/podman-inside-kubernetes)
## Privileged sidecar container
-A [privileged container](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities) can be added to your templates to add docker support. This may come in handy if your nodes cannot run Sysbox.
+A
+[privileged container](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)
+can be added to your templates to add docker support. This may come in handy if
+your nodes cannot run Sysbox.
-> ⚠️ **Warning**: This is insecure. Workspaces will be able to gain root access to the host machine.
+> ⚠️ **Warning**: This is insecure. Workspaces will be able to gain root access
+> to the host machine.
### Use a privileged sidecar container in Docker-based templates
@@ -345,10 +388,13 @@ resource "kubernetes_pod" "main" {
## Systemd in Docker
-Additionally, [Sysbox](https://github.com/nestybox/sysbox) can be used to give workspaces full `systemd` capabilities.
+Additionally, [Sysbox](https://github.com/nestybox/sysbox) can be used to give
+workspaces full `systemd` capabilities.
-After [installing Sysbox on Kubernetes](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/install-k8s.md),
-modify your template to use the sysbox-runc RuntimeClass. This requires the Kubernetes Terraform provider version 2.16.0 or greater.
+After
+[installing Sysbox on Kubernetes](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/install-k8s.md),
+modify your template to use the sysbox-runc RuntimeClass. This requires the
+Kubernetes Terraform provider version 2.16.0 or greater.
```hcl
terraform {
diff --git a/docs/templates/index.md b/docs/templates/index.md
index c9a3a455be064..1cd7c5786f244 100644
--- a/docs/templates/index.md
+++ b/docs/templates/index.md
@@ -4,9 +4,10 @@ Templates are written in [Terraform](https://www.terraform.io/) and describe the
infrastructure for workspaces (e.g., docker_container, aws_instance,
kubernetes_pod).
-In most cases, a small group of users (team leads or Coder administrators) [have permissions](../admin/users.md#roles) to create and manage templates. Then, other
-users provision their [workspaces](../workspaces.md) from templates using the UI
-or CLI.
+In most cases, a small group of users (team leads or Coder administrators)
+[have permissions](../admin/users.md#roles) to create and manage templates.
+Then, other users provision their [workspaces](../workspaces.md) from templates
+using the UI or CLI.
## Get the CLI
@@ -16,13 +17,13 @@ individuals can start their own Coder deployments.
From your local machine, download the CLI for your operating system from the
[releases](https://github.com/coder/coder/releases/latest) or run:
-```console
+```shell
curl -fsSL https://coder.com/install.sh | sh
```
To see the sub-commands for managing templates, run:
-```console
+```shell
coder templates --help
```
@@ -31,7 +32,7 @@ coder templates --help
Before you can create templates, you must first login to your Coder deployment
with the CLI.
-```console
+```shell
coder login https://coder.example.com # aka the URL to your coder instance
```
@@ -41,7 +42,7 @@ returning an API Key.
> Make a note of the API Key. You can re-use the API Key in future CLI logins or
> sessions.
-```console
+```shell
coder --token login https://coder.example.com/ # aka the URL to your coder instance
```
@@ -49,7 +50,7 @@ coder --token login https://coder.example.com/ # aka the URL to y
Before users can create workspaces, you'll need at least one template in Coder.
-```sh
+```shell
# create a local directory to store templates
mkdir -p $HOME/coder/templates
cd $HOME/coder/templates
@@ -74,7 +75,7 @@ coder templates create
To control cost, specify a maximum time to live flag for a template in hours or
minutes.
-```sh
+```shell
coder templates create my-template --default-ttl 4h
```
@@ -83,28 +84,35 @@ coder templates create my-template --default-ttl 4h
Example templates are not designed to support every use (e.g
[examples/aws-linux](https://github.com/coder/coder/tree/main/examples/templates/aws-linux)
does not support custom VPCs). You can add these features by editing the
-Terraform code once you run `coder templates init` (new) or `coder templates pull` (existing).
+Terraform code once you run `coder templates init` (new) or
+`coder templates pull` (existing).
Refer to the following resources to build your own templates:
- Terraform: [Documentation](https://developer.hashicorp.com/terraform/docs) and
[Registry](https://registry.terraform.io)
-- Common [concepts in templates](#concepts-in-templates) and [Coder Terraform provider](https://registry.terraform.io/providers/coder/coder/latest/docs)
-- [Coder example templates](https://github.com/coder/coder/tree/main/examples/templates) code
+- Common [concepts in templates](#concepts-in-templates) and
+ [Coder Terraform provider](https://registry.terraform.io/providers/coder/coder/latest/docs)
+- [Coder example templates](https://github.com/coder/coder/tree/main/examples/templates)
+ code
## Concepts in templates
-While templates are written with standard Terraform, the [Coder Terraform Provider](https://registry.terraform.io/providers/coder/coder/latest/docs) is used to define the workspace lifecycle and establish a connection from resources
-to Coder.
+While templates are written with standard Terraform, the
+[Coder Terraform Provider](https://registry.terraform.io/providers/coder/coder/latest/docs)
+is used to define the workspace lifecycle and establish a connection from
+resources to Coder.
Below is an overview of some key concepts in templates (and workspaces). For all
-template options, reference [Coder Terraform provider docs](https://registry.terraform.io/providers/coder/coder/latest/docs).
+template options, reference
+[Coder Terraform provider docs](https://registry.terraform.io/providers/coder/coder/latest/docs).
### Resource
-Resources in Coder are simply [Terraform resources](https://www.terraform.io/language/resources).
-If a Coder agent is attached to a resource, users can connect directly to the
-resource over SSH or web apps.
+Resources in Coder are simply
+[Terraform resources](https://www.terraform.io/language/resources). If a Coder
+agent is attached to a resource, users can connect directly to the resource over
+SSH or web apps.
### Coder agent
@@ -139,9 +147,10 @@ resource "kubernetes_pod" "pod1" {
}
```
-The `coder_agent` resource can be configured with additional arguments. For example,
-you can use the `env` property to set environment variables that will be inherited
-by all child processes of the agent, including SSH sessions. See the
+The `coder_agent` resource can be configured with additional arguments. For
+example, you can use the `env` property to set environment variables that will
+be inherited by all child processes of the agent, including SSH sessions. See
+the
[Coder Terraform Provider documentation](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent)
for the full list of supported arguments for the `coder_agent`.
@@ -151,14 +160,17 @@ Use the Coder agent's `startup_script` to run additional commands like
installing IDEs, [cloning dotfiles](../dotfiles.md#templates), and cloning
project repos.
-**Note:** By default, the startup script is executed in the background.
-This allows users to access the workspace before the script completes.
-If you want to change this, see [`startup_script_behavior`](#startup_script_behavior) below.
+**Note:** By default, the startup script is executed in the background. This
+allows users to access the workspace before the script completes. If you want to
+change this, see [`startup_script_behavior`](#startup_script_behavior) below.
-Here are a few guidelines for writing a good startup script (more on these below):
+Here are a few guidelines for writing a good startup script (more on these
+below):
-1. Use `set -e` to exit the script if any command fails and `|| true` for commands that are allowed to fail
-2. Use `&` to start a process in the background, allowing the startup script to complete
+1. Use `set -e` to exit the script if any command fails and `|| true` for
+ commands that are allowed to fail
+2. Use `&` to start a process in the background, allowing the startup script to
+ complete
3. Inform the user about what's going on via `echo`
```hcl
@@ -198,17 +210,41 @@ coder dotfiles -y "$DOTFILES_URI"
}
```
-The startup script can contain important steps that must be executed successfully so that the workspace is in a usable state, for this reason we recommend using `set -e` (exit on error) at the top and `|| true` (allow command to fail) to ensure the user is notified when something goes wrong. These are not shown in the example above because, while useful, they need to be used with care. For more assurance, you can utilize [shellcheck](https://www.shellcheck.net) to find bugs in the script and employ [`set -euo pipefail`](https://wizardzines.com/comics/bash-errors/) to exit on error, unset variables, and fail on pipe errors.
-
-We also recommend that startup scripts do not run forever. Long-running processes, like code-server, should be run in the background. This is usually achieved by adding `&` to the end of the command. For example, `sleep 10 &` will run the command in the background and allow the startup script to complete.
-
-> **Note:** If a backgrounded command (`&`) writes to stdout or stderr, the startup script will not complete until the command completes or closes the file descriptors. To avoid this, you can redirect the stdout and stderr to a file. For example, `sleep 10 >/dev/null 2>&1 &` will redirect the stdout and stderr to `/dev/null` (discard) and run the command in the background.
-
-PS. Notice how each step starts with `echo "..."` to provide feedback to the user about what is happening? This is especially useful when the startup script behavior is set to blocking because the user will be informed about why they're waiting to access their workspace.
+The startup script can contain important steps that must be executed
+successfully so that the workspace is in a usable state, for this reason we
+recommend using `set -e` (exit on error) at the top and `|| true` (allow command
+to fail) to ensure the user is notified when something goes wrong. These are not
+shown in the example above because, while useful, they need to be used with
+care. For more assurance, you can utilize
+[shellcheck](https://www.shellcheck.net) to find bugs in the script and employ
+[`set -euo pipefail`](https://wizardzines.com/comics/bash-errors/) to exit on
+error, unset variables, and fail on pipe errors.
+
+We also recommend that startup scripts do not run forever. Long-running
+processes, like code-server, should be run in the background. This is usually
+achieved by adding `&` to the end of the command. For example, `sleep 10 &` will
+run the command in the background and allow the startup script to complete.
+
+> **Note:** If a backgrounded command (`&`) writes to stdout or stderr, the
+> startup script will not complete until the command completes or closes the
+> file descriptors. To avoid this, you can redirect the stdout and stderr to a
+> file. For example, `sleep 10 >/dev/null 2>&1 &` will redirect the stdout and
+> stderr to `/dev/null` (discard) and run the command in the background.
+
+PS. Notice how each step starts with `echo "..."` to provide feedback to the
+user about what is happening? This is especially useful when the startup script
+behavior is set to blocking because the user will be informed about why they're
+waiting to access their workspace.
#### `startup_script_behavior`
-Use the Coder agent's `startup_script_behavior` to change the behavior between `blocking` and `non-blocking` (default). The blocking behavior is recommended for most use cases because it allows the startup script to complete before the user accesses the workspace. For example, let's say you want to check out a very large repo in the startup script. If the startup script is non-blocking, the user may log in via SSH or open the IDE before the repo is fully checked out. This can lead to a poor user experience.
+Use the Coder agent's `startup_script_behavior` to change the behavior between
+`blocking` and `non-blocking` (default). The blocking behavior is recommended
+for most use cases because it allows the startup script to complete before the
+user accesses the workspace. For example, let's say you want to check out a very
+large repo in the startup script. If the startup script is non-blocking, the
+user may log in via SSH or open the IDE before the repo is fully checked out.
+This can lead to a poor user experience.
```hcl
resource "coder_agent" "coder" {
@@ -218,7 +254,10 @@ resource "coder_agent" "coder" {
startup_script = "echo 'Starting...'"
```
-Whichever behavior is enabled, the user can still choose to override it by specifying the appropriate flags (or environment variables) in the CLI when connecting to the workspace. The behavior can be overridden by one of the following means:
+Whichever behavior is enabled, the user can still choose to override it by
+specifying the appropriate flags (or environment variables) in the CLI when
+connecting to the workspace. The behavior can be overridden by one of the
+following means:
- Set an environment variable (for use with `ssh` or `coder ssh`):
- `export CODER_SSH_WAIT=yes` (blocking)
@@ -236,8 +275,9 @@ Whichever behavior is enabled, the user can still choose to override it by speci
Coder workspaces can be started/stopped. This is often used to save on cloud
costs or enforce ephemeral workflows. When a workspace is started or stopped,
-the Coder server runs an additional [terraform apply](https://www.terraform.io/cli/commands/apply),
-informing the Coder provider that the workspace has a new transition state.
+the Coder server runs an additional
+[terraform apply](https://www.terraform.io/cli/commands/apply), informing the
+Coder provider that the workspace has a new transition state.
This template sample has one persistent resource (docker volume) and one
ephemeral resource (docker container).
@@ -278,7 +318,7 @@ Alternatively, if you're willing to wait for longer start times from Coder, you
can set the `imagePullPolicy` to `Always` in your Terraform template; when set,
Coder will check `image:tag` on every build and update if necessary:
-```tf
+```hcl
resource "kubernetes_pod" "podName" {
spec {
container {
@@ -290,17 +330,23 @@ resource "kubernetes_pod" "podName" {
### Edit templates
-You can edit a template using the coder CLI or the UI. Only [template admins and
-owners](../admin/users.md) can edit a template.
+You can edit a template using the coder CLI or the UI. Only
+[template admins and owners](../admin/users.md) can edit a template.
-Using the UI, navigate to the template page, click on the menu, and select "Edit files". In the template editor, you create, edit and remove files. Before publishing a new template version, you can test your modifications by clicking the "Build template" button. Newly published template versions automatically become the default version selection when creating a workspace.
+Using the UI, navigate to the template page, click on the menu, and select "Edit
+files". In the template editor, you create, edit and remove files. Before
+publishing a new template version, you can test your modifications by clicking
+the "Build template" button. Newly published template versions automatically
+become the default version selection when creating a workspace.
-> **Tip**: Even without publishing a version as active, you can still use it to create a workspace before making it the default for everybody in your organization. This may help you debug new changes without impacting others.
+> **Tip**: Even without publishing a version as active, you can still use it to
+> create a workspace before making it the default for everybody in your
+> organization. This may help you debug new changes without impacting others.
Using the CLI, login to Coder and run the following command to edit a single
template:
-```console
+```shell
coder templates edit --description "This is my template"
```
@@ -309,20 +355,20 @@ Review editable template properties by running `coder templates edit -h`.
Alternatively, you can pull down the template as a tape archive (`.tar`) to your
current directory:
-```console
+```shell
coder templates pull file.tar
```
Then, extract it by running:
-```sh
+```shell
tar -xf file.tar
```
Make the changes to your template then run this command from the root of the
template folder:
-```console
+```shell
coder templates push
```
@@ -331,14 +377,14 @@ prompt in the dashboard to update.
### Delete templates
-You can delete a template using both the coder CLI and UI. Only [template admins
-and owners](../admin/users.md) can delete a template, and the template must not
-have any running workspaces associated to it.
+You can delete a template using both the coder CLI and UI. Only
+[template admins and owners](../admin/users.md) can delete a template, and the
+template must not have any running workspaces associated to it.
Using the CLI, login to Coder and run the following command to delete a
template:
-```console
+```shell
coder templates delete
```
@@ -349,9 +395,9 @@ in the right-hand corner of the page to delete the template.
#### Delete workspaces
-When a workspace is deleted, the Coder server essentially runs a [terraform
-destroy](https://www.terraform.io/cli/commands/destroy) to remove all resources
-associated with the workspace.
+When a workspace is deleted, the Coder server essentially runs a
+[terraform destroy](https://www.terraform.io/cli/commands/destroy) to remove all
+resources associated with the workspace.
> Terraform's
> [prevent-destroy](https://www.terraform.io/language/meta-arguments/lifecycle#prevent_destroy)
@@ -368,14 +414,17 @@ users access to additional web applications.
### Data source
When a workspace is being started or stopped, the `coder_workspace` data source
-provides some useful parameters. See the [Coder Terraform provider](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace) for more information.
+provides some useful parameters. See the
+[Coder Terraform provider](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace)
+for more information.
-For example, the [Docker quick-start template](https://github.com/coder/coder/tree/main/examples/templates/docker)
+For example, the
+[Docker quick-start template](https://github.com/coder/coder/tree/main/examples/templates/docker)
sets a few environment variables based on the username and email address of the
workspace's owner, so that you can make Git commits immediately without any
manual configuration:
-```tf
+```hcl
resource "coder_agent" "main" {
# ...
env = {
@@ -393,12 +442,14 @@ customize them however you like.
## Troubleshooting templates
Occasionally, you may run into scenarios where a workspace is created, but the
-agent is either not connected or the [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)
+agent is either not connected or the
+[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)
has failed or timed out.
### Agent connection issues
-If the agent is not connected, it means the agent or [init script](https://github.com/coder/coder/tree/main/provisionersdk/scripts)
+If the agent is not connected, it means the agent or
+[init script](https://github.com/coder/coder/tree/main/provisionersdk/scripts)
has failed on the resource.
```console
@@ -410,33 +461,78 @@ While troubleshooting steps vary by resource, here are some general best
practices:
- Ensure the resource has `curl` installed (alternatively, `wget` or `busybox`)
-- Ensure the resource can `curl` your Coder [access
- URL](../admin/configure.md#access-url)
-- Manually connect to the resource and check the agent logs (e.g., `kubectl exec`, `docker exec` or AWS console)
+- Ensure the resource can `curl` your Coder
+ [access URL](../admin/configure.md#access-url)
+- Manually connect to the resource and check the agent logs (e.g.,
+ `kubectl exec`, `docker exec` or AWS console)
- The Coder agent logs are typically stored in `/tmp/coder-agent.log`
- - The Coder agent startup script logs are typically stored in `/tmp/coder-startup-script.log`
- - The Coder agent shutdown script logs are typically stored in `/tmp/coder-shutdown-script.log`
-- This can also happen if the websockets are not being forwarded correctly when running Coder behind a reverse proxy. [Read our reverse-proxy docs](https://coder.com/docs/v2/latest/admin/configure#tls--reverse-proxy)
+ - The Coder agent startup script logs are typically stored in
+ `/tmp/coder-startup-script.log`
+ - The Coder agent shutdown script logs are typically stored in
+ `/tmp/coder-shutdown-script.log`
+- This can also happen if the websockets are not being forwarded correctly when
+ running Coder behind a reverse proxy.
+ [Read our reverse-proxy docs](../admin/configure.md#tls--reverse-proxy)
### Startup script issues
-Depending on the contents of the [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script), and whether or not the [startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior) is set to blocking or non-blocking, you may notice issues related to the startup script. In this section we will cover common scenarios and how to resolve them.
+Depending on the contents of the
+[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script),
+and whether or not the
+[startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior)
+is set to blocking or non-blocking, you may notice issues related to the startup
+script. In this section we will cover common scenarios and how to resolve them.
#### Unable to access workspace, startup script is still running
-If you're trying to access your workspace and are unable to because the [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) is still running, it means the [startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior) option is set to blocking or you have enabled the `--wait=yes` option (for e.g. `coder ssh` or `coder config-ssh`). In such an event, you can always access the workspace by using the web terminal, or via SSH using the `--wait=no` option. If the startup script is running longer than it should, or never completing, you can try to [debug the startup script](#debugging-the-startup-script) to resolve the issue. Alternatively, you can try to force the startup script to exit by terminating processes started by it or terminating the startup script itself (on Linux, `ps` and `kill` are useful tools).
-
-For tips on how to write a startup script that doesn't run forever, see the [`startup_script`](#startup_script) section. For more ways to override the startup script behavior, see the [`startup_script_behavior`](#startup_script_behavior) section.
-
-Template authors can also set the [startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior) option to non-blocking, which will allow users to access the workspace while the startup script is still running. Note that the workspace must be updated after changing this option.
+If you're trying to access your workspace and are unable to because the
+[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)
+is still running, it means the
+[startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior)
+option is set to blocking or you have enabled the `--wait=yes` option (for e.g.
+`coder ssh` or `coder config-ssh`). In such an event, you can always access the
+workspace by using the web terminal, or via SSH using the `--wait=no` option. If
+the startup script is running longer than it should, or never completing, you
+can try to [debug the startup script](#debugging-the-startup-script) to resolve
+the issue. Alternatively, you can try to force the startup script to exit by
+terminating processes started by it or terminating the startup script itself (on
+Linux, `ps` and `kill` are useful tools).
+
+For tips on how to write a startup script that doesn't run forever, see the
+[`startup_script`](#startup_script) section. For more ways to override the
+startup script behavior, see the
+[`startup_script_behavior`](#startup_script_behavior) section.
+
+Template authors can also set the
+[startup script behavior](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script_behavior)
+option to non-blocking, which will allow users to access the workspace while the
+startup script is still running. Note that the workspace must be updated after
+changing this option.
#### Your workspace may be incomplete
-If you see a warning that your workspace may be incomplete, it means you should be aware that programs, files, or settings may be missing from your workspace. This can happen if the [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) is still running or has exited with a non-zero status (see [startup script error](#startup-script-error)). No action is necessary, but you may want to [start a new shell session](#session-was-started-before-the-startup-script-finished-web-terminal) after it has completed or check the [startup script logs](#debugging-the-startup-script) to see if there are any issues.
+If you see a warning that your workspace may be incomplete, it means you should
+be aware that programs, files, or settings may be missing from your workspace.
+This can happen if the
+[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)
+is still running or has exited with a non-zero status (see
+[startup script error](#startup-script-error)). No action is necessary, but you
+may want to
+[start a new shell session](#session-was-started-before-the-startup-script-finished-web-terminal)
+after it has completed or check the
+[startup script logs](#debugging-the-startup-script) to see if there are any
+issues.
#### Session was started before the startup script finished
-The web terminal may show this message if it was started before the [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) finished, but the startup script has since finished. This message can safely be dismissed, however, be aware that your preferred shell or dotfiles may not yet be activated for this shell session. You can either start a new session or source your dotfiles manually. Note that starting a new session means that commands running in the terminal will be terminated and you may lose unsaved work.
+The web terminal may show this message if it was started before the
+[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)
+finished, but the startup script has since finished. This message can safely be
+dismissed, however, be aware that your preferred shell or dotfiles may not yet
+be activated for this shell session. You can either start a new session or
+source your dotfiles manually. Note that starting a new session means that
+commands running in the terminal will be terminated and you may lose unsaved
+work.
Examples for activating your preferred shell or sourcing your dotfiles:
@@ -445,7 +541,15 @@ Examples for activating your preferred shell or sourcing your dotfiles:
#### Startup script exited with an error
-When the [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) exits with an error, it means the last command run by the script failed. When `set -e` is used, this means that any failing command will immediately exit the script and the remaining commands will not be executed. This also means that [your workspace may be incomplete](#your-workspace-may-be-incomplete). If you see this error, you can check the [startup script logs](#debugging-the-startup-script) to figure out what the issue is.
+When the
+[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)
+exits with an error, it means the last command run by the script failed. When
+`set -e` is used, this means that any failing command will immediately exit the
+script and the remaining commands will not be executed. This also means that
+[your workspace may be incomplete](#your-workspace-may-be-incomplete). If you
+see this error, you can check the
+[startup script logs](#debugging-the-startup-script) to figure out what the
+issue is.
Common causes for startup script errors:
@@ -455,11 +559,20 @@ Common causes for startup script errors:
#### Debugging the startup script
-The simplest way to debug the [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script) is to open the workspace in the Coder dashboard and click "Show startup log" (if not already visible). This will show all the output from the script. Another option is to view the log file inside the workspace (usually `/tmp/coder-startup-script.log`). If the logs don't indicate what's going on or going wrong, you can increase verbosity by adding `set -x` to the top of the startup script (note that this will show all commands run and may output sensitive information). Alternatively, you can add `echo` statements to show what's going on.
+The simplest way to debug the
+[startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#startup_script)
+is to open the workspace in the Coder dashboard and click "Show startup log" (if
+not already visible). This will show all the output from the script. Another
+option is to view the log file inside the workspace (usually
+`/tmp/coder-startup-script.log`). If the logs don't indicate what's going on or
+going wrong, you can increase verbosity by adding `set -x` to the top of the
+startup script (note that this will show all commands run and may output
+sensitive information). Alternatively, you can add `echo` statements to show
+what's going on.
Here's a short example of an informative startup script:
-```sh
+```shell
echo "Running startup script..."
echo "Run: long-running-command"
/path/to/long-running-command
@@ -471,9 +584,13 @@ if [ $status -ne 0 ]; then
fi
```
-> **Note:** We don't use `set -x` here because we're manually echoing the commands. This protects against sensitive information being shown in the log.
+> **Note:** We don't use `set -x` here because we're manually echoing the
+> commands. This protects against sensitive information being shown in the log.
-This script tells us what command is being run and what the exit status is. If the exit status is non-zero, it means the command failed and we exit the script. Since we are manually checking the exit status here, we don't need `set -e` at the top of the script to exit on error.
+This script tells us what command is being run and what the exit status is. If
+the exit status is non-zero, it means the command failed and we exit the script.
+Since we are manually checking the exit status here, we don't need `set -e` at
+the top of the script to exit on error.
## Template permissions (enterprise)
diff --git a/docs/templates/modules.md b/docs/templates/modules.md
index 06827bc2c0fbd..070e1d06cd7a3 100644
--- a/docs/templates/modules.md
+++ b/docs/templates/modules.md
@@ -1,8 +1,12 @@
# Template inheritance
-In instances where you want to reuse code across different Coder templates, such as common scripts or resource definitions, we suggest using [Terraform Modules](https://developer.hashicorp.com/terraform/language/modules).
+In instances where you want to reuse code across different Coder templates, such
+as common scripts or resource definitions, we suggest using
+[Terraform Modules](https://developer.hashicorp.com/terraform/language/modules).
-These modules can be stored externally from Coder, like in a Git repository or a Terraform registry. Below is an example of how to reference a module in your template:
+These modules can be stored externally from Coder, like in a Git repository or a
+Terraform registry. Below is an example of how to reference a module in your
+template:
```hcl
data "coder_workspace" "me" {}
@@ -25,36 +29,52 @@ resource "coder_agent" "dev" {
}
```
-> Learn more about [creating modules](https://developer.hashicorp.com/terraform/language/modules) and [module sources](https://developer.hashicorp.com/terraform/language/modules/sources) in the Terraform documentation.
+> Learn more about
+> [creating modules](https://developer.hashicorp.com/terraform/language/modules)
+> and
+> [module sources](https://developer.hashicorp.com/terraform/language/modules/sources)
+> in the Terraform documentation.
## Git authentication
-If you are importing a module from a private git repository, the Coder server [or provisioner](../admin/provisioners.md) needs git credentials. Since this token will only be used for cloning your repositories with modules, it is best to create a token with limited access to repositories and no extra permissions. In GitHub, you can generate a [fine-grained token](https://docs.github.com/en/rest/overview/permissions-required-for-fine-grained-personal-access-tokens?apiVersion=2022-11-28) with read only access to repos.
+If you are importing a module from a private git repository, the Coder server
+[or provisioner](../admin/provisioners.md) needs git credentials. Since this
+token will only be used for cloning your repositories with modules, it is best
+to create a token with limited access to repositories and no extra permissions.
+In GitHub, you can generate a
+[fine-grained token](https://docs.github.com/en/rest/overview/permissions-required-for-fine-grained-personal-access-tokens?apiVersion=2022-11-28)
+with read only access to repos.
-If you are running Coder on a VM, make sure you have `git` installed and the `coder` user has access to the following files
+If you are running Coder on a VM, make sure you have `git` installed and the
+`coder` user has access to the following files
-```sh
+```toml
# /home/coder/.gitconfig
[credential]
helper = store
```
-```sh
+```toml
# /home/coder/.git-credentials
# GitHub example:
https://your-github-username:your-github-pat@github.com
```
-If you are running Coder on Docker or Kubernetes, `git` is pre-installed in the Coder image. However, you still need to mount credentials. This can be done via a Docker volume mount or Kubernetes secrets.
+If you are running Coder on Docker or Kubernetes, `git` is pre-installed in the
+Coder image. However, you still need to mount credentials. This can be done via
+a Docker volume mount or Kubernetes secrets.
### Passing git credentials in Kubernetes
-First, create a `.gitconfig` and `.git-credentials` file on your local machine. You may want to do this in a temporary directory to avoid conflicting with your own git credentials.
+First, create a `.gitconfig` and `.git-credentials` file on your local machine.
+You may want to do this in a temporary directory to avoid conflicting with your
+own git credentials.
-Next, create the secret in Kubernetes. Be sure to do this in the same namespace that Coder is installed in.
+Next, create the secret in Kubernetes. Be sure to do this in the same namespace
+that Coder is installed in.
-```sh
+```shell
export NAMESPACE=coder
kubectl apply -f - <
@@ -9,13 +10,17 @@ Your browser does not support the video tag.
## How it works
-To support any infrastructure and software stack, Coder provides a generic approach for "Open in Coder" flows.
+To support any infrastructure and software stack, Coder provides a generic
+approach for "Open in Coder" flows.
-1. Set up [Git Authentication](../admin/git-providers.md#require-git-authentication-in-templates) in your Coder deployment
+1. Set up
+ [Git Authentication](../admin/git-providers.md#require-git-authentication-in-templates)
+ in your Coder deployment
1. Modify your template to auto-clone repos:
-> The id in the template's `coder_git_auth` data source must match the `CODER_GITAUTH_0_ID` in the Coder deployment configuration.
+> The id in the template's `coder_git_auth` data source must match the
+> `CODER_GITAUTH_0_ID` in the Coder deployment configuration.
- If you want the template to clone a specific git repo
@@ -46,7 +51,8 @@ To support any infrastructure and software stack, Coder provides a generic appro
> - `/home/coder/coder`
> - `coder` (relative to the home directory)
-- If you want the template to support any repository via [parameters](./parameters.md)
+- If you want the template to support any repository via
+ [parameters](./parameters.md)
```hcl
# Require git authentication to use this template
@@ -86,7 +92,9 @@ To support any infrastructure and software stack, Coder provides a generic appro
[](https://YOUR_ACCESS_URL/templates/YOUR_TEMPLATE/workspace)
```
- > Be sure to replace `YOUR_ACCESS_URL` with your Coder access url (e.g. https://coder.example.com) and `YOUR_TEMPLATE` with the name of your template.
+ > Be sure to replace `YOUR_ACCESS_URL` with your Coder access url (e.g.
+ > https://coder.example.com) and `YOUR_TEMPLATE` with the name of your
+ > template.
1. Optional: pre-fill parameter values in the "Create Workspace" page
@@ -100,8 +108,10 @@ To support any infrastructure and software stack, Coder provides a generic appro
## Example: Kubernetes
-For a full example of the Open in Coder flow in Kubernetes, check out [this example template](https://github.com/bpmct/coder-templates/tree/main/kubernetes-open-in-coder).
+For a full example of the Open in Coder flow in Kubernetes, check out
+[this example template](https://github.com/bpmct/coder-templates/tree/main/kubernetes-open-in-coder).
## Devcontainer support
-Devcontainer support is on the roadmap. [Follow along here](https://github.com/coder/coder/issues/5559)
+Devcontainer support is on the roadmap.
+[Follow along here](https://github.com/coder/coder/issues/5559)
diff --git a/docs/templates/parameters.md b/docs/templates/parameters.md
index c74413d48b392..9ed108367a805 100644
--- a/docs/templates/parameters.md
+++ b/docs/templates/parameters.md
@@ -1,6 +1,7 @@
# Parameters
-Templates can contain _parameters_, which allow prompting the user for additional information when creating workspaces in both the UI and CLI.
+Templates can contain _parameters_, which allow prompting the user for
+additional information when creating workspaces in both the UI and CLI.

@@ -45,12 +46,15 @@ provider "docker" {
## Types
-The following parameter types are supported: `string`, `list(string)`, `bool`, and `number`.
+The following parameter types are supported: `string`, `list(string)`, `bool`,
+and `number`.
### List of strings
-List of strings is a specific parameter type, that can't be easily mapped to the default value, which is string type.
-Parameters with the `list(string)` type must be converted to JSON arrays using [jsonencode](https://developer.hashicorp.com/terraform/language/functions/jsonencode)
+List of strings is a specific parameter type, that can't be easily mapped to the
+default value, which is string type. Parameters with the `list(string)` type
+must be converted to JSON arrays using
+[jsonencode](https://developer.hashicorp.com/terraform/language/functions/jsonencode)
function.
```hcl
@@ -99,9 +103,41 @@ data "coder_parameter" "docker_host" {
}
```
+### Incompatibility in Parameter Options for Workspace Builds
+
+When creating Coder templates, authors have the flexibility to modify parameter
+options associated with rich parameters. Such modifications can involve adding,
+substituting, or removing a parameter option. It's important to note that making
+these changes can lead to discrepancies in parameter values utilized by ongoing
+workspace builds.
+
+Consequently, workspace users will be prompted to select the new value from a
+pop-up window or by using the command-line interface. While this additional
+interactive step might seem like an interruption, it serves a crucial purpose.
+It prevents workspace users from becoming trapped with outdated template
+versions, ensuring they can smoothly update their workspace without any
+hindrances.
+
+Example:
+
+- Bob creates a workspace using the `python-dev` template. This template has a
+ parameter `image_tag`, and Bob selects `1.12`.
+- Later, the template author Alice is notified of a critical vulnerability in a
+ package installed in the `python-dev` template, which affects the image tag
+ `1.12`.
+- Alice remediates this vulnerability, and pushes an updated template version
+ that replaces option `1.12` with `1.13` for the `image_tag` parameter. She
+ then notifies all users of that template to update their workspace
+ immediately.
+- Bob saves their work, and selects the `Update` option in the UI. As their
+ workspace uses the now-invalid option `1.12`, for the `image_tag` parameter,
+ they are prompted to select a new value for `image_tag`.
+
## Required and optional parameters
-A parameter is considered to be _required_ if it doesn't have the `default` property. The user **must** provide a value to this parameter before creating a workspace.
+A parameter is considered to be _required_ if it doesn't have the `default`
+property. The user **must** provide a value to this parameter before creating a
+workspace.
```hcl
data "coder_parameter" "account_name" {
@@ -111,8 +147,8 @@ data "coder_parameter" "account_name" {
}
```
-If a parameter contains the `default` property, Coder will use this value
-if the user does not specify any:
+If a parameter contains the `default` property, Coder will use this value if the
+user does not specify any:
```hcl
data "coder_parameter" "base_image" {
@@ -122,7 +158,8 @@ data "coder_parameter" "base_image" {
}
```
-Admins can also set the `default` property to an empty value so that the parameter field can remain empty:
+Admins can also set the `default` property to an empty value so that the
+parameter field can remain empty:
```hcl
data "coder_parameter" "dotfiles_url" {
@@ -133,9 +170,30 @@ data "coder_parameter" "dotfiles_url" {
}
```
+Terraform
+[conditional expressions](https://developer.hashicorp.com/terraform/language/expressions/conditionals)
+can be used to determine whether the user specified a value for an optional
+parameter:
+
+```hcl
+resource "coder_agent" "main" {
+ # ...
+ startup_script_timeout = 180
+ startup_script = <<-EOT
+ set -e
+
+ echo "The optional parameter value is: ${data.coder_parameter.optional.value == "" ? "[empty]" : data.coder_parameter.optional.value}"
+
+ EOT
+}
+```
+
## Mutability
-Immutable parameters can be only set before workspace creation, or during update on the first usage to set the initial value for required parameters. The idea is to prevent users from modifying fragile or persistent workspace resources like volumes, regions, etc.:
+Immutable parameters can be only set before workspace creation, or during update
+on the first usage to set the initial value for required parameters. The idea is
+to prevent users from modifying fragile or persistent workspace resources like
+volumes, regions, etc.:
```hcl
data "coder_parameter" "region" {
@@ -146,16 +204,19 @@ data "coder_parameter" "region" {
}
```
-It is allowed to modify the mutability state anytime. In case of emergency, template authors can temporarily allow for changing immutable parameters to fix an operational issue, but it is not
-advised to overuse this opportunity.
+It is allowed to modify the mutability state anytime. In case of emergency,
+template authors can temporarily allow for changing immutable parameters to fix
+an operational issue, but it is not advised to overuse this opportunity.
## Ephemeral parameters
-Ephemeral parameters are introduced to users in the form of "build options." This functionality can be used to model
-specific behaviors within a Coder workspace, such as reverting to a previous image, restoring from a volume snapshot, or
-building a project without utilizing cache.
+Ephemeral parameters are introduced to users in the form of "build options."
+This functionality can be used to model specific behaviors within a Coder
+workspace, such as reverting to a previous image, restoring from a volume
+snapshot, or building a project without utilizing cache.
-As these parameters are ephemeral in nature, subsequent builds will proceed in the standard manner.
+As these parameters are ephemeral in nature, subsequent builds will proceed in
+the standard manner.
```hcl
data "coder_parameter" "force_rebuild" {
@@ -170,12 +231,15 @@ data "coder_parameter" "force_rebuild" {
## Validation
-Rich parameters support multiple validation modes - min, max, monotonic numbers, and regular expressions.
+Rich parameters support multiple validation modes - min, max, monotonic numbers,
+and regular expressions.
### Number
-A _number_ parameter can be limited to boundaries - min, max. Additionally, the monotonicity (`increasing` or `decreasing`) between the current parameter value and the new one can be verified too.
-Monotonicity can be enabled for resources that can't be shrunk without implications, for instance - disk volume size.
+A _number_ parameter can be limited to boundaries - min, max. Additionally, the
+monotonicity (`increasing` or `decreasing`) between the current parameter value
+and the new one can be verified too. Monotonicity can be enabled for resources
+that can't be shrunk without implications, for instance - disk volume size.
```hcl
data "coder_parameter" "instances" {
@@ -192,7 +256,9 @@ data "coder_parameter" "instances" {
### String
-A _string_ parameter can have a regular expression defined to make sure that the parameter value matches the pattern. The `regex` property requires a corresponding `error` property.
+A _string_ parameter can have a regular expression defined to make sure that the
+parameter value matches the pattern. The `regex` property requires a
+corresponding `error` property.
```hcl
data "coder_parameter" "project_id" {
@@ -209,21 +275,29 @@ data "coder_parameter" "project_id" {
### Legacy parameters are unsupported now
-In Coder, workspaces using legacy parameters can't be deployed anymore. To address this, it is necessary to either remove or adjust incompatible templates.
-In some cases, deleting a workspace with a hard dependency on a legacy parameter may be challenging. To cleanup unsupported workspaces, administrators are advised to take the following actions for affected templates:
+In Coder, workspaces using legacy parameters can't be deployed anymore. To
+address this, it is necessary to either remove or adjust incompatible templates.
+In some cases, deleting a workspace with a hard dependency on a legacy parameter
+may be challenging. To cleanup unsupported workspaces, administrators are
+advised to take the following actions for affected templates:
1. Enable the `feature_use_managed_variables` provider flag.
-2. Ensure that every legacy variable block has defined missing default values, or convert it to `coder_parameter`.
+2. Ensure that every legacy variable block has defined missing default values,
+ or convert it to `coder_parameter`.
3. Push the new template version using UI or CLI.
4. Update unsupported workspaces to the newest template version.
-5. Delete the affected workspaces that have been updated to the newest template version.
+5. Delete the affected workspaces that have been updated to the newest template
+ version.
### Migration
> ⚠️ Migration is available until v0.24.0 (Jun 2023) release.
-Terraform `variable` shouldn't be used for workspace scoped parameters anymore, and it's required to convert `variable` to `coder_parameter` resources. To make the migration smoother, there is a special property introduced -
-`legacy_variable` and `legacy_variable_name` , which can link `coder_parameter` with a legacy variable.
+Terraform `variable` shouldn't be used for workspace scoped parameters anymore,
+and it's required to convert `variable` to `coder_parameter` resources. To make
+the migration smoother, there is a special property introduced -
+`legacy_variable` and `legacy_variable_name` , which can link `coder_parameter`
+with a legacy variable.
```hcl
variable "legacy_cpu" {
@@ -248,33 +322,44 @@ data "coder_parameter" "cpu" {
1. Prepare and update a new template version:
- Add `coder_parameter` resource matching the legacy variable to migrate.
- - Use `legacy_variable_name` and `legacy_variable` to link the `coder_parameter` to the legacy variable.
- - Mark the new parameter as `mutable`, so that Coder will not block updating existing workspaces.
+ - Use `legacy_variable_name` and `legacy_variable` to link the
+ `coder_parameter` to the legacy variable.
+ - Mark the new parameter as `mutable`, so that Coder will not block updating
+ existing workspaces.
-2. Update all workspaces to the updated template version. Coder will populate the added `coder_parameter`s with values from legacy variables.
+2. Update all workspaces to the updated template version. Coder will populate
+ the added `coder_parameter`s with values from legacy variables.
3. Prepare another template version:
- Remove the migrated variables.
- - Remove properties `legacy_variable` and `legacy_variable_name` from `coder_parameter`s.
+ - Remove properties `legacy_variable` and `legacy_variable_name` from
+ `coder_parameter`s.
4. Update all workspaces to the updated template version (2nd).
5. Prepare a third template version:
- - Enable the `feature_use_managed_variables` provider flag to use managed Terraform variables for template customization. Once the flag is enabled, legacy variables won't be used.
+ - Enable the `feature_use_managed_variables` provider flag to use managed
+ Terraform variables for template customization. Once the flag is enabled,
+ legacy variables won't be used.
6. Update all workspaces to the updated template version (3rd).
7. Delete legacy parameters.
-As a template improvement, the template author can consider making some of the new `coder_parameter` resources `mutable`.
+As a template improvement, the template author can consider making some of the
+new `coder_parameter` resources `mutable`.
## Terraform template-wide variables
-> ⚠️ Flag `feature_use_managed_variables` is available until v0.25.0 (Jul 2023) release. After this release, template-wide Terraform variables will be enabled by default.
+> ⚠️ Flag `feature_use_managed_variables` is available until v0.25.0 (Jul 2023)
+> release. After this release, template-wide Terraform variables will be enabled
+> by default.
-As parameters are intended to be used only for workspace customization purposes, Terraform variables can be freely managed by the template author to build templates. Workspace users are not able to modify
-template variables.
+As parameters are intended to be used only for workspace customization purposes,
+Terraform variables can be freely managed by the template author to build
+templates. Workspace users are not able to modify template variables.
-The template author can enable Terraform template-wide variables mode by specifying the following flag:
+The template author can enable Terraform template-wide variables mode by
+specifying the following flag:
```hcl
provider "coder" {
@@ -282,4 +367,5 @@ provider "coder" {
}
```
-Once it's defined, coder will allow for modifying variables by using CLI and UI forms, but it will not be possible to use legacy parameters.
+Once it's defined, coder will allow for modifying variables by using CLI and UI
+forms, but it will not be possible to use legacy parameters.
diff --git a/docs/templates/process-logging.md b/docs/templates/process-logging.md
new file mode 100644
index 0000000000000..51bf613238a44
--- /dev/null
+++ b/docs/templates/process-logging.md
@@ -0,0 +1,315 @@
+# Workspace Process Logging
+
+The workspace process logging feature allows you to log all system-level
+processes executing in the workspace.
+
+> **Note:** This feature is only available on Linux in Kubernetes. There are
+> additional requirements outlined further in this document.
+
+Workspace process logging adds a sidecar container to workspace pods that will
+log all processes started in the workspace container (e.g., commands executed in
+the terminal or processes created in the background by other processes).
+Processes launched inside containers or nested containers within the workspace
+are also logged. You can view the output from the sidecar or send it to a
+monitoring stack, such as CloudWatch, for further analysis or long-term storage.
+
+Please note that these logs are not recorded or captured by the Coder
+organization in any way, shape, or form.
+
+> This is an [Enterprise](https://coder.com/docs/v2/latest/enterprise) feature.
+> To learn more about Coder Enterprise, please
+> [contact sales](https://coder.com/contact).
+
+## How this works
+
+Coder uses [eBPF](https://ebpf.io/) (which we chose for its minimal performance
+impact) to perform in-kernel logging and filtering of all exec system calls
+originating from the workspace container.
+
+The core of this feature is also open source and can be found in the
+[exectrace](https://github.com/coder/exectrace) GitHub repo. The enterprise
+component (in the `enterprise/` directory of the repo) is responsible for
+starting the eBPF program with the correct filtering options for the specific
+workspace.
+
+## Requirements
+
+The host machine must be running a Linux kernel >= 5.8 with the kernel config
+`CONFIG_DEBUG_INFO_BTF=y` enabled.
+
+To check your kernel version, run:
+
+```shell
+uname -r
+```
+
+To validate the required kernel config is enabled, run either of the following
+commands on your nodes directly (_not_ from a workspace terminal):
+
+```shell
+cat /proc/config.gz | gunzip | grep CONFIG_DEBUG_INFO_BTF
+```
+
+```shell
+cat "/boot/config-$(uname -r)" | grep CONFIG_DEBUG_INFO_BTF
+```
+
+If these requirements are not met, workspaces will fail to start for security
+reasons.
+
+Your template must be a Kubernetes template. Workspace process logging is not
+compatible with the `sysbox-runc` runtime due to technical limitations, but it
+is compatible with our `envbox` template family.
+
+## Example templates
+
+We provide working example templates for Kubernetes, and Kubernetes with
+`envbox` (for [Docker support in workspaces](./docker-in-workspaces.md)). You
+can view these templates in the
+[exectrace repo](https://github.com/coder/exectrace/tree/main/enterprise/templates).
+
+## Configuring custom templates to use workspace process logging
+
+If you have an existing Kubernetes or Kubernetes with `envbox` template that you
+would like to add workspace process logging to, follow these steps:
+
+1. Ensure the image used in your template has `curl` installed.
+
+1. Add the following section to your template's `main.tf` file:
+
+
+
+ ```hcl
+ locals {
+ # This is the init script for the main workspace container that runs before the
+ # agent starts to configure workspace process logging.
+ exectrace_init_script = </dev/null 2>&1; then
+ echo "curl is required to download the Coder binary"
+ echo "Please install curl to your image and try again"
+ # 127 is command not found.
+ exit 127
+ fi
+
+ echo "Sending process ID namespace inum to exectrace sidecar"
+ rc=0
+ max_retry=5
+ counter=0
+ until [ $counter -ge $max_retry ]; do
+ set +e
+ curl \
+ --fail \
+ --silent \
+ --connect-timeout 5 \
+ -X POST \
+ -H "Content-Type: text/plain" \
+ --data "$pidns_inum" \
+ http://127.0.0.1:56123
+ rc=$?
+ set -e
+ if [ $rc -eq 0 ]; then
+ break
+ fi
+
+ counter=$((counter+1))
+ echo "Curl failed with exit code $${rc}, attempt $${counter}/$${max_retry}; Retrying in 3 seconds..."
+ sleep 3
+ done
+ if [ $rc -ne 0 ]; then
+ echo "Failed to send process ID namespace inum to exectrace sidecar"
+ exit $rc
+ fi
+
+ EOT
+ }
+ ```
+
+1. Update the `command` of your workspace container like the following:
+
+
+
+ ```hcl
+ resource "kubernetes_pod" "main" {
+ ...
+ spec {
+ ...
+ container {
+ ...
+ // NOTE: this command is changed compared to the upstream kubernetes
+ // template
+ command = [
+ "sh",
+ "-c",
+ "${local.exectrace_init_script}\n\n${coder_agent.main.init_script}",
+ ]
+ ...
+ }
+ ...
+ }
+ ...
+ }
+ ```
+
+ > **Note:** If you are using the `envbox` template, you will need to update
+ > the third argument to be
+ > `"${local.exectrace_init_script}\n\nexec /envbox docker"` instead.
+
+1. Add the following container to your workspace pod spec.
+
+
+
+ ```hcl
+ resource "kubernetes_pod" "main" {
+ ...
+ spec {
+ ...
+ // NOTE: this container is added compared to the upstream kubernetes
+ // template
+ container {
+ name = "exectrace"
+ image = "ghcr.io/coder/exectrace:latest"
+ image_pull_policy = "Always"
+ command = [
+ "/opt/exectrace",
+ "--init-address", "127.0.0.1:56123",
+ "--label", "workspace_id=${data.coder_workspace.me.id}",
+ "--label", "workspace_name=${data.coder_workspace.me.name}",
+ "--label", "user_id=${data.coder_workspace.me.owner_id}",
+ "--label", "username=${data.coder_workspace.me.owner}",
+ "--label", "user_email=${data.coder_workspace.me.owner_email}",
+ ]
+ security_context {
+ // exectrace must be started as root so it can attach probes into the
+ // kernel to record process events with high throughput.
+ run_as_user = "0"
+ run_as_group = "0"
+ // exectrace requires a privileged container so it can control mounts
+ // and perform privileged syscalls against the host kernel to attach
+ // probes.
+ privileged = true
+ }
+ }
+ ...
+ }
+ ...
+ }
+ ```
+
+ > **Note:** `exectrace` requires root privileges and a privileged container
+ > to attach probes to the kernel. This is a requirement of eBPF.
+
+1. Add the following environment variable to your workspace pod:
+
+
+
+ ```hcl
+ resource "kubernetes_pod" "main" {
+ ...
+ spec {
+ ...
+ env {
+ name = "CODER_AGENT_SUBSYSTEM"
+ value = "exectrace"
+ }
+ ...
+ }
+ ...
+ }
+ ```
+
+Once you have made these changes, you can push a new version of your template
+and workspace process logging will be enabled for all workspaces once they are
+restarted.
+
+## Viewing workspace process logs
+
+To view the process logs for a specific workspace you can use `kubectl` to print
+the logs:
+
+```bash
+kubectl logs pod-name --container exectrace
+```
+
+The raw logs will look something like this:
+
+```json
+{
+ "ts": "2022-02-28T20:29:38.038452202Z",
+ "level": "INFO",
+ "msg": "exec",
+ "fields": {
+ "labels": {
+ "user_email": "jessie@coder.com",
+ "user_id": "5e876e9a-121663f01ebd1522060d5270",
+ "username": "jessie",
+ "workspace_id": "621d2e52-a6987ef6c56210058ee2593c",
+ "workspace_name": "main"
+ },
+ "cmdline": "uname -a",
+ "event": {
+ "filename": "/usr/bin/uname",
+ "argv": ["uname", "-a"],
+ "truncated": false,
+ "pid": 920684,
+ "uid": 101000,
+ "gid": 101000,
+ "comm": "bash"
+ }
+ }
+}
+```
+
+### View logs in AWS EKS
+
+If you're using AWS' Elastic Kubernetes Service, you can
+[configure your cluster](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Container-Insights-EKS-logs.html)
+to send logs to CloudWatch. This allows you to view the logs for a specific user
+or workspace.
+
+To view your logs, go to the CloudWatch dashboard (which is available on the
+**Log Insights** tab) and run a query similar to the following:
+
+```text
+fields @timestamp, log_processed.fields.cmdline
+| sort @timestamp asc
+| filter kubernetes.container_name="exectrace"
+| filter log_processed.fields.labels.username="zac"
+| filter log_processed.fields.labels.workspace_name="code"
+```
+
+## Usage considerations
+
+- The sidecar attached to each workspace is a privileged container, so you may
+ need to review your organization's security policies before enabling this
+ feature. Enabling workspace process logging does _not_ grant extra privileges
+ to the workspace container itself, however.
+- `exectrace` will log processes from nested Docker containers (including deeply
+ nested containers) correctly, but Coder does not distinguish between processes
+ started in the workspace and processes started in a child container in the
+ logs.
+- With `envbox` workspaces, this feature will detect and log startup processes
+ begun in the outer container (including container initialization processes).
+- Because this feature logs **all** processes in the workspace, high levels of
+ usage (e.g., during a `make` run) will result in an abundance of output in the
+ sidecar container. Depending on how your Kubernetes cluster is configured, you
+ may incur extra charges from your cloud provider to store the additional logs.
diff --git a/docs/templates/resource-metadata.md b/docs/templates/resource-metadata.md
index ef267cdf33113..52e96aeda073a 100644
--- a/docs/templates/resource-metadata.md
+++ b/docs/templates/resource-metadata.md
@@ -1,6 +1,8 @@
# Resource Metadata
-Expose key workspace information to your users via [`coder_metadata`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/metadata) resources in your template code.
+Expose key workspace information to your users via
+[`coder_metadata`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/metadata)
+resources in your template code.

@@ -19,8 +21,8 @@ and any other Terraform resource attribute.
## Example
-Expose the disk size, deployment name, and persistent
-directory in a Kubernetes template with:
+Expose the disk size, deployment name, and persistent directory in a Kubernetes
+template with:
```hcl
resource "kubernetes_persistent_volume_claim" "root" {
@@ -57,7 +59,8 @@ resource "coder_metadata" "deployment" {
## Hiding resources in the UI
-Some resources don't need to be exposed in the UI; this helps keep the workspace view clean for developers. To hide a resource, use the `hide` attribute:
+Some resources don't need to be exposed in the UI; this helps keep the workspace
+view clean for developers. To hide a resource, use the `hide` attribute:
```hcl
resource "coder_metadata" "hide_serviceaccount" {
@@ -73,7 +76,8 @@ resource "coder_metadata" "hide_serviceaccount" {
## Using custom resource icon
-To use custom icons on your resources, use the `icon` attribute (must be a valid path or URL):
+To use custom icons on your resources, use the `icon` attribute (must be a valid
+path or URL):
```hcl
resource "coder_metadata" "resource_with_icon" {
@@ -95,7 +99,8 @@ To make easier for you to customize your resource we added some built-in icons:
- Widgets `/icon/widgets.svg`
- Database `/icon/database.svg`
-We also have other icons related to the IDEs. You can see all the icons [here](https://github.com/coder/coder/tree/main/site/static/icon).
+We also have other icons related to the IDEs. You can see all the icons
+[here](https://github.com/coder/coder/tree/main/site/static/icon).
## Agent Metadata
diff --git a/docs/templates/resource-persistence.md b/docs/templates/resource-persistence.md
index 97233460f3fdd..f532369a21e9b 100644
--- a/docs/templates/resource-persistence.md
+++ b/docs/templates/resource-persistence.md
@@ -1,22 +1,23 @@
# Resource Persistence
-Coder templates have full control over workspace ephemerality. In a
-completely ephemeral workspace, there are zero resources in the Off state. In
-a completely persistent workspace, there is no difference between the Off and
-On states.
+Coder templates have full control over workspace ephemerality. In a completely
+ephemeral workspace, there are zero resources in the Off state. In a completely
+persistent workspace, there is no difference between the Off and On states.
-Most workspaces fall somewhere in the middle, persisting user data
-such as filesystem volumes, but deleting expensive, reproducible resources
-such as compute instances.
+Most workspaces fall somewhere in the middle, persisting user data such as
+filesystem volumes, but deleting expensive, reproducible resources such as
+compute instances.
-By default, all Coder resources are persistent, but
-production templates **must** employ the practices laid out in this document
-to prevent accidental deletion.
+By default, all Coder resources are persistent, but production templates
+**must** employ the practices laid out in this document to prevent accidental
+deletion.
## Disabling Persistence
-The [`coder_workspace` data source](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace) exposes the `start_count = [0 | 1]` attribute that other
-resources reference to become ephemeral.
+The
+[`coder_workspace` data source](https://registry.terraform.io/providers/coder/coder/latest/docs/data-sources/workspace)
+exposes the `start_count = [0 | 1]` attribute that other resources reference to
+become ephemeral.
For example:
@@ -45,8 +46,8 @@ resource "docker_volume" "home_volume" {
```
Because we depend on `coder_workspace.me.owner`, if the owner changes their
-username, Terraform would recreate the volume (wiping its data!) the next
-time the workspace restarts.
+username, Terraform would recreate the volume (wiping its data!) the next time
+the workspace restarts.
Therefore, persistent resource names must only depend on immutable IDs such as:
@@ -67,9 +68,12 @@ resource "docker_volume" "home_volume" {
## 🛡 Bulletproofing
Even if our persistent resource depends exclusively on static IDs, a change to
-the `name` format or other attributes would cause Terraform to rebuild the resource.
+the `name` format or other attributes would cause Terraform to rebuild the
+resource.
-Prevent Terraform from recreating the resource under any circumstance by setting the [`ignore_changes = all` directive in the `lifecycle` block](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#ignore_changes).
+Prevent Terraform from recreating the resource under any circumstance by setting
+the
+[`ignore_changes = all` directive in the `lifecycle` block](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#ignore_changes).
```hcl
data "coder_workspace" "me" {
diff --git a/docs/workspaces.md b/docs/workspaces.md
index 757f0b02d75bc..9d1c6d1766fa6 100644
--- a/docs/workspaces.md
+++ b/docs/workspaces.md
@@ -5,9 +5,10 @@ for software development.
## Create workspaces
-Each Coder user has their own workspaces created from [shared templates](./templates/index.md):
+Each Coder user has their own workspaces created from
+[shared templates](./templates/index.md):
-```console
+```shell
# create a workspace from the template; specify any variables
coder create --template=""
@@ -22,15 +23,17 @@ Coder [supports multiple IDEs](./ides.md) for use with your workspaces.
## Workspace lifecycle
Workspaces in Coder are started and stopped, often based on whether there was
-any activity or if there was a [template update](./templates/index.md#Start/stop) available.
+any activity or if there was a
+[template update](./templates/index.md#Start/stop) available.
Resources are often destroyed and re-created when a workspace is restarted,
-though the exact behavior depends on the template. For more
-information, see [Resource Persistence](./templates/resource-persistence.md).
+though the exact behavior depends on the template. For more information, see
+[Resource Persistence](./templates/resource-persistence.md).
> ⚠️ To avoid data loss, refer to your template documentation for information on
> where to store files, install software, etc., so that they persist. Default
-> templates are documented in [../examples/templates](https://github.com/coder/coder/tree/c6b1daabc5a7aa67bfbb6c89966d728919ba7f80/examples/templates).
+> templates are documented in
+> [../examples/templates](https://github.com/coder/coder/tree/c6b1daabc5a7aa67bfbb6c89966d728919ba7f80/examples/templates).
>
> You can use `coder show ` to see which resources are
> persistent and which are ephemeral.
@@ -39,49 +42,51 @@ When a workspace is deleted, all of the workspace's resources are deleted.
## Workspace scheduling
-By default, workspaces are manually turned on/off by the user. However, a schedule
-can be defined on a per-workspace basis to automate the workspace start/stop.
+By default, workspaces are manually turned on/off by the user. However, a
+schedule can be defined on a per-workspace basis to automate the workspace
+start/stop.

### Autostart
-The autostart feature automates the workspace build at a user-specified time
-and day(s) of the week. In addition, users can select their preferred timezone.
+The autostart feature automates the workspace build at a user-specified time and
+day(s) of the week. In addition, users can select their preferred timezone.

### Autostop
-The autostop feature shuts off workspaces after given number of hours in the "on"
-state. If Coder detects workspace connection activity, the autostop timer is bumped up
-one hour. IDE, SSH, Port Forwarding, and coder_app activity trigger this bump.
+The autostop feature shuts off workspaces after given number of hours in the
+"on" state. If Coder detects workspace connection activity, the autostop timer
+is bumped up one hour. IDE, SSH, Port Forwarding, and coder_app activity trigger
+this bump.

### Max lifetime
Max lifetime is a template-level setting that determines the number of hours a
-workspace can run before it is automatically shutdown, regardless of any
-active connections. This setting ensures workspaces do not run in perpetuity
-when connections are left open inadvertently.
+workspace can run before it is automatically shutdown, regardless of any active
+connections. This setting ensures workspaces do not run in perpetuity when
+connections are left open inadvertently.
## Updating workspaces
Use the following command to update a workspace to the latest template version.
The workspace will be stopped and started:
-```console
+```shell
coder update
```
## Repairing workspaces
-Use the following command to re-enter template input
-variables in an existing workspace. This command is useful when a workspace fails
-to build because its state is out of sync with the template.
+Use the following command to re-enter template input variables in an existing
+workspace. This command is useful when a workspace fails to build because its
+state is out of sync with the template.
-```console
+```shell
coder update --always-prompt
```
@@ -99,16 +104,22 @@ Coder stores macOS and Linux logs at the following locations:
## Workspace filtering
-In the Coder UI, you can filter your workspaces using pre-defined filters or employing the Coder's filter query. Take a look at the following examples to understand how to use the Coder's filter query:
+In the Coder UI, you can filter your workspaces using pre-defined filters or
+employing the Coder's filter query. Take a look at the following examples to
+understand how to use the Coder's filter query:
- To find the workspaces that you own, use the filter `owner:me`.
-- To find workspaces that are currently running, use the filter `status:running`.
+- To find workspaces that are currently running, use the filter
+ `status:running`.
The following filters are supported:
-- `owner` - Represents the `username` of the owner. You can also use `me` as a convenient alias for the logged-in user.
+- `owner` - Represents the `username` of the owner. You can also use `me` as a
+ convenient alias for the logged-in user.
- `template` - Specifies the name of the template.
-- `status` - Indicates the status of the workspace. For a list of supported statuses, please refer to the [WorkspaceStatus documentation](https://pkg.go.dev/github.com/coder/coder/codersdk#WorkspaceStatus).
+- `status` - Indicates the status of the workspace. For a list of supported
+ statuses, please refer to the
+ [WorkspaceStatus documentation](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#WorkspaceStatus).
---
diff --git a/dogfood/Dockerfile b/dogfood/Dockerfile
deleted file mode 100644
index c5f1481679147..0000000000000
--- a/dogfood/Dockerfile
+++ /dev/null
@@ -1,347 +0,0 @@
-FROM rust:slim AS rust-utils
-# Install rust helper programs
-# ENV CARGO_NET_GIT_FETCH_WITH_CLI=true
-ENV CARGO_INSTALL_ROOT=/tmp/
-RUN cargo install exa bat ripgrep typos-cli watchexec-cli
-
-FROM ubuntu:jammy AS go
-
-RUN apt-get update && apt-get install --yes curl gcc
-# Install Go manually, so that we can control the version
-ARG GO_VERSION=1.20.6
-RUN mkdir --parents /usr/local/go
-
-# Boring Go is needed to build FIPS-compliant binaries.
-RUN curl --silent --show-error --location \
- "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
- -o /usr/local/go.tar.gz
-
-RUN tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1
-
-ENV PATH=$PATH:/usr/local/go/bin
-
-# Install Go utilities.
-ARG GOPATH="/tmp/"
-RUN mkdir --parents "$GOPATH" && \
- # moq for Go tests.
- go install github.com/matryer/moq@v0.2.3 && \
- # swag for Swagger doc generation
- go install github.com/swaggo/swag/cmd/swag@v1.7.4 && \
- # go-swagger tool to generate the go coder api client
- go install github.com/go-swagger/go-swagger/cmd/swagger@v0.28.0 && \
- # goimports for updating imports
- go install golang.org/x/tools/cmd/goimports@v0.1.7 && \
- # protoc-gen-go is needed to build sysbox from source
- go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 && \
- # drpc support for v2
- go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33 && \
- # migrate for migration support for v2
- go install github.com/golang-migrate/migrate/v4/cmd/migrate@v4.15.1 && \
- # goreleaser for compiling v2 binaries
- go install github.com/goreleaser/goreleaser@v1.6.1 && \
- # Install the latest version of gopls for editors that support
- # the language server protocol
- go install golang.org/x/tools/gopls@latest && \
- # gotestsum makes test output more readable
- go install gotest.tools/gotestsum@v1.9.0 && \
- # goveralls collects code coverage metrics from tests
- # and sends to Coveralls
- go install github.com/mattn/goveralls@v0.0.11 && \
- # kind for running Kubernetes-in-Docker, needed for tests
- go install sigs.k8s.io/kind@v0.10.0 && \
- # helm-docs generates our Helm README based on a template and the
- # charts and values files
- go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.5.0 && \
- # sqlc for Go code generation
- go install github.com/kyleconroy/sqlc/cmd/sqlc@v1.19.1 && \
- # gcr-cleaner-cli used by CI to prune unused images
- go install github.com/sethvargo/gcr-cleaner/cmd/gcr-cleaner-cli@v0.5.1 && \
- # ruleguard for checking custom rules, without needing to run all of
- # golangci-lint. Check the go.mod in the release of golangci-lint that
- # we're using for the version of go-critic that it embeds, then check
- # the version of ruleguard in go-critic for that tag.
- go install github.com/quasilyte/go-ruleguard/cmd/ruleguard@v0.3.13 && \
- # go-fuzz for fuzzy testing. they don't publish releases so we rely on latest.
- go install github.com/dvyukov/go-fuzz/go-fuzz@latest && \
- go install github.com/dvyukov/go-fuzz/go-fuzz-build@latest && \
- # go-releaser for building 'fat binaries' that work cross-platform
- go install github.com/goreleaser/goreleaser@v1.6.1 && \
- go install mvdan.cc/sh/v3/cmd/shfmt@latest && \
- # nfpm is used with `make build` to make release packages
- go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.16.0 && \
- # yq v4 is used to process yaml files in coder v2. Conflicts with
- # yq v3 used in v1.
- go install github.com/mikefarah/yq/v4@v4.30.6 && \
- mv /tmp/bin/yq /tmp/bin/yq4 && \
- go install github.com/golang/mock/mockgen@v1.6.0
-
-FROM gcr.io/coder-dev-1/alpine:3.18 as proto
-WORKDIR /tmp
-RUN apk add curl unzip
-RUN curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip
-RUN unzip protoc.zip
-
-FROM ubuntu:jammy
-
-SHELL ["/bin/bash", "-c"]
-
-# Updated certificates are necessary to use the teraswitch mirror.
-# This must be ran before copying in configuration since the config replaces
-# the default mirror with teraswitch.
-RUN apt-get update && apt-get install --yes ca-certificates
-
-COPY files /
-
-# Install packages from apt repositories
-ARG DEBIAN_FRONTEND="noninteractive"
-
-RUN apt-get update --quiet && apt-get install --yes \
- apt-transport-https \
- apt-utils \
- bash \
- bash-completion \
- bats \
- bind9-dnsutils \
- build-essential \
- ca-certificates \
- cmake \
- crypto-policies \
- curl \
- fd-find \
- file \
- git \
- gnupg \
- graphviz \
- htop \
- httpie \
- inetutils-tools \
- iproute2 \
- iputils-ping \
- iputils-tracepath \
- jq \
- language-pack-en \
- less \
- lsb-release \
- man \
- meld \
- net-tools \
- openjdk-11-jdk-headless \
- openssh-server \
- openssl \
- libssl-dev \
- pkg-config \
- python3 \
- python3-pip \
- rsync \
- shellcheck \
- strace \
- sudo \
- tcptraceroute \
- termshark \
- traceroute \
- vim \
- wget \
- xauth \
- zip \
- ncdu \
- cargo \
- asciinema \
- zsh \
- ansible \
- neovim \
- google-cloud-sdk \
- google-cloud-sdk-datastore-emulator \
- kubectl \
- postgresql-13 \
- containerd.io \
- docker-ce \
- docker-ce-cli \
- docker-compose-plugin \
- packer \
- terraform \
- fish \
- unzip \
- zstd \
- gettext-base && \
- # Delete package cache to avoid consuming space in layer
- apt-get clean && \
- # Configure FIPS-compliant policies
- update-crypto-policies --set FIPS
-
-# Install the docker buildx component.
-RUN DOCKER_BUILDX_VERSION=$(curl -s "https://api.github.com/repos/docker/buildx/releases/latest" | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\1/') && \
- mkdir -p /usr/local/lib/docker/cli-plugins && \
- curl -Lo /usr/local/lib/docker/cli-plugins/docker-buildx "https://github.com/docker/buildx/releases/download/${DOCKER_BUILDX_VERSION}/buildx-${DOCKER_BUILDX_VERSION}.linux-amd64" && \
- chmod a+x /usr/local/lib/docker/cli-plugins/docker-buildx
-
-# See https://github.com/cli/cli/issues/6175#issuecomment-1235984381 for proof
-# the apt repository is unreliable
-RUN GH_CLI_VERSION=$(curl -s "https://api.github.com/repos/cli/cli/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') && \
- curl -L https://github.com/cli/cli/releases/download/v${GH_CLI_VERSION}/gh_${GH_CLI_VERSION}_linux_amd64.deb -o gh.deb && \
- dpkg -i gh.deb && \
- rm gh.deb
-
-# Install Lazygit
-# See https://github.com/jesseduffield/lazygit#ubuntu
-RUN LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v*([^"]+)".*/\1/') && \
- curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" && \
- tar xf lazygit.tar.gz -C /usr/local/bin lazygit
-
-# Install frontend utilities
-RUN apt-get update && \
- # Node.js (from nodesource) and Yarn (from yarnpkg)
- curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - &&\
- apt-get install --yes --quiet \
- nodejs yarn \
- # Install browsers for e2e testing
- google-chrome-stable microsoft-edge-beta && \
- # Pre-install system dependencies that Playwright needs. npx doesn't work here
- # for some reason. See https://github.com/microsoft/playwright-cli/issues/136
- npm i -g playwright@1.36.2 pnpm@^8 && playwright install-deps && \
- npm cache clean --force
-
-# Ensure PostgreSQL binaries are in the users $PATH.
-RUN update-alternatives --install /usr/local/bin/initdb initdb /usr/lib/postgresql/13/bin/initdb 100 && \
- update-alternatives --install /usr/local/bin/postgres postgres /usr/lib/postgresql/13/bin/postgres 100
-
-# Create links for injected dependencies
-RUN ln --symbolic /var/tmp/coder/coder-cli/coder /usr/local/bin/coder && \
- ln --symbolic /var/tmp/coder/code-server/bin/code-server /usr/local/bin/code-server
-
-# Disable the PostgreSQL systemd service.
-# Coder uses a custom timescale container to test the database instead.
-RUN systemctl disable \
- postgresql
-
-# Configure systemd services for CVMs
-RUN systemctl enable \
- docker \
- ssh
-
-# Install tools with published releases, where that is the
-# preferred/recommended installation method.
-ARG CLOUD_SQL_PROXY_VERSION=2.2.0 \
- DIVE_VERSION=0.10.0 \
- DOCKER_GCR_VERSION=2.1.8 \
- GOLANGCI_LINT_VERSION=1.52.2 \
- GRYPE_VERSION=0.61.1 \
- HELM_VERSION=3.12.0 \
- KUBE_LINTER_VERSION=0.6.3 \
- KUBECTX_VERSION=0.9.4 \
- STRIPE_VERSION=1.14.5 \
- TERRAGRUNT_VERSION=0.45.11 \
- TRIVY_VERSION=0.41.0
-
-# cloud_sql_proxy, for connecting to cloudsql instances
-# the upstream go.mod prevents this from being installed with go install
-RUN curl --silent --show-error --location --output /usr/local/bin/cloud_sql_proxy "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v${CLOUD_SQL_PROXY_VERSION}/cloud-sql-proxy.linux.amd64" && \
- chmod a=rx /usr/local/bin/cloud_sql_proxy && \
- # dive for scanning image layer utilization metrics in CI
- curl --silent --show-error --location "https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/bin --file=- dive && \
- # docker-credential-gcr is a Docker credential helper for pushing/pulling
- # images from Google Container Registry and Artifact Registry
- curl --silent --show-error --location "https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases/download/v${DOCKER_GCR_VERSION}/docker-credential-gcr_linux_amd64-${DOCKER_GCR_VERSION}.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/bin --file=- docker-credential-gcr && \
- # golangci-lint performs static code analysis for our Go code
- curl --silent --show-error --location "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/bin --file=- --strip-components=1 "golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64/golangci-lint" && \
- # Anchore Grype for scanning container images for security issues
- curl --silent --show-error --location "https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}/grype_${GRYPE_VERSION}_linux_amd64.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/bin --file=- grype && \
- # Helm is necessary for deploying Coder
- curl --silent --show-error --location "https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/bin --file=- --strip-components=1 linux-amd64/helm && \
- # kube-linter for linting Kubernetes objects, including those
- # that Helm generates from our charts
- curl --silent --show-error --location "https://github.com/stackrox/kube-linter/releases/download/${KUBE_LINTER_VERSION}/kube-linter-linux" --output /usr/local/bin/kube-linter && \
- # kubens and kubectx for managing Kubernetes namespaces and contexts
- curl --silent --show-error --location "https://github.com/ahmetb/kubectx/releases/download/v${KUBECTX_VERSION}/kubectx_v${KUBECTX_VERSION}_linux_x86_64.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/bin --file=- kubectx && \
- curl --silent --show-error --location "https://github.com/ahmetb/kubectx/releases/download/v${KUBECTX_VERSION}/kubens_v${KUBECTX_VERSION}_linux_x86_64.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/bin --file=- kubens && \
- # stripe for coder.com billing API
- curl --silent --show-error --location "https://github.com/stripe/stripe-cli/releases/download/v${STRIPE_VERSION}/stripe_${STRIPE_VERSION}_linux_x86_64.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/bin --file=- stripe && \
- # terragrunt for running Terraform and Terragrunt files
- curl --silent --show-error --location --output /usr/local/bin/terragrunt "https://github.com/gruntwork-io/terragrunt/releases/download/v${TERRAGRUNT_VERSION}/terragrunt_linux_amd64" && \
- chmod a=rx /usr/local/bin/terragrunt && \
- # AquaSec Trivy for scanning container images for security issues
- curl --silent --show-error --location "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/bin --file=- trivy
-
-# Add Vercel globally. We can't install it in packages.json, because it
-# includes Go files which make golangci-lint unhappy.
-RUN yarn global add --prefix=/usr/local \
- vercel \
- typescript \
- typescript-language-server \
- prettier && \
- yarn cache clean
-
-# We use yq during "make deploy" to manually substitute out fields in
-# our helm values.yaml file. See https://github.com/helm/helm/issues/3141
-#
-# TODO: update to 4.x, we can't do this now because it included breaking
-# changes (yq w doesn't work anymore)
-# RUN curl --silent --show-error --location "https://github.com/mikefarah/yq/releases/download/v4.9.0/yq_linux_amd64.tar.gz" | \
-# tar --extract --gzip --directory=/usr/local/bin --file=- ./yq_linux_amd64 && \
-# mv /usr/local/bin/yq_linux_amd64 /usr/local/bin/yq
-
-RUN curl --silent --show-error --location --output /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/3.3.0/yq_linux_amd64" && \
- chmod a=rx /usr/local/bin/yq
-
-# Install GoLand.
-RUN mkdir --parents /usr/local/goland && \
- curl --silent --show-error --location "https://download.jetbrains.com/go/goland-2021.2.tar.gz" | \
- tar --extract --gzip --directory=/usr/local/goland --file=- --strip-components=1 && \
- ln --symbolic /usr/local/goland/bin/goland.sh /usr/local/bin/goland
-
-# Install Antlrv4, needed to generate paramlang lexer/parser
-RUN curl --silent --show-error --location --output /usr/local/lib/antlr-4.9.2-complete.jar "https://www.antlr.org/download/antlr-4.9.2-complete.jar"
-ENV CLASSPATH="/usr/local/lib/antlr-4.9.2-complete.jar:${PATH}"
-
-# Add coder user and allow use of docker/sudo
-RUN useradd coder \
- --create-home \
- --shell=/bin/bash \
- --groups=docker \
- --uid=1000 \
- --user-group
-
-# Adjust OpenSSH config
-RUN echo "PermitUserEnvironment yes" >>/etc/ssh/sshd_config && \
- echo "X11Forwarding yes" >>/etc/ssh/sshd_config && \
- echo "X11UseLocalhost no" >>/etc/ssh/sshd_config
-
-# We avoid copying the extracted directory since COPY slows to minutes when there
-# are a lot of small files.
-COPY --from=go /usr/local/go.tar.gz /usr/local/go.tar.gz
-RUN mkdir /usr/local/go && \
- tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1
-
-ENV PATH=$PATH:/usr/local/go/bin
-
-RUN update-alternatives --install /usr/local/bin/gofmt gofmt /usr/local/go/bin/gofmt 100
-
-COPY --from=go /tmp/bin /usr/local/bin
-COPY --from=rust-utils /tmp/bin /usr/local/bin
-COPY --from=proto /tmp/bin /usr/local/bin
-COPY --from=proto /tmp/include /usr/local/bin/include
-
-USER coder
-
-# Ensure go bins are in the 'coder' user's path. Note that no go bins are
-# installed in this docker file, as they'd be mounted over by the persistent
-# home volume.
-ENV PATH="/home/coder/go/bin:${PATH}"
-
-# This setting prevents Go from using the public checksum database for
-# our module path prefixes. It is required because these are in private
-# repositories that require authentication.
-#
-# For details, see: https://golang.org/ref/mod#private-modules
-ENV GOPRIVATE="coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder"
-
-# Increase memory allocation to NodeJS
-ENV NODE_OPTIONS="--max-old-space-size=8192"
diff --git a/dogfood/Makefile b/dogfood/Makefile
deleted file mode 100644
index 061530f50dd45..0000000000000
--- a/dogfood/Makefile
+++ /dev/null
@@ -1,10 +0,0 @@
-.PHONY: docker-build docker-push
-
-branch=$(shell git rev-parse --abbrev-ref HEAD)
-build_tag=codercom/oss-dogfood:${branch}
-
-build:
- DOCKER_BUILDKIT=1 docker build . -t ${build_tag}
-
-push: build
- docker push ${build_tag}
diff --git a/dogfood/files/etc/apt/apt.conf.d/80-no-recommends b/dogfood/files/etc/apt/apt.conf.d/80-no-recommends
deleted file mode 100644
index 8cb79c96386c4..0000000000000
--- a/dogfood/files/etc/apt/apt.conf.d/80-no-recommends
+++ /dev/null
@@ -1,6 +0,0 @@
-// Do not install recommended packages by default
-APT::Install-Recommends "0";
-
-// Do not install suggested packages by default (this is already
-// the Ubuntu default)
-APT::Install-Suggests "0";
diff --git a/dogfood/files/etc/apt/apt.conf.d/80-retries b/dogfood/files/etc/apt/apt.conf.d/80-retries
deleted file mode 100644
index d7ee5185258ec..0000000000000
--- a/dogfood/files/etc/apt/apt.conf.d/80-retries
+++ /dev/null
@@ -1 +0,0 @@
-APT::Acquire::Retries "3";
diff --git a/dogfood/files/etc/apt/preferences.d/docker b/dogfood/files/etc/apt/preferences.d/docker
deleted file mode 100644
index a92c0abb03d7c..0000000000000
--- a/dogfood/files/etc/apt/preferences.d/docker
+++ /dev/null
@@ -1,19 +0,0 @@
-# Ignore all packages from this repository by default
-Package: *
-Pin: origin download.docker.com
-Pin-Priority: 1
-
-# Docker Community Edition
-Package: docker-ce
-Pin: origin download.docker.com
-Pin-Priority: 500
-
-# Docker command-line tool
-Package: docker-ce-cli
-Pin: origin download.docker.com
-Pin-Priority: 500
-
-# containerd runtime
-Package: containerd.io
-Pin: origin download.docker.com
-Pin-Priority: 500
diff --git a/dogfood/files/etc/apt/preferences.d/github-cli b/dogfood/files/etc/apt/preferences.d/github-cli
deleted file mode 100644
index d2dce9f5f3097..0000000000000
--- a/dogfood/files/etc/apt/preferences.d/github-cli
+++ /dev/null
@@ -1,8 +0,0 @@
-# Ignore all packages from this repository by default
-Package: *
-Pin: origin cli.github.com
-Pin-Priority: 1
-
-Package: gh
-Pin: origin cli.github.com
-Pin-Priority: 500
diff --git a/dogfood/files/etc/apt/preferences.d/google-chrome b/dogfood/files/etc/apt/preferences.d/google-chrome
deleted file mode 100644
index 4551ec390ff20..0000000000000
--- a/dogfood/files/etc/apt/preferences.d/google-chrome
+++ /dev/null
@@ -1,16 +0,0 @@
-# Ignore all packages from this repository by default
-Package: *
-Pin: origin dl.google.com
-Pin-Priority: 1
-
-Package: google-chrome-stable
-Pin: origin dl.google.com
-Pin-Priority: 500
-
-Package: google-chrome-beta
-Pin: origin dl.google.com
-Pin-Priority: 500
-
-Package: google-chrome-unstable
-Pin: origin dl.google.com
-Pin-Priority: 500
diff --git a/dogfood/files/etc/apt/preferences.d/google-cloud b/dogfood/files/etc/apt/preferences.d/google-cloud
deleted file mode 100644
index 637b0e9bb3c51..0000000000000
--- a/dogfood/files/etc/apt/preferences.d/google-cloud
+++ /dev/null
@@ -1,19 +0,0 @@
-# Ignore all packages from this repository by default
-Package: *
-Pin: origin packages.cloud.google.com
-Pin-Priority: 1
-
-# Google Cloud SDK for gcloud and gsutil CLI tools
-Package: google-cloud-sdk
-Pin: origin packages.cloud.google.com
-Pin-Priority: 500
-
-# Datastore emulator for working with the licensor
-Package: google-cloud-sdk-datastore-emulator
-Pin: origin packages.cloud.google.com
-Pin-Priority: 500
-
-# Kubectl for working with Kubernetes (GKE)
-Package: kubectl
-Pin: origin packages.cloud.google.com
-Pin-Priority: 500
diff --git a/dogfood/files/etc/apt/preferences.d/hashicorp b/dogfood/files/etc/apt/preferences.d/hashicorp
deleted file mode 100644
index 4323f331cc722..0000000000000
--- a/dogfood/files/etc/apt/preferences.d/hashicorp
+++ /dev/null
@@ -1,14 +0,0 @@
-# Ignore all packages from this repository by default
-Package: *
-Pin: origin apt.releases.hashicorp.com
-Pin-Priority: 1
-
-# Packer for creating virtual machine disk images
-Package: packer
-Pin: origin apt.releases.hashicorp.com
-Pin-Priority: 500
-
-# Terraform for managing infrastructure
-Package: terraform
-Pin: origin apt.releases.hashicorp.com
-Pin-Priority: 500
diff --git a/dogfood/files/etc/apt/preferences.d/microsoft-edge b/dogfood/files/etc/apt/preferences.d/microsoft-edge
deleted file mode 100644
index 2441961adac38..0000000000000
--- a/dogfood/files/etc/apt/preferences.d/microsoft-edge
+++ /dev/null
@@ -1,12 +0,0 @@
-# Ignore all packages from this repository by default
-Package: *
-Pin: origin packages.microsoft.com
-Pin-Priority: 1
-
-Package: microsoft-edge-beta
-Pin: origin packages.microsoft.com
-Pin-Priority: 500
-
-Package: microsoft-edge-dev
-Pin: origin packages.microsoft.com
-Pin-Priority: 500
diff --git a/dogfood/files/etc/apt/preferences.d/nodesource b/dogfood/files/etc/apt/preferences.d/nodesource
deleted file mode 100644
index de55d5553411e..0000000000000
--- a/dogfood/files/etc/apt/preferences.d/nodesource
+++ /dev/null
@@ -1,9 +0,0 @@
-# Ignore all packages from this repository by default
-Package: *
-Pin: origin deb.nodesource.com
-Pin-Priority: 1
-
-# Node.js for building the frontend
-Package: nodejs
-Pin: origin deb.nodesource.com
-Pin-Priority: 500
diff --git a/dogfood/files/etc/apt/preferences.d/ppa b/dogfood/files/etc/apt/preferences.d/ppa
deleted file mode 100644
index 1dc9da8f9fffc..0000000000000
--- a/dogfood/files/etc/apt/preferences.d/ppa
+++ /dev/null
@@ -1,19 +0,0 @@
-# Ignore all packages from this repository by default
-Package: *
-Pin: origin ppa.launchpad.net
-Pin-Priority: 1
-
-# Ansible
-Package: ansible-base
-Pin: origin ppa.launchpad.net
-Pin-Priority: 500
-
-# Neovim
-Package: neovim
-Pin: origin ppa.launchpad.net
-Pin-Priority: 500
-
-# Neovim Runtime
-Package: neovim-runtime
-Pin: origin ppa.launchpad.net
-Pin-Priority: 500
diff --git a/dogfood/files/etc/apt/preferences.d/yarnpkg b/dogfood/files/etc/apt/preferences.d/yarnpkg
deleted file mode 100644
index 7237fcad5c356..0000000000000
--- a/dogfood/files/etc/apt/preferences.d/yarnpkg
+++ /dev/null
@@ -1,9 +0,0 @@
-# Ignore all packages from this repository by default
-Package: *
-Pin: origin dl.yarnpkg.com
-Pin-Priority: 1
-
-# Yarn for managing Node.js packages
-Package: yarn
-Pin: origin dl.yarnpkg.com
-Pin-Priority: 500
diff --git a/dogfood/files/etc/apt/sources.list b/dogfood/files/etc/apt/sources.list
deleted file mode 100644
index 745bcefcf2b0c..0000000000000
--- a/dogfood/files/etc/apt/sources.list
+++ /dev/null
@@ -1,3 +0,0 @@
-deb https://mirror.pit.teraswitch.com/ubuntu/ jammy main restricted universe
-deb https://mirror.pit.teraswitch.com/ubuntu/ jammy-updates main restricted universe
-deb https://mirror.pit.teraswitch.com/ubuntu/ jammy-backports main restricted universe
diff --git a/dogfood/files/etc/apt/sources.list.d/docker.list b/dogfood/files/etc/apt/sources.list.d/docker.list
deleted file mode 100644
index f00cada1ad16e..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/docker.list
+++ /dev/null
@@ -1 +0,0 @@
-deb [signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable
diff --git a/dogfood/files/etc/apt/sources.list.d/google-chrome.list b/dogfood/files/etc/apt/sources.list.d/google-chrome.list
deleted file mode 100644
index 8dd71926f26df..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/google-chrome.list
+++ /dev/null
@@ -1 +0,0 @@
-deb [signed-by=/usr/share/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main
diff --git a/dogfood/files/etc/apt/sources.list.d/google-cloud.list b/dogfood/files/etc/apt/sources.list.d/google-cloud.list
deleted file mode 100644
index 24df98effea28..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/google-cloud.list
+++ /dev/null
@@ -1 +0,0 @@
-deb [signed-by=/usr/share/keyrings/google-cloud.gpg] https://packages.cloud.google.com/apt cloud-sdk main
diff --git a/dogfood/files/etc/apt/sources.list.d/hashicorp.list b/dogfood/files/etc/apt/sources.list.d/hashicorp.list
deleted file mode 100644
index 6e60053905ec7..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/hashicorp.list
+++ /dev/null
@@ -1 +0,0 @@
-deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com jammy main
diff --git a/dogfood/files/etc/apt/sources.list.d/microsoft-edge.list b/dogfood/files/etc/apt/sources.list.d/microsoft-edge.list
deleted file mode 100644
index f0c036f79a5c5..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/microsoft-edge.list
+++ /dev/null
@@ -1 +0,0 @@
-deb [signed-by=/usr/share/keyrings/microsoft.gpg] https://packages.microsoft.com/repos/edge stable main
diff --git a/dogfood/files/etc/apt/sources.list.d/nodesource.list b/dogfood/files/etc/apt/sources.list.d/nodesource.list
deleted file mode 100644
index a328c2c3c47dc..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/nodesource.list
+++ /dev/null
@@ -1 +0,0 @@
-deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x jammy main
diff --git a/dogfood/files/etc/apt/sources.list.d/postgresql.list b/dogfood/files/etc/apt/sources.list.d/postgresql.list
deleted file mode 100644
index 10262f3e64a10..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/postgresql.list
+++ /dev/null
@@ -1 +0,0 @@
-deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt jammy-pgdg main
diff --git a/dogfood/files/etc/apt/sources.list.d/ppa.list b/dogfood/files/etc/apt/sources.list.d/ppa.list
deleted file mode 100644
index e817c20915cb1..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/ppa.list
+++ /dev/null
@@ -1,2 +0,0 @@
-deb [signed-by=/usr/share/keyrings/ansible.gpg] https://ppa.launchpadcontent.net/ansible/ansible/ubuntu focal main
-deb [signed-by=/usr/share/keyrings/neovim.gpg] https://ppa.launchpadcontent.net/neovim-ppa/stable/ubuntu focal main
diff --git a/dogfood/files/etc/apt/sources.list.d/security.list b/dogfood/files/etc/apt/sources.list.d/security.list
deleted file mode 100644
index 1f3dae8d09b19..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/security.list
+++ /dev/null
@@ -1 +0,0 @@
-deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted universe
diff --git a/dogfood/files/etc/apt/sources.list.d/yarnpkg.list b/dogfood/files/etc/apt/sources.list.d/yarnpkg.list
deleted file mode 100644
index ada8a06f7b9b2..0000000000000
--- a/dogfood/files/etc/apt/sources.list.d/yarnpkg.list
+++ /dev/null
@@ -1 +0,0 @@
-deb [signed-by=/usr/share/keyrings/yarnpkg.gpg] https://dl.yarnpkg.com/debian/ stable main
diff --git a/dogfood/files/etc/default/google-chrome b/dogfood/files/etc/default/google-chrome
deleted file mode 100644
index 8620a6054380a..0000000000000
--- a/dogfood/files/etc/default/google-chrome
+++ /dev/null
@@ -1,4 +0,0 @@
-# These settings are required to prevent the postinst script
-# from modifying /etc/apt/sources.list.d
-repo_add_once="false"
-repo_reenable_on_distupgrade="false"
diff --git a/dogfood/files/etc/default/microsoft-edge-beta b/dogfood/files/etc/default/microsoft-edge-beta
deleted file mode 100644
index 8620a6054380a..0000000000000
--- a/dogfood/files/etc/default/microsoft-edge-beta
+++ /dev/null
@@ -1,4 +0,0 @@
-# These settings are required to prevent the postinst script
-# from modifying /etc/apt/sources.list.d
-repo_add_once="false"
-repo_reenable_on_distupgrade="false"
diff --git a/dogfood/files/etc/docker/daemon.json b/dogfood/files/etc/docker/daemon.json
deleted file mode 100644
index 8e19eeeec15b8..0000000000000
--- a/dogfood/files/etc/docker/daemon.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "registry-mirrors": ["https://mirror.gcr.io"]
-}
diff --git a/dogfood/files/etc/sudoers.d/nopasswd b/dogfood/files/etc/sudoers.d/nopasswd
deleted file mode 100644
index 3283f4455630c..0000000000000
--- a/dogfood/files/etc/sudoers.d/nopasswd
+++ /dev/null
@@ -1 +0,0 @@
-coder ALL=(ALL) NOPASSWD:ALL
diff --git a/dogfood/files/usr/share/keyrings/ansible.gpg b/dogfood/files/usr/share/keyrings/ansible.gpg
deleted file mode 100644
index 1731dd2b2fbd7..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/ansible.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/docker.gpg b/dogfood/files/usr/share/keyrings/docker.gpg
deleted file mode 100644
index e5dc8cfda8e5d..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/docker.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/github-cli.gpg b/dogfood/files/usr/share/keyrings/github-cli.gpg
deleted file mode 100644
index eddea90bd75df..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/github-cli.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/google-chrome.gpg b/dogfood/files/usr/share/keyrings/google-chrome.gpg
deleted file mode 100644
index cee005a7386d9..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/google-chrome.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/google-cloud.gpg b/dogfood/files/usr/share/keyrings/google-cloud.gpg
deleted file mode 100644
index 0f478144f1491..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/google-cloud.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/hashicorp.gpg b/dogfood/files/usr/share/keyrings/hashicorp.gpg
deleted file mode 100644
index 674dd40c4219e..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/hashicorp.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/microsoft.gpg b/dogfood/files/usr/share/keyrings/microsoft.gpg
deleted file mode 100644
index 0cffae08d061d..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/microsoft.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/neovim.gpg b/dogfood/files/usr/share/keyrings/neovim.gpg
deleted file mode 100644
index b88f69c53b482..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/neovim.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/nodesource.gpg b/dogfood/files/usr/share/keyrings/nodesource.gpg
deleted file mode 100644
index 4f3ec4ed793b3..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/nodesource.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/postgresql.gpg b/dogfood/files/usr/share/keyrings/postgresql.gpg
deleted file mode 100644
index afa15cb1087de..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/postgresql.gpg and /dev/null differ
diff --git a/dogfood/files/usr/share/keyrings/yarnpkg.gpg b/dogfood/files/usr/share/keyrings/yarnpkg.gpg
deleted file mode 100644
index 32a096756e317..0000000000000
Binary files a/dogfood/files/usr/share/keyrings/yarnpkg.gpg and /dev/null differ
diff --git a/dogfood/guide.md b/dogfood/guide.md
index 621cb69d2a588..fc6e8cd93d932 100644
--- a/dogfood/guide.md
+++ b/dogfood/guide.md
@@ -1,6 +1,8 @@
# Dogfooding Guide
-This guide explains how to [dogfood](https://www.techopedia.com/definition/30784/dogfooding) coder for employees at Coder.
+This guide explains how to
+[dogfood](https://www.techopedia.com/definition/30784/dogfooding) coder for
+employees at Coder.
## How to
@@ -8,17 +10,21 @@ The following explains how to do certain things related to dogfooding.
### Dogfood using Coder's Deployment
-1. Go to [https://dev.coder.com/templates/coder-ts](https://dev.coder.com/templates/coder-ts)
+1. Go to
+ [https://dev.coder.com/templates/coder-ts](https://dev.coder.com/templates/coder-ts)
1. If you don't have an account, sign in with GitHub
2. If you see a dialog/pop-up, hit "Cancel" (this is because of Rippling)
2. Create a workspace
3. [Connect with your favorite IDE](https://coder.com/docs/coder-oss/latest/ides)
4. Clone the repo: `git clone git@github.com:coder/coder.git`
-5. Follow the [contributing guide](https://coder.com/docs/coder-oss/latest/CONTRIBUTING)
+5. Follow the
+ [contributing guide](https://coder.com/docs/coder-oss/latest/CONTRIBUTING)
### Run Coder in your Coder Workspace
-1. Clone the Git repo `[https://github.com/coder/coder](https://github.com/coder/coder)` and `cd` into it
+1. Clone the Git repo
+ `[https://github.com/coder/coder](https://github.com/coder/coder)` and `cd`
+ into it
2. Run `sudo apt update` and then `sudo apt install -y netcat`
- skip this step if using the `coder` template
3. Run `make bin`
@@ -33,7 +39,8 @@ The following explains how to do certain things related to dogfooding.
Don’t fret! This is a known issue. To get around it:
- 1. Add `export DB_FROM=coderdb` to your `.bashrc` (make sure you `source ~/.bashrc`)
+ 1. Add `export DB_FROM=coderdb` to your `.bashrc` (make sure you
+ `source ~/.bashrc`)
2. Run `sudo service postgresql start`
3. Run `sudo -u postgres psql` (this will open the PostgreSQL CLI)
4. Run `postgres-# alter user postgres password 'postgres';`
@@ -44,13 +51,23 @@ The following explains how to do certain things related to dogfooding.
4. Run `./scripts/develop.sh` which will start _two_ separate processes:
- 1. `[http://localhost:3000](http://localhost:3000)` — backend API server 👈 Backend devs will want to talk to this
- 2. `[http://localhost:8080](http://localhost:8080)` — Node.js dev server 👈 Frontend devs will want to talk to this
-5. Ensure that you’re logged in: `./scripts/coder-dev.sh list` — should return no workspace. If this returns an error, double-check the output of running `scripts/develop.sh`.
-6. A template named `docker-amd64` (or `docker-arm64` if you’re on ARM) will have automatically been created for you. If you just want to create a workspace quickly, you can run `./scripts/coder-dev.sh create myworkspace -t docker-amd64` and this will get you going quickly!
-7. To create your own template, you can do: `./scripts/coder-dev.sh templates init` and choose your preferred option.
- For example, choosing “Develop in Docker” will create a new folder `docker` that contains the bare bones for starting a Docker workspace template.
- Then, enter the folder that was just created and customize as you wish.
+ 1. `[http://localhost:3000](http://localhost:3000)` — backend API server
+ 👈 Backend devs will want to talk to this
+ 2. `[http://localhost:8080](http://localhost:8080)` — Node.js dev server
+ 👈 Frontend devs will want to talk to this
+5. Ensure that you’re logged in: `./scripts/coder-dev.sh list` — should return
+ no workspace. If this returns an error, double-check the output of running
+ `scripts/develop.sh`.
+6. A template named `docker-amd64` (or `docker-arm64` if you’re on ARM) will
+ have automatically been created for you. If you just want to create a
+ workspace quickly, you can run
+ `./scripts/coder-dev.sh create myworkspace -t docker-amd64` and this will
+ get you going quickly!
+7. To create your own template, you can do:
+ `./scripts/coder-dev.sh templates init` and choose your preferred option.
+ For example, choosing “Develop in Docker” will create a new folder `docker`
+ that contains the bare bones for starting a Docker workspace template. Then,
+ enter the folder that was just created and customize as you wish.
💡 **For all Docker templates:**
@@ -85,11 +102,13 @@ Run 'coder create --help' for usage.
Check the output of `docker ps -a`
-- If you see a container with the status `Exited` run `docker logs ` and see what the issue with the container output is
+- If you see a container with the status `Exited` run
+ `docker logs ` and see what the issue with the container
+ output is
Enable verbose container logging for Docker:
-```console
+```shell
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.orig
sudo cat > /etc/docker/daemon.json << EOF
{
@@ -105,4 +124,5 @@ sudo journalctl -u docker -f
### Help! I'm still blocked
-Post in the #dogfood Slack channel internally or open a Discussion on GitHub and tag @jsjoeio or @bpmct
+Post in the #dogfood Slack channel internally or open a Discussion on GitHub and
+tag @jsjoeio or @bpmct
diff --git a/dogfood/main.tf b/dogfood/main.tf
index 554ba21eda07f..6f225f7f32dec 100644
--- a/dogfood/main.tf
+++ b/dogfood/main.tf
@@ -6,7 +6,7 @@ terraform {
}
docker = {
source = "kreuzwerker/docker"
- version = "~> 2.22.0"
+ version = "~> 3.0.0"
}
}
}
@@ -40,9 +40,10 @@ data "coder_parameter" "dotfiles_url" {
}
data "coder_parameter" "region" {
- type = "string"
- name = "Region"
- icon = "/emojis/1f30e.png"
+ type = "string"
+ name = "Region"
+ icon = "/emojis/1f30e.png"
+ default = "us-pittsburgh"
option {
icon = "/emojis/1f1fa-1f1f8.png"
name = "Pittsburgh"
@@ -260,15 +261,15 @@ locals {
registry_name = "codercom/oss-dogfood"
}
data "docker_registry_image" "dogfood" {
- name = "${local.registry_name}:main"
+ // This is temporarily pinned to a pre-nix version of the image at commit
+ // 6cdf1c73c until the Nix kinks are worked out.
+ name = "${local.registry_name}:pre-nix"
}
resource "docker_image" "dogfood" {
name = "${local.registry_name}@${data.docker_registry_image.dogfood.sha256_digest}"
pull_triggers = [
- data.docker_registry_image.dogfood.sha256_digest,
- sha1(join("", [for f in fileset(path.module, "files/*") : filesha1(f)])),
- filesha1("Dockerfile"),
+ data.docker_registry_image.dogfood.sha256_digest
]
keep_locally = true
}
@@ -326,4 +327,8 @@ resource "coder_metadata" "container_info" {
key = "runtime"
value = docker_container.workspace[0].runtime
}
+ item {
+ key = "region"
+ value = data.coder_parameter.region.option[index(data.coder_parameter.region.option.*.value, data.coder_parameter.region.value)].name
+ }
}
diff --git a/dogfood/update-keys.sh b/dogfood/update-keys.sh
deleted file mode 100755
index 9ebaf77bb5256..0000000000000
--- a/dogfood/update-keys.sh
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-PROJECT_ROOT="$(git rev-parse --show-toplevel)"
-
-curl_flags=(
- --silent
- --show-error
- --location
-)
-
-gpg_flags=(
- --dearmor
- --yes
-)
-
-pushd "$PROJECT_ROOT/dogfood/files/usr/share/keyrings"
-# Upstream Docker signing key
-curl "${curl_flags[@]}" "https://download.docker.com/linux/ubuntu/gpg" |
- gpg "${gpg_flags[@]}" --output="docker.gpg"
-
-# Google Cloud signing key
-curl "${curl_flags[@]}" "https://packages.cloud.google.com/apt/doc/apt-key.gpg" |
- gpg "${gpg_flags[@]}" --output="google-cloud.gpg"
-
-# Google Linux Software repository signing key (Chrome)
-curl "${curl_flags[@]}" "https://dl.google.com/linux/linux_signing_key.pub" |
- gpg "${gpg_flags[@]}" --output="google-chrome.gpg"
-
-# Microsoft repository signing key (Edge)
-curl "${curl_flags[@]}" "https://packages.microsoft.com/keys/microsoft.asc" |
- gpg "${gpg_flags[@]}" --output="microsoft.gpg"
-
-# Upstream PostgreSQL signing key
-curl "${curl_flags[@]}" "https://www.postgresql.org/media/keys/ACCC4CF8.asc" |
- gpg "${gpg_flags[@]}" --output="postgresql.gpg"
-
-# NodeSource signing key
-curl "${curl_flags[@]}" "https://deb.nodesource.com/gpgkey/nodesource.gpg.key" |
- gpg "${gpg_flags[@]}" --output="nodesource.gpg"
-
-# Yarnpkg signing key
-curl "${curl_flags[@]}" "https://dl.yarnpkg.com/debian/pubkey.gpg" |
- gpg "${gpg_flags[@]}" --output="yarnpkg.gpg"
-
-# Ansible PPA signing key
-curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x6125e2a8c77f2818fb7bd15b93c4a3fd7bb9c367" |
- gpg "${gpg_flags[@]}" --output="ansible.gpg"
-
-# Neovim signing key
-curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9dbb0be9366964f134855e2255f96fcf8231b6dd" |
- gpg "${gpg_flags[@]}" --output="neovim.gpg"
-
-# Hashicorp signing key
-curl "${curl_flags[@]}" "https://apt.releases.hashicorp.com/gpg" |
- gpg "${gpg_flags[@]}" --output="hashicorp.gpg"
-
-# GitHub CLI signing key
-curl "${curl_flags[@]}" "https://cli.github.com/packages/githubcli-archive-keyring.gpg" |
- gpg "${gpg_flags[@]}" --output="github-cli.gpg"
-popd
diff --git a/enterprise/audit/audit.go b/enterprise/audit/audit.go
index 7b345f22f8c66..5c745b8debc61 100644
--- a/enterprise/audit/audit.go
+++ b/enterprise/audit/audit.go
@@ -5,8 +5,8 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
)
// Backends can store or send audit logs to arbitrary locations.
diff --git a/enterprise/audit/audit_test.go b/enterprise/audit/audit_test.go
index 06c2514323eb5..421a637aa10c5 100644
--- a/enterprise/audit/audit_test.go
+++ b/enterprise/audit/audit_test.go
@@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/enterprise/audit"
- "github.com/coder/coder/enterprise/audit/audittest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/enterprise/audit"
+ "github.com/coder/coder/v2/enterprise/audit/audittest"
)
func TestAuditor(t *testing.T) {
diff --git a/enterprise/audit/audittest/rand.go b/enterprise/audit/audittest/rand.go
index c1560825f0e37..271ac98ed019c 100644
--- a/enterprise/audit/audittest/rand.go
+++ b/enterprise/audit/audittest/rand.go
@@ -9,7 +9,7 @@ import (
"github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
)
func RandomLog() database.AuditLog {
diff --git a/enterprise/audit/backends/postgres.go b/enterprise/audit/backends/postgres.go
index a0cadd28995ed..7d792dfdd0918 100644
--- a/enterprise/audit/backends/postgres.go
+++ b/enterprise/audit/backends/postgres.go
@@ -5,8 +5,8 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/enterprise/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/enterprise/audit"
)
type postgresBackend struct {
diff --git a/enterprise/audit/backends/postgres_test.go b/enterprise/audit/backends/postgres_test.go
index a11001e55d6e4..c1360c6430004 100644
--- a/enterprise/audit/backends/postgres_test.go
+++ b/enterprise/audit/backends/postgres_test.go
@@ -6,10 +6,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/enterprise/audit/audittest"
- "github.com/coder/coder/enterprise/audit/backends"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/enterprise/audit/audittest"
+ "github.com/coder/coder/v2/enterprise/audit/backends"
)
func TestPostgresBackend(t *testing.T) {
diff --git a/enterprise/audit/backends/slog.go b/enterprise/audit/backends/slog.go
index 2fea8fcbc57c9..298310448e9cb 100644
--- a/enterprise/audit/backends/slog.go
+++ b/enterprise/audit/backends/slog.go
@@ -6,8 +6,8 @@ import (
"github.com/fatih/structs"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/enterprise/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/enterprise/audit"
)
type slogBackend struct {
diff --git a/enterprise/audit/backends/slog_test.go b/enterprise/audit/backends/slog_test.go
index c963746bf2dd3..03ea27ac524eb 100644
--- a/enterprise/audit/backends/slog_test.go
+++ b/enterprise/audit/backends/slog_test.go
@@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog"
- "github.com/coder/coder/enterprise/audit/audittest"
- "github.com/coder/coder/enterprise/audit/backends"
+ "github.com/coder/coder/v2/enterprise/audit/audittest"
+ "github.com/coder/coder/v2/enterprise/audit/backends"
)
func TestSlogBackend(t *testing.T) {
diff --git a/enterprise/audit/diff.go b/enterprise/audit/diff.go
index ca5c454e51b54..59780d2918418 100644
--- a/enterprise/audit/diff.go
+++ b/enterprise/audit/diff.go
@@ -8,9 +8,9 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/util/ptr"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/util/ptr"
)
func structName(t reflect.Type) string {
diff --git a/enterprise/audit/diff_internal_test.go b/enterprise/audit/diff_internal_test.go
index d53a9ad1f3e82..f98d16138cf1f 100644
--- a/enterprise/audit/diff_internal_test.go
+++ b/enterprise/audit/diff_internal_test.go
@@ -11,9 +11,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/util/ptr"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/util/ptr"
)
func Test_diffValues(t *testing.T) {
diff --git a/enterprise/audit/filter.go b/enterprise/audit/filter.go
index 868d5bb7d77db..113bfc101b799 100644
--- a/enterprise/audit/filter.go
+++ b/enterprise/audit/filter.go
@@ -3,7 +3,7 @@ package audit
import (
"context"
- "github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/v2/coderd/database"
)
// FilterDecision is a bitwise flag describing the actions a given filter allows
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index b5ab50457c963..a1dfef2d053d3 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -6,8 +6,8 @@ import (
"reflect"
"runtime"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
)
// This mapping creates a relationship between an Auditable Resource
@@ -82,8 +82,8 @@ var auditableResourcesTypes = map[any]map[string]Action{
"allow_user_autostop": ActionTrack,
"allow_user_cancel_workspace_jobs": ActionTrack,
"failure_ttl": ActionTrack,
- "inactivity_ttl": ActionTrack,
- "locked_ttl": ActionTrack,
+ "time_til_dormant": ActionTrack,
+ "time_til_dormant_autodelete": ActionTrack,
},
&database.TemplateVersion{}: {
"id": ActionTrack,
@@ -127,7 +127,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"autostart_schedule": ActionTrack,
"ttl": ActionTrack,
"last_used_at": ActionIgnore,
- "locked_at": ActionTrack,
+ "dormant_at": ActionTrack,
"deleting_at": ActionTrack,
},
&database.WorkspaceBuild{}: {
@@ -156,6 +156,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"avatar_url": ActionTrack,
"quota_allowance": ActionTrack,
"members": ActionTrack,
+ "source": ActionIgnore,
},
&database.APIKey{}: {
"id": ActionIgnore,
diff --git a/enterprise/audit/table_internal_test.go b/enterprise/audit/table_internal_test.go
index 77e75257d3f08..4882828574db6 100644
--- a/enterprise/audit/table_internal_test.go
+++ b/enterprise/audit/table_internal_test.go
@@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/tools/go/packages"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/util/slice"
)
// TestAuditableResources ensures that all auditable resources are included in
@@ -48,7 +48,7 @@ func TestAuditableResources(t *testing.T) {
expectedList := make([]string, 0)
// Now we check we have all the resources in the AuditableResources
for i := 0; i < unionType.Len(); i++ {
- // All types come across like 'github.com/coder/coder/coderd/database.'
+ // All types come across like 'github.com/coder/coder/v2/coderd/database.'
typeName := unionType.Term(i).Type().String()
_, ok := AuditableResources[typeName]
assert.True(t, ok, "missing resource %q from AuditableResources", typeName)
diff --git a/enterprise/cli/features.go b/enterprise/cli/features.go
index e02feb7781ce6..57435dbbf29dd 100644
--- a/enterprise/cli/features.go
+++ b/enterprise/cli/features.go
@@ -10,9 +10,9 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) features() *clibase.Cmd {
diff --git a/enterprise/cli/features_test.go b/enterprise/cli/features_test.go
index 042a67a2dad31..406e626d363bd 100644
--- a/enterprise/cli/features_test.go
+++ b/enterprise/cli/features_test.go
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestFeaturesList(t *testing.T) {
diff --git a/enterprise/cli/groupcreate.go b/enterprise/cli/groupcreate.go
index d80c1d441e69d..5f40b79bb4502 100644
--- a/enterprise/cli/groupcreate.go
+++ b/enterprise/cli/groupcreate.go
@@ -5,10 +5,10 @@ import (
"golang.org/x/xerrors"
- agpl "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ agpl "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) groupCreate() *clibase.Cmd {
diff --git a/enterprise/cli/groupcreate_test.go b/enterprise/cli/groupcreate_test.go
index 484cb42c46b59..4e9bdfdfb5ed2 100644
--- a/enterprise/cli/groupcreate_test.go
+++ b/enterprise/cli/groupcreate_test.go
@@ -6,12 +6,12 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestCreateGroup(t *testing.T) {
diff --git a/enterprise/cli/groupdelete.go b/enterprise/cli/groupdelete.go
index 268d8064df310..adada073e32a6 100644
--- a/enterprise/cli/groupdelete.go
+++ b/enterprise/cli/groupdelete.go
@@ -5,10 +5,10 @@ import (
"golang.org/x/xerrors"
- agpl "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ agpl "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) groupDelete() *clibase.Cmd {
diff --git a/enterprise/cli/groupdelete_test.go b/enterprise/cli/groupdelete_test.go
index 91ee5a337155b..c3ff2593e0b56 100644
--- a/enterprise/cli/groupdelete_test.go
+++ b/enterprise/cli/groupdelete_test.go
@@ -6,13 +6,13 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestGroupDelete(t *testing.T) {
diff --git a/enterprise/cli/groupedit.go b/enterprise/cli/groupedit.go
index 9ef6a3100658d..a1c8db9ab8995 100644
--- a/enterprise/cli/groupedit.go
+++ b/enterprise/cli/groupedit.go
@@ -7,10 +7,10 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- agpl "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ agpl "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) groupEdit() *clibase.Cmd {
diff --git a/enterprise/cli/groupedit_test.go b/enterprise/cli/groupedit_test.go
index d91704885717d..a6bf396338147 100644
--- a/enterprise/cli/groupedit_test.go
+++ b/enterprise/cli/groupedit_test.go
@@ -6,14 +6,14 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestGroupEdit(t *testing.T) {
diff --git a/enterprise/cli/grouplist.go b/enterprise/cli/grouplist.go
index 51c36c4ba8ac9..78bcb28ca13ac 100644
--- a/enterprise/cli/grouplist.go
+++ b/enterprise/cli/grouplist.go
@@ -7,10 +7,10 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- agpl "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ agpl "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) groupList() *clibase.Cmd {
diff --git a/enterprise/cli/grouplist_test.go b/enterprise/cli/grouplist_test.go
index 90e054a03faab..2787893faecb1 100644
--- a/enterprise/cli/grouplist_test.go
+++ b/enterprise/cli/grouplist_test.go
@@ -5,13 +5,13 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestGroupList(t *testing.T) {
@@ -74,10 +74,10 @@ func TestGroupList(t *testing.T) {
}
})
- t.Run("NoGroups", func(t *testing.T) {
+ t.Run("Everyone", func(t *testing.T) {
t.Parallel()
- client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
+ client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureTemplateRBAC: 1,
},
@@ -87,13 +87,19 @@ func TestGroupList(t *testing.T) {
pty := ptytest.New(t)
- inv.Stderr = pty.Output()
+ inv.Stdout = pty.Output()
clitest.SetupConfig(t, client, conf)
err := inv.Run()
require.NoError(t, err)
- pty.ExpectMatch("No groups found")
- pty.ExpectMatch("coder groups create ")
+ matches := []string{
+ "NAME", "ORGANIZATION ID", "MEMBERS", " AVATAR URL",
+ "Everyone", user.OrganizationID.String(), coderdtest.FirstUserParams.Email, "",
+ }
+
+ for _, match := range matches {
+ pty.ExpectMatch(match)
+ }
})
}
diff --git a/enterprise/cli/groups.go b/enterprise/cli/groups.go
index adf7c8c527509..ff537697ea02c 100644
--- a/enterprise/cli/groups.go
+++ b/enterprise/cli/groups.go
@@ -1,7 +1,7 @@
package cli
import (
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/cli/clibase"
)
func (r *RootCmd) groups() *clibase.Cmd {
diff --git a/enterprise/cli/licenses.go b/enterprise/cli/licenses.go
index e4bf3e0731636..45e3d3e6ec97d 100644
--- a/enterprise/cli/licenses.go
+++ b/enterprise/cli/licenses.go
@@ -13,9 +13,9 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
var jwtRegexp = regexp.MustCompile(`^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$`)
diff --git a/enterprise/cli/licenses_test.go b/enterprise/cli/licenses_test.go
index 9272a103fad08..df21ba6b66b25 100644
--- a/enterprise/cli/licenses_test.go
+++ b/enterprise/cli/licenses_test.go
@@ -16,13 +16,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
const (
diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go
index f3dfc2ba367d7..e46756578d542 100644
--- a/enterprise/cli/provisionerdaemons.go
+++ b/enterprise/cli/provisionerdaemons.go
@@ -11,16 +11,16 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- agpl "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner/terraform"
- "github.com/coder/coder/provisionerd"
- provisionerdproto "github.com/coder/coder/provisionerd/proto"
- "github.com/coder/coder/provisionersdk"
- "github.com/coder/coder/provisionersdk/proto"
+ agpl "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner/terraform"
+ "github.com/coder/coder/v2/provisionerd"
+ provisionerdproto "github.com/coder/coder/v2/provisionerd/proto"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
func (r *RootCmd) provisionerDaemons() *clibase.Cmd {
@@ -44,13 +44,14 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
rawTags []string
pollInterval time.Duration
pollJitter time.Duration
+ preSharedKey string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Use: "start",
Short: "Run a provisioner daemon",
Middleware: clibase.Chain(
- r.InitClient(client),
+ r.InitClientMissingTokenOK(client),
),
Handler: func(inv *clibase.Invocation) error {
ctx, cancel := context.WithCancel(inv.Context())
@@ -59,11 +60,6 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
notifyCtx, notifyStop := signal.NotifyContext(ctx, agpl.InterruptSignals...)
defer notifyStop()
- org, err := agpl.CurrentOrganization(inv, client)
- if err != nil {
- return xerrors.Errorf("get current organization: %w", err)
- }
-
tags, err := agpl.ParseProvisionerTags(rawTags)
if err != nil {
return err
@@ -74,6 +70,11 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
return xerrors.Errorf("mkdir %q: %w", cacheDir, err)
}
+ tempDir, err := os.MkdirTemp("", "provisionerd")
+ if err != nil {
+ return err
+ }
+
terraformClient, terraformServer := provisionersdk.MemTransportPipe()
go func() {
<-ctx.Done()
@@ -88,10 +89,11 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
err := terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
- Listener: terraformServer,
+ Listener: terraformServer,
+ Logger: logger.Named("terraform"),
+ WorkDirectory: tempDir,
},
CachePath: cacheDir,
- Logger: logger.Named("terraform"),
})
if err != nil && !xerrors.Is(err, context.Canceled) {
select {
@@ -101,27 +103,25 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
}
}()
- tempDir, err := os.MkdirTemp("", "provisionerd")
- if err != nil {
- return err
- }
-
logger.Info(ctx, "starting provisioner daemon", slog.F("tags", tags))
provisioners := provisionerd.Provisioners{
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(terraformClient),
}
srv := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
- return client.ServeProvisionerDaemon(ctx, org.ID, []codersdk.ProvisionerType{
- codersdk.ProvisionerTypeTerraform,
- }, tags)
+ return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Provisioners: []codersdk.ProvisionerType{
+ codersdk.ProvisionerTypeTerraform,
+ },
+ Tags: tags,
+ PreSharedKey: preSharedKey,
+ })
}, &provisionerd.Options{
Logger: logger,
JobPollInterval: pollInterval,
JobPollJitter: pollJitter,
UpdateInterval: 500 * time.Millisecond,
Provisioners: provisioners,
- WorkDirectory: tempDir,
})
var exitErr error
@@ -137,9 +137,7 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
cliui.Errorf(inv.Stderr, "Unexpected error, shutting down server: %s\n", exitErr)
}
- shutdown, shutdownCancel := context.WithTimeout(ctx, time.Minute)
- defer shutdownCancel()
- err = srv.Shutdown(shutdown)
+ err = srv.Shutdown(ctx)
if err != nil {
return xerrors.Errorf("shutdown: %w", err)
}
@@ -182,6 +180,12 @@ func (r *RootCmd) provisionerDaemonStart() *clibase.Cmd {
Default: (100 * time.Millisecond).String(),
Value: clibase.DurationOf(&pollJitter),
},
+ {
+ Flag: "psk",
+ Env: "CODER_PROVISIONER_DAEMON_PSK",
+ Description: "Pre-shared key to authenticate with Coder server.",
+ Value: clibase.StringOf(&preSharedKey),
+ },
}
return cmd
diff --git a/enterprise/cli/provisionerdaemons_test.go b/enterprise/cli/provisionerdaemons_test.go
new file mode 100644
index 0000000000000..038ba97ec7a54
--- /dev/null
+++ b/enterprise/cli/provisionerdaemons_test.go
@@ -0,0 +1,56 @@
+package cli_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
+)
+
+func TestProvisionerDaemon_PSK(t *testing.T) {
+ t.Parallel()
+
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ ProvisionerDaemonPSK: "provisionersftw",
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ })
+ inv, conf := newCLI(t, "provisionerd", "start", "--psk=provisionersftw")
+ err := conf.URL().Write(client.URL.String())
+ require.NoError(t, err)
+ pty := ptytest.New(t).Attach(inv)
+ ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
+ defer cancel()
+ clitest.Start(t, inv)
+ pty.ExpectMatchContext(ctx, "starting provisioner daemon")
+}
+
+func TestProvisionerDaemon_SessionToken(t *testing.T) {
+ t.Parallel()
+
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ ProvisionerDaemonPSK: "provisionersftw",
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ })
+ inv, conf := newCLI(t, "provisionerd", "start")
+ clitest.SetupConfig(t, client, conf)
+ pty := ptytest.New(t).Attach(inv)
+ ctx, cancel := context.WithTimeout(inv.Context(), testutil.WaitLong)
+ defer cancel()
+ clitest.Start(t, inv)
+ pty.ExpectMatchContext(ctx, "starting provisioner daemon")
+}
diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go
index f40d12d6d4cde..e7f4e8650783c 100644
--- a/enterprise/cli/proxyserver.go
+++ b/enterprise/cli/proxyserver.go
@@ -22,14 +22,14 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/wsproxy"
+ "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/wsproxy"
)
type closers []func()
diff --git a/enterprise/cli/proxyserver_slim.go b/enterprise/cli/proxyserver_slim.go
index 48dd85710ded7..85570a98d45fc 100644
--- a/enterprise/cli/proxyserver_slim.go
+++ b/enterprise/cli/proxyserver_slim.go
@@ -7,8 +7,8 @@ import (
"io"
"os"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
)
func (r *RootCmd) proxyServer() *clibase.Cmd {
diff --git a/enterprise/cli/root.go b/enterprise/cli/root.go
index a89a8008fc977..9f7bfb9039683 100644
--- a/enterprise/cli/root.go
+++ b/enterprise/cli/root.go
@@ -1,8 +1,8 @@
package cli
import (
- "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clibase"
)
type RootCmd struct {
diff --git a/enterprise/cli/root_internal_test.go b/enterprise/cli/root_internal_test.go
index 3b3c86bc8d011..e2af6bcdd46ae 100644
--- a/enterprise/cli/root_internal_test.go
+++ b/enterprise/cli/root_internal_test.go
@@ -5,9 +5,9 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/clitest"
+ "github.com/coder/coder/v2/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/clitest"
)
//nolint:tparallel,paralleltest
diff --git a/enterprise/cli/root_test.go b/enterprise/cli/root_test.go
index bf44ad08ff921..e957c8e09d428 100644
--- a/enterprise/cli/root_test.go
+++ b/enterprise/cli/root_test.go
@@ -5,10 +5,10 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/cli/config"
- "github.com/coder/coder/enterprise/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/cli/config"
+ "github.com/coder/coder/v2/enterprise/cli"
)
func newCLI(t *testing.T, args ...string) (*clibase.Invocation, config.Root) {
diff --git a/enterprise/cli/server.go b/enterprise/cli/server.go
index b0561a0de1850..197eab61e10e9 100644
--- a/enterprise/cli/server.go
+++ b/enterprise/cli/server.go
@@ -13,15 +13,15 @@ import (
"tailscale.com/derp"
"tailscale.com/types/key"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/enterprise/audit"
- "github.com/coder/coder/enterprise/audit/backends"
- "github.com/coder/coder/enterprise/coderd"
- "github.com/coder/coder/enterprise/trialer"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/enterprise/audit"
+ "github.com/coder/coder/v2/enterprise/audit/backends"
+ "github.com/coder/coder/v2/enterprise/coderd"
+ "github.com/coder/coder/v2/enterprise/trialer"
+ "github.com/coder/coder/v2/tailnet"
- agplcoderd "github.com/coder/coder/coderd"
+ agplcoderd "github.com/coder/coder/v2/coderd"
)
func (r *RootCmd) server() *clibase.Cmd {
@@ -66,6 +66,7 @@ func (r *RootCmd) server() *clibase.Cmd {
DERPServerRegionID: int(options.DeploymentValues.DERP.Server.RegionID.Value()),
ProxyHealthInterval: options.DeploymentValues.ProxyHealthStatusInterval.Value(),
DefaultQuietHoursSchedule: options.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Value(),
+ ProvisionerDaemonPSK: options.DeploymentValues.Provisioner.DaemonPSK.Value(),
}
api, err := coderd.New(ctx, o)
diff --git a/enterprise/cli/server_slim.go b/enterprise/cli/server_slim.go
index 301151fd38893..a0e9800ed760f 100644
--- a/enterprise/cli/server_slim.go
+++ b/enterprise/cli/server_slim.go
@@ -8,8 +8,8 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- agplcoderd "github.com/coder/coder/coderd"
+ "github.com/coder/coder/v2/cli/clibase"
+ agplcoderd "github.com/coder/coder/v2/coderd"
)
func (r *RootCmd) server() *clibase.Cmd {
diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden
index 7fc6962ded847..ae24592079a69 100644
--- a/enterprise/cli/testdata/coder_--help.golden
+++ b/enterprise/cli/testdata/coder_--help.golden
@@ -33,6 +33,11 @@ variables or flags.
Additional HTTP headers added to all requests. Provide as key=value.
Can be specified multiple times.
+ --header-command string, $CODER_HEADER_COMMAND
+ An external command that outputs additional HTTP headers added to all
+ requests. The command must output each header as `key=value` on its
+ own line.
+
--no-feature-warning bool, $CODER_NO_FEATURE_WARNING
Suppress warnings about unlicensed features.
diff --git a/enterprise/cli/testdata/coder_provisionerd_start_--help.golden b/enterprise/cli/testdata/coder_provisionerd_start_--help.golden
index 1236cfb5ae7e1..5258c33125173 100644
--- a/enterprise/cli/testdata/coder_provisionerd_start_--help.golden
+++ b/enterprise/cli/testdata/coder_provisionerd_start_--help.golden
@@ -12,6 +12,9 @@ Run a provisioner daemon
--poll-jitter duration, $CODER_PROVISIONERD_POLL_JITTER (default: 100ms)
How much to jitter the poll interval by.
+ --psk string, $CODER_PROVISIONER_DAEMON_PSK
+ Pre-shared key to authenticate with Coder server.
+
-t, --tag string-array, $CODER_PROVISIONERD_TAGS
Tags to filter provisioner jobs by.
diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden
index cb7ca61b4913a..d3a5d74bcddbe 100644
--- a/enterprise/cli/testdata/coder_server_--help.golden
+++ b/enterprise/cli/testdata/coder_server_--help.golden
@@ -172,21 +172,25 @@ backed by Tailscale and WireGuard.
URL to fetch a DERP mapping on startup. See:
https://tailscale.com/kb/1118/custom-derp-servers/.
+ --derp-force-websockets bool, $CODER_DERP_FORCE_WEBSOCKETS
+ Force clients and agents to always use WebSocket to connect to DERP
+ relay servers. By default, DERP uses `Upgrade: derp`, which may cause
+ issues with some reverse proxies. Clients may automatically fallback
+ to WebSocket if they detect an issue with `Upgrade: derp`, but this
+ does not work in all situations.
+
--derp-server-enable bool, $CODER_DERP_SERVER_ENABLE (default: true)
Whether to enable or disable the embedded DERP relay server.
- --derp-server-region-code string, $CODER_DERP_SERVER_REGION_CODE (default: coder)
- Region code to use for the embedded DERP server.
-
- --derp-server-region-id int, $CODER_DERP_SERVER_REGION_ID (default: 999)
- Region ID to use for the embedded DERP server.
-
--derp-server-region-name string, $CODER_DERP_SERVER_REGION_NAME (default: Coder Embedded Relay)
Region name that for the embedded DERP server.
- --derp-server-stun-addresses string-array, $CODER_DERP_SERVER_STUN_ADDRESSES (default: stun.l.google.com:19302)
- Addresses for STUN servers to establish P2P connections. Use special
- value 'disable' to turn off STUN.
+ --derp-server-stun-addresses string-array, $CODER_DERP_SERVER_STUN_ADDRESSES (default: stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302)
+ Addresses for STUN servers to establish P2P connections. It's
+ recommended to have at least two STUN servers to give users the best
+ chance of connecting P2P to workspaces. Each STUN server will get it's
+ own DERP region, with region IDs starting at `--derp-server-region-id
+ + 1`. Use special value 'disable' to turn off STUN completely.
[1mNetworking / HTTP Options[0m
--disable-password-auth bool, $CODER_DISABLE_PASSWORD_AUTH
@@ -298,15 +302,28 @@ can safely ignore these settings.
GitHub.
[1mOIDC Options[0m
+ --oidc-group-auto-create bool, $CODER_OIDC_GROUP_AUTO_CREATE (default: false)
+ Automatically creates missing groups from a user's groups claim.
+
--oidc-allow-signups bool, $CODER_OIDC_ALLOW_SIGNUPS (default: true)
Whether new users can sign up with OIDC.
--oidc-auth-url-params struct[map[string]string], $CODER_OIDC_AUTH_URL_PARAMS (default: {"access_type": "offline"})
OIDC auth URL parameters to pass to the upstream provider.
+ --oidc-client-cert-file string, $CODER_OIDC_CLIENT_CERT_FILE
+ Pem encoded certificate file to use for oauth2 PKI/JWT authorization.
+ The public certificate that accompanies oidc-client-key-file. A
+ standard x509 certificate is expected.
+
--oidc-client-id string, $CODER_OIDC_CLIENT_ID
Client ID to use for Login with OIDC.
+ --oidc-client-key-file string, $CODER_OIDC_CLIENT_KEY_FILE
+ Pem encoded RSA private key to use for oauth2 PKI/JWT authorization.
+ This can be used instead of oidc-client-secret if your IDP supports
+ it.
+
--oidc-client-secret string, $CODER_OIDC_CLIENT_SECRET
Client secret to use for Login with OIDC.
@@ -334,6 +351,11 @@ can safely ignore these settings.
--oidc-issuer-url string, $CODER_OIDC_ISSUER_URL
Issuer URL to use for Login with OIDC.
+ --oidc-group-regex-filter regexp, $CODER_OIDC_GROUP_REGEX_FILTER (default: .*)
+ If provided any group name not matching the regex is ignored. This
+ allows for filtering out groups that are not needed. This filter is
+ applied after the group mapping.
+
--oidc-scopes string-array, $CODER_OIDC_SCOPES (default: openid,profile,email)
Scopes to grant when authenticating with OIDC.
@@ -373,6 +395,10 @@ updating, and deleting workspace resources.
--provisioner-daemon-poll-jitter duration, $CODER_PROVISIONER_DAEMON_POLL_JITTER (default: 100ms)
Random jitter added to the poll interval.
+ --provisioner-daemon-psk string, $CODER_PROVISIONER_DAEMON_PSK
+ Pre-shared key to authenticate external provisioner daemons to Coder
+ server.
+
--provisioner-daemons int, $CODER_PROVISIONER_DAEMONS (default: 3)
Number of provisioner daemons to create on start. If builds are stuck
in queued state for a long time, consider increasing this.
diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go
index 6c0f7aa06e614..a0f2004d7a824 100644
--- a/enterprise/cli/workspaceproxy.go
+++ b/enterprise/cli/workspaceproxy.go
@@ -8,9 +8,9 @@ import (
"github.com/fatih/color"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/cli/cliui"
+ "github.com/coder/coder/v2/codersdk"
)
func (r *RootCmd) workspaceProxy() *clibase.Cmd {
diff --git a/enterprise/cli/workspaceproxy_test.go b/enterprise/cli/workspaceproxy_test.go
index e7396ab12191f..fd9d241172560 100644
--- a/enterprise/cli/workspaceproxy_test.go
+++ b/enterprise/cli/workspaceproxy_test.go
@@ -8,13 +8,13 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clitest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func Test_ProxyCRUD(t *testing.T) {
diff --git a/enterprise/cmd/coder/main.go b/enterprise/cmd/coder/main.go
index b44275e3696a5..0aa1400c5c32f 100644
--- a/enterprise/cmd/coder/main.go
+++ b/enterprise/cmd/coder/main.go
@@ -3,7 +3,7 @@ package main
import (
_ "time/tzdata"
- entcli "github.com/coder/coder/enterprise/cli"
+ entcli "github.com/coder/coder/v2/enterprise/cli"
)
func main() {
diff --git a/enterprise/coderd/appearance.go b/enterprise/coderd/appearance.go
index c4dc8b34e1941..74d4bcb769515 100644
--- a/enterprise/coderd/appearance.go
+++ b/enterprise/coderd/appearance.go
@@ -12,9 +12,9 @@ import (
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
var DefaultSupportLinks = []codersdk.LinkConfig{
diff --git a/enterprise/coderd/appearance_test.go b/enterprise/coderd/appearance_test.go
index 6f564eaa3a680..8ee2c071377d0 100644
--- a/enterprise/coderd/appearance_test.go
+++ b/enterprise/coderd/appearance_test.go
@@ -9,15 +9,15 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/enterprise/coderd"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/enterprise/coderd"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
func TestServiceBanners(t *testing.T) {
@@ -111,7 +111,7 @@ func TestServiceBanners(t *testing.T) {
agentClient.SetSessionToken(authToken)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
diff --git a/enterprise/coderd/authorize_test.go b/enterprise/coderd/authorize_test.go
index 68452bc52b38f..0daf08cbc8734 100644
--- a/enterprise/coderd/authorize_test.go
+++ b/enterprise/coderd/authorize_test.go
@@ -7,12 +7,12 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/testutil"
)
func TestCheckACLPermissions(t *testing.T) {
diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go
index c3cc5e0be5ccd..a2be3e466e2ab 100644
--- a/enterprise/coderd/coderd.go
+++ b/enterprise/coderd/coderd.go
@@ -22,22 +22,22 @@ import (
"github.com/prometheus/client_golang/prometheus"
"cdr.dev/slog"
- "github.com/coder/coder/coderd"
- agplaudit "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- agplschedule "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/enterprise/coderd/proxyhealth"
- "github.com/coder/coder/enterprise/coderd/schedule"
- "github.com/coder/coder/enterprise/derpmesh"
- "github.com/coder/coder/enterprise/replicasync"
- "github.com/coder/coder/enterprise/tailnet"
- "github.com/coder/coder/provisionerd/proto"
- agpltailnet "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/coderd"
+ agplaudit "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ agplschedule "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/enterprise/coderd/proxyhealth"
+ "github.com/coder/coder/v2/enterprise/coderd/schedule"
+ "github.com/coder/coder/v2/enterprise/derpmesh"
+ "github.com/coder/coder/v2/enterprise/replicasync"
+ "github.com/coder/coder/v2/enterprise/tailnet"
+ "github.com/coder/coder/v2/provisionerd/proto"
+ agpltailnet "github.com/coder/coder/v2/tailnet"
)
// New constructs an Enterprise coderd API instance.
@@ -67,6 +67,10 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
AGPL: coderd.New(options.Options),
Options: options,
+ provisionerDaemonAuth: &provisionerDaemonAuth{
+ psk: options.ProvisionerDaemonPSK,
+ authorizer: options.Authorizer,
+ },
}
defer func() {
if err != nil {
@@ -126,6 +130,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
})
r.Route("/licenses", func(r chi.Router) {
r.Use(apiKeyMiddleware)
+ r.Post("/refresh-entitlements", api.postRefreshEntitlements)
r.Post("/", api.postLicense)
r.Get("/", api.licenses)
r.Delete("/{id}", api.deleteLicense)
@@ -163,6 +168,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
)
r.Get("/coordinate", api.workspaceProxyCoordinate)
r.Post("/issue-signed-app-token", api.workspaceProxyIssueSignedAppToken)
+ r.Post("/app-stats", api.workspaceProxyReportAppStats)
r.Post("/register", api.workspaceProxyRegister)
r.Post("/deregister", api.workspaceProxyDeregister)
})
@@ -193,14 +199,21 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Get("/", api.groupByOrganization)
})
})
+ // TODO: provisioner daemons are not scoped to organizations in the database, so placing them
+ // under an organization route doesn't make sense. In order to allow the /serve endpoint to
+ // work with a pre-shared key (PSK) without an API key, these routes will simply ignore the
+ // value of {organization}. That is, the route will work with any organization ID, whether or
+ // not it exits. This doesn't leak any information about the existence of organizations, so is
+ // fine from a security perspective, but might be a little surprising.
+ //
+ // We may in future decide to scope provisioner daemons to organizations, so we'll keep the API
+ // route as is.
r.Route("/organizations/{organization}/provisionerdaemons", func(r chi.Router) {
r.Use(
api.provisionerDaemonsEnabledMW,
- apiKeyMiddleware,
- httpmw.ExtractOrganizationParam(api.Database),
)
- r.Get("/", api.provisionerDaemons)
- r.Get("/serve", api.provisionerDaemonServe)
+ r.With(apiKeyMiddleware).Get("/", api.provisionerDaemons)
+ r.With(apiKeyMiddlewareOptional).Get("/serve", api.provisionerDaemonServe)
})
r.Route("/templates/{template}/acl", func(r chi.Router) {
r.Use(
@@ -362,6 +375,9 @@ type Options struct {
EntitlementsUpdateInterval time.Duration
ProxyHealthInterval time.Duration
Keys map[string]ed25519.PublicKey
+
+ // optional pre-shared key for authentication of external provisioner daemons
+ ProvisionerDaemonPSK string
}
type API struct {
@@ -383,13 +399,18 @@ type API struct {
entitlementsUpdateMu sync.Mutex
entitlementsMu sync.RWMutex
entitlements codersdk.Entitlements
+
+ provisionerDaemonAuth *provisionerDaemonAuth
}
func (api *API) Close() error {
- api.cancel()
+ // Replica manager should be closed first. This is because the replica
+ // manager updates the replica's table in the database when it closes.
+ // This tells other Coderds that it is now offline.
if api.replicaManager != nil {
_ = api.replicaManager.Close()
}
+ api.cancel()
if api.derpMesh != nil {
_ = api.derpMesh.Close()
}
@@ -481,7 +502,10 @@ func (api *API) updateEntitlements(ctx context.Context) error {
if initial, changed, enabled := featureChanged(codersdk.FeatureTemplateRBAC); shouldUpdate(initial, changed, enabled) {
if enabled {
- committer := committer{Database: api.Database}
+ committer := committer{
+ Log: api.Logger.Named("quota_committer"),
+ Database: api.Database,
+ }
ptr := proto.QuotaCommitter(&committer)
api.AGPL.QuotaCommitter.Store(&ptr)
} else {
@@ -491,7 +515,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
if initial, changed, enabled := featureChanged(codersdk.FeatureAdvancedTemplateScheduling); shouldUpdate(initial, changed, enabled) {
if enabled {
- templateStore := schedule.NewEnterpriseTemplateScheduleStore()
+ templateStore := schedule.NewEnterpriseTemplateScheduleStore(api.AGPL.UserQuietHoursScheduleStore)
templateStoreInterface := agplschedule.TemplateScheduleStore(templateStore)
api.AGPL.TemplateScheduleStore.Store(&templateStoreInterface)
} else {
@@ -578,7 +602,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
if initial, changed, enabled := featureChanged(codersdk.FeatureWorkspaceProxy); shouldUpdate(initial, changed, enabled) {
if enabled {
- fn := derpMapper(api.Logger, api.DeploymentValues, api.ProxyHealth)
+ fn := derpMapper(api.Logger, api.ProxyHealth)
api.AGPL.DERPMapper.Store(&fn)
} else {
api.AGPL.DERPMapper.Store(nil)
@@ -643,7 +667,7 @@ var (
lastDerpConflictLog time.Time
)
-func derpMapper(logger slog.Logger, cfg *codersdk.DeploymentValues, proxyHealth *proxyhealth.ProxyHealth) func(*tailcfg.DERPMap) *tailcfg.DERPMap {
+func derpMapper(logger slog.Logger, proxyHealth *proxyhealth.ProxyHealth) func(*tailcfg.DERPMap) *tailcfg.DERPMap {
return func(derpMap *tailcfg.DERPMap) *tailcfg.DERPMap {
derpMap = derpMap.Clone()
@@ -737,43 +761,22 @@ func derpMapper(logger slog.Logger, cfg *codersdk.DeploymentValues, proxyHealth
}
}
- var stunNodes []*tailcfg.DERPNode
- if !cfg.DERP.Config.BlockDirect.Value() {
- stunNodes, err = agpltailnet.STUNNodes(regionID, cfg.DERP.Server.STUNAddresses)
- if err != nil {
- // Log a warning if we haven't logged one in the last
- // minute.
- lastDerpConflictMutex.Lock()
- shouldLog := lastDerpConflictLog.IsZero() || time.Since(lastDerpConflictLog) > time.Minute
- if shouldLog {
- lastDerpConflictLog = time.Now()
- }
- lastDerpConflictMutex.Unlock()
- if shouldLog {
- logger.Error(context.Background(), "failed to calculate STUN nodes", slog.Error(err))
- }
-
- // No continue because we can keep going.
- stunNodes = []*tailcfg.DERPNode{}
- }
- }
-
- nodes := append(stunNodes, &tailcfg.DERPNode{
- Name: fmt.Sprintf("%da", regionID),
- RegionID: regionID,
- HostName: u.Hostname(),
- DERPPort: portInt,
- STUNPort: -1,
- ForceHTTP: u.Scheme == "http",
- })
-
derpMap.Regions[regionID] = &tailcfg.DERPRegion{
// EmbeddedRelay ONLY applies to the primary.
EmbeddedRelay: false,
RegionID: regionID,
RegionCode: regionCode,
RegionName: regionName,
- Nodes: nodes,
+ Nodes: []*tailcfg.DERPNode{
+ {
+ Name: fmt.Sprintf("%da", regionID),
+ RegionID: regionID,
+ HostName: u.Hostname(),
+ DERPPort: portInt,
+ STUNPort: -1,
+ ForceHTTP: u.Scheme == "http",
+ },
+ },
}
}
@@ -803,6 +806,17 @@ func (api *API) runEntitlementsLoop(ctx context.Context) {
updates := make(chan struct{}, 1)
subscribed := false
+ defer func() {
+ // If this function ends, it means the context was canceled and this
+ // coderd is shutting down. In this case, post a pubsub message to
+ // tell other coderd's to resync their entitlements. This is required to
+ // make sure things like replica counts are updated in the UI.
+ // Ignore the error, as this is just a best effort. If it fails,
+ // the system will eventually recover as replicas timeout
+ // if their heartbeats stop. The best effort just tries to update the
+ // UI faster if it succeeds.
+ _ = api.Pubsub.Publish(PubsubEventLicenses, []byte("going away"))
+ }()
for {
select {
case <-ctx.Done():
diff --git a/enterprise/coderd/coderd_test.go b/enterprise/coderd/coderd_test.go
index 2271c79064b4f..123a22938fff2 100644
--- a/enterprise/coderd/coderd_test.go
+++ b/enterprise/coderd/coderd_test.go
@@ -8,22 +8,22 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/rbac"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
- agplaudit "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/audit"
- "github.com/coder/coder/enterprise/coderd"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/testutil"
+ agplaudit "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/audit"
+ "github.com/coder/coder/v2/enterprise/coderd"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go
index 92e0b627d60ae..81e43c4fd5755 100644
--- a/enterprise/coderd/coderdenttest/coderdenttest.go
+++ b/enterprise/coderd/coderdenttest/coderdenttest.go
@@ -10,17 +10,17 @@ import (
"testing"
"time"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd"
- "github.com/coder/coder/enterprise/coderd/license"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
)
const (
@@ -56,6 +56,7 @@ type Options struct {
DontAddLicense bool
DontAddFirstUser bool
ReplicaSyncUpdateInterval time.Duration
+ ProvisionerDaemonPSK string
}
// New constructs a codersdk client connected to an in-memory Enterprise API instance.
@@ -94,6 +95,7 @@ func NewWithAPI(t *testing.T, options *Options) (
Keys: Keys,
ProxyHealthInterval: options.ProxyHealthInterval,
DefaultQuietHoursSchedule: oop.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Value(),
+ ProvisionerDaemonPSK: options.ProvisionerDaemonPSK,
})
require.NoError(t, err)
setHandler(coderAPI.AGPL.RootHandler)
diff --git a/enterprise/coderd/coderdenttest/coderdenttest_test.go b/enterprise/coderd/coderdenttest/coderdenttest_test.go
index c3db8b31eadc2..ef828ac699e58 100644
--- a/enterprise/coderd/coderdenttest/coderdenttest_test.go
+++ b/enterprise/coderd/coderdenttest/coderdenttest_test.go
@@ -3,7 +3,7 @@ package coderdenttest_test
import (
"testing"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
)
func TestNew(t *testing.T) {
diff --git a/enterprise/coderd/coderdenttest/proxytest.go b/enterprise/coderd/coderdenttest/proxytest.go
index c544e14b44a46..8a28b077c16f4 100644
--- a/enterprise/coderd/coderdenttest/proxytest.go
+++ b/enterprise/coderd/coderdenttest/proxytest.go
@@ -19,10 +19,10 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd"
- "github.com/coder/coder/enterprise/wsproxy"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd"
+ "github.com/coder/coder/v2/enterprise/wsproxy"
)
type ProxyOptions struct {
@@ -110,6 +110,10 @@ func NewWorkspaceProxy(t *testing.T, coderdAPI *coderd.API, owner *codersdk.Clie
})
require.NoError(t, err, "failed to create workspace proxy")
+ // Inherit collector options from coderd, but keep the wsproxy reporter.
+ statsCollectorOptions := coderdAPI.Options.WorkspaceAppsStatsCollectorOptions
+ statsCollectorOptions.Reporter = nil
+
wssrv, err := wsproxy.New(ctx, &wsproxy.Options{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
Experiments: options.Experiments,
@@ -129,6 +133,7 @@ func NewWorkspaceProxy(t *testing.T, coderdAPI *coderd.API, owner *codersdk.Clie
DERPEnabled: !options.DerpDisabled,
DERPOnly: options.DerpOnly,
DERPServerRelayAddress: accessURL.String(),
+ StatsCollectorOptions: statsCollectorOptions,
})
require.NoError(t, err)
t.Cleanup(func() {
diff --git a/enterprise/coderd/coderdenttest/swagger_test.go b/enterprise/coderd/coderdenttest/swagger_test.go
index 7de2678d55dba..c8b95174867d9 100644
--- a/enterprise/coderd/coderdenttest/swagger_test.go
+++ b/enterprise/coderd/coderdenttest/swagger_test.go
@@ -5,8 +5,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
)
func TestEnterpriseEndpointsDocumented(t *testing.T) {
diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go
index b6f126e1f62e0..c209fd5a80255 100644
--- a/enterprise/coderd/groups.go
+++ b/enterprise/coderd/groups.go
@@ -8,13 +8,13 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Create group for organization
@@ -46,9 +46,9 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request)
return
}
- if req.Name == database.AllUsersGroup {
+ if req.Name == database.EveryoneGroup {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: fmt.Sprintf("%q is a reserved keyword and cannot be used for a group name.", database.AllUsersGroup),
+ Message: fmt.Sprintf("%q is a reserved keyword and cannot be used for a group name.", database.EveryoneGroup),
})
return
}
@@ -81,9 +81,11 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request)
// @Summary Update group by name
// @ID update-group-by-name
// @Security CoderSessionToken
+// @Accept json
// @Produce json
// @Tags Enterprise
// @Param group path string true "Group name"
+// @Param request body codersdk.PatchGroupRequest true "Patch group request"
// @Success 200 {object} codersdk.Group
// @Router /groups/{group} [patch]
func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
@@ -100,36 +102,56 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
)
defer commitAudit()
- currentMembers, currentMembersErr := api.Database.GetGroupMembers(ctx, group.ID)
- if currentMembersErr != nil {
- httpapi.InternalServerError(rw, currentMembersErr)
+ var req codersdk.PatchGroupRequest
+ if !httpapi.Read(ctx, rw, r, &req) {
return
}
- aReq.Old = group.Auditable(currentMembers)
+ // If the name matches the existing group name pretend we aren't
+ // updating the name at all.
+ if req.Name == group.Name {
+ req.Name = ""
+ }
- var req codersdk.PatchGroupRequest
- if !httpapi.Read(ctx, rw, r, &req) {
+ if group.IsEveryone() && req.Name != "" {
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: fmt.Sprintf("Cannot rename the %q group!", database.EveryoneGroup),
+ })
return
}
- if req.Name != "" && req.Name == database.AllUsersGroup {
+ if group.IsEveryone() && (req.DisplayName != nil && *req.DisplayName != "") {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: fmt.Sprintf("%q is a reserved group name!", database.AllUsersGroup),
+ Message: fmt.Sprintf("Cannot update the Display Name for the %q group!", database.EveryoneGroup),
})
return
}
- // If the name matches the existing group name pretend we aren't
- // updating the name at all.
- if req.Name == group.Name {
- req.Name = ""
+ if req.Name == database.EveryoneGroup {
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: fmt.Sprintf("%q is a reserved group name!", database.EveryoneGroup),
+ })
+ return
}
users := make([]string, 0, len(req.AddUsers)+len(req.RemoveUsers))
users = append(users, req.AddUsers...)
users = append(users, req.RemoveUsers...)
+ if len(users) > 0 && group.Name == database.EveryoneGroup {
+ httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
+ Message: fmt.Sprintf("Cannot add or remove users from the %q group!", database.EveryoneGroup),
+ })
+ return
+ }
+
+ currentMembers, err := api.Database.GetGroupMembers(ctx, group.ID)
+ if err != nil {
+ httpapi.InternalServerError(rw, err)
+ return
+ }
+ aReq.Old = group.Auditable(currentMembers)
+
for _, id := range users {
if _, err := uuid.Parse(id); err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -154,6 +176,7 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
return
}
}
+
if req.Name != "" && req.Name != group.Name {
_, err := api.Database.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{
OrganizationID: group.OrganizationID,
@@ -167,8 +190,7 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
}
}
- err := api.Database.InTx(func(tx database.Store) error {
- var err error
+ err = database.ReadModifyUpdate(api.Database, func(tx database.Store) error {
group, err = tx.GetGroupByID(ctx, group.ID)
if err != nil {
return xerrors.Errorf("get group by ID: %w", err)
@@ -228,7 +250,8 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
}
}
return nil
- }, nil)
+ })
+
if database.IsUniqueViolation(err) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Cannot add the same user to a group twice!",
@@ -281,6 +304,13 @@ func (api *API) deleteGroup(rw http.ResponseWriter, r *http.Request) {
)
defer commitAudit()
+ if group.Name == database.EveryoneGroup {
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: fmt.Sprintf("%q is a reserved group and cannot be deleted!", database.EveryoneGroup),
+ })
+ return
+ }
+
groupMembers, getMembersErr := api.Database.GetGroupMembers(ctx, group.ID)
if getMembersErr != nil {
httpapi.InternalServerError(rw, getMembersErr)
@@ -289,13 +319,6 @@ func (api *API) deleteGroup(rw http.ResponseWriter, r *http.Request) {
aReq.Old = group.Auditable(groupMembers)
- if group.Name == database.AllUsersGroup {
- httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: fmt.Sprintf("%q is a reserved group and cannot be deleted!", database.AllUsersGroup),
- })
- return
- }
-
err := api.Database.DeleteGroupByID(ctx, group.ID)
if err != nil {
httpapi.InternalServerError(rw, err)
@@ -320,12 +343,12 @@ func (api *API) groupByOrganization(rw http.ResponseWriter, r *http.Request) {
api.group(rw, r)
}
-// @Summary Get group by name
-// @ID get-group-by-name
+// @Summary Get group by ID
+// @ID get-group-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
-// @Param group path string true "Group name"
+// @Param group path string true "Group id"
// @Success 200 {object} codersdk.Group
// @Router /groups/{group} [get]
func (api *API) group(rw http.ResponseWriter, r *http.Request) {
@@ -409,6 +432,7 @@ func convertGroup(g database.Group, users []database.User) codersdk.Group {
AvatarURL: g.AvatarURL,
QuotaAllowance: int(g.QuotaAllowance),
Members: convertUsers(users, orgs),
+ Source: codersdk.GroupSource(g.Source),
}
}
diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go
index 5999fa47b1f6d..2f16aa7884934 100644
--- a/enterprise/coderd/groups_test.go
+++ b/enterprise/coderd/groups_test.go
@@ -7,14 +7,14 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/testutil"
)
func TestCreateGroup(t *testing.T) {
@@ -105,7 +105,7 @@ func TestCreateGroup(t *testing.T) {
}})
ctx := testutil.Context(t, testutil.WaitLong)
_, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
- Name: database.AllUsersGroup,
+ Name: database.EveryoneGroup,
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
@@ -399,7 +399,7 @@ func TestPatchGroup(t *testing.T) {
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
- t.Run("allUsers", func(t *testing.T) {
+ t.Run("ReservedName", func(t *testing.T) {
t.Parallel()
client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
@@ -414,13 +414,114 @@ func TestPatchGroup(t *testing.T) {
require.NoError(t, err)
group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
- Name: database.AllUsersGroup,
+ Name: database.EveryoneGroup,
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})
+
+ t.Run("Everyone", func(t *testing.T) {
+ t.Parallel()
+ t.Run("NoUpdateName", func(t *testing.T) {
+ t.Parallel()
+
+ client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureTemplateRBAC: 1,
+ },
+ }})
+ ctx := testutil.Context(t, testutil.WaitLong)
+ _, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
+ Name: "hi",
+ })
+ require.Error(t, err)
+ cerr, ok := codersdk.AsError(err)
+ require.True(t, ok)
+ require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
+ })
+
+ t.Run("NoUpdateDisplayName", func(t *testing.T) {
+ t.Parallel()
+
+ client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureTemplateRBAC: 1,
+ },
+ }})
+ ctx := testutil.Context(t, testutil.WaitLong)
+ _, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
+ DisplayName: ptr.Ref("hi"),
+ })
+ require.Error(t, err)
+ cerr, ok := codersdk.AsError(err)
+ require.True(t, ok)
+ require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
+ })
+
+ t.Run("NoAddUsers", func(t *testing.T) {
+ t.Parallel()
+
+ client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureTemplateRBAC: 1,
+ },
+ }})
+ _, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ _, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
+ AddUsers: []string{user2.ID.String()},
+ })
+ require.Error(t, err)
+ cerr, ok := codersdk.AsError(err)
+ require.True(t, ok)
+ require.Equal(t, http.StatusForbidden, cerr.StatusCode())
+ })
+
+ t.Run("NoRmUsers", func(t *testing.T) {
+ t.Parallel()
+
+ client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureTemplateRBAC: 1,
+ },
+ }})
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ _, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
+ RemoveUsers: []string{user.UserID.String()},
+ })
+ require.Error(t, err)
+ cerr, ok := codersdk.AsError(err)
+ require.True(t, ok)
+ require.Equal(t, http.StatusForbidden, cerr.StatusCode())
+ })
+
+ t.Run("UpdateQuota", func(t *testing.T) {
+ t.Parallel()
+
+ client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureTemplateRBAC: 1,
+ },
+ }})
+
+ ctx := testutil.Context(t, testutil.WaitLong)
+ group, err := client.Group(ctx, user.OrganizationID)
+ require.NoError(t, err)
+
+ require.Equal(t, 0, group.QuotaAllowance)
+
+ expectedQuota := 123
+ group, err = client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
+ QuotaAllowance: ptr.Ref(expectedQuota),
+ })
+ require.NoError(t, err)
+ require.Equal(t, expectedQuota, group.QuotaAllowance)
+ })
+ })
}
// TODO: test auth.
@@ -591,13 +692,17 @@ func TestGroup(t *testing.T) {
codersdk.FeatureTemplateRBAC: 1,
},
}})
+ _, user1 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
+ _, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
+
ctx := testutil.Context(t, testutil.WaitLong)
// The 'Everyone' group always has an ID that matches the organization ID.
group, err := client.Group(ctx, user.OrganizationID)
require.NoError(t, err)
- require.Len(t, group.Members, 0)
+ require.Len(t, group.Members, 3)
require.Equal(t, "Everyone", group.Name)
require.Equal(t, user.OrganizationID, group.OrganizationID)
+ require.Contains(t, group.Members, user1, user2)
})
}
@@ -641,7 +746,8 @@ func TestGroups(t *testing.T) {
groups, err := client.GroupsByOrganization(ctx, user.OrganizationID)
require.NoError(t, err)
- require.Len(t, groups, 2)
+ // 'Everyone' group + 2 custom groups.
+ require.Len(t, groups, 3)
require.Contains(t, groups, group1)
require.Contains(t, groups, group2)
})
diff --git a/enterprise/coderd/insights_test.go b/enterprise/coderd/insights_test.go
index be74c8dad2808..0af7b7ad94840 100644
--- a/enterprise/coderd/insights_test.go
+++ b/enterprise/coderd/insights_test.go
@@ -9,11 +9,11 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/testutil"
)
func TestTemplateInsightsWithTemplateAdminACL(t *testing.T) {
diff --git a/enterprise/coderd/license/license.go b/enterprise/coderd/license/license.go
index a41dba4be3972..4abf8dfcd218e 100644
--- a/enterprise/coderd/license/license.go
+++ b/enterprise/coderd/license/license.go
@@ -12,9 +12,9 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/codersdk"
)
// Entitlements processes licenses to return whether features are enabled or not.
@@ -225,6 +225,7 @@ func Entitlements(
entitlements.Features[featureName] = feature
}
}
+ entitlements.RefreshedAt = now
return entitlements, nil
}
diff --git a/enterprise/coderd/license/license_test.go b/enterprise/coderd/license/license_test.go
index 546333b87a2af..cb76296fdc601 100644
--- a/enterprise/coderd/license/license_test.go
+++ b/enterprise/coderd/license/license_test.go
@@ -10,11 +10,11 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
)
func TestEntitlements(t *testing.T) {
diff --git a/enterprise/coderd/licenses.go b/enterprise/coderd/licenses.go
index 24085ee9a7bea..aff3f41fa5a76 100644
--- a/enterprise/coderd/licenses.go
+++ b/enterprise/coderd/licenses.go
@@ -8,6 +8,7 @@ import (
_ "embed"
"encoding/base64"
"encoding/json"
+ "fmt"
"net/http"
"strconv"
"strings"
@@ -19,13 +20,13 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/license"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
)
const (
@@ -150,6 +151,75 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusCreated, convertLicense(dl, rawClaims))
}
+// postRefreshEntitlements forces an `updateEntitlements` call and publishes
+// a message to the PubsubEventLicenses topic to force other replicas
+// to update their entitlements.
+// Updates happen automatically on a timer, however that time is every 10 minutes,
+// and we want to be able to force an update immediately in some cases.
+//
+// @Summary Update license entitlements
+// @ID update-license-entitlements
+// @Security CoderSessionToken
+// @Produce json
+// @Tags Organizations
+// @Success 201 {object} codersdk.Response
+// @Router /licenses/refresh-entitlements [post]
+func (api *API) postRefreshEntitlements(rw http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // If the user cannot create a new license, then they cannot refresh entitlements.
+ // Refreshing entitlements is a way to force a refresh of the license, so it is
+ // equivalent to creating a new license.
+ if !api.AGPL.Authorize(r, rbac.ActionCreate, rbac.ResourceLicense) {
+ httpapi.Forbidden(rw)
+ return
+ }
+
+ // Prevent abuse by limiting how often we allow a forced refresh.
+ now := time.Now()
+ if diff := now.Sub(api.entitlements.RefreshedAt); diff < time.Minute {
+ wait := time.Minute - diff
+ rw.Header().Set("Retry-After", strconv.Itoa(int(wait.Seconds())))
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: fmt.Sprintf("Entitlements already recently refreshed, please wait %d seconds to force a new refresh", int(wait.Seconds())),
+ Detail: fmt.Sprintf("Last refresh at %s", now.UTC().String()),
+ })
+ return
+ }
+
+ err := api.replicaManager.UpdateNow(ctx)
+ if err != nil {
+ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Failed to sync replicas",
+ Detail: err.Error(),
+ })
+ return
+ }
+
+ err = api.updateEntitlements(ctx)
+ if err != nil {
+ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Failed to update entitlements",
+ Detail: err.Error(),
+ })
+ return
+ }
+
+ err = api.Pubsub.Publish(PubsubEventLicenses, []byte("refresh"))
+ if err != nil {
+ api.Logger.Error(context.Background(), "failed to publish forced entitlement update", slog.Error(err))
+ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Failed to publish forced entitlement update. Other replicas might not be updated.",
+ Detail: err.Error(),
+ })
+ return
+ }
+
+ httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{
+ Message: "Entitlements updated",
+ })
+}
+
// @Summary Get licenses
// @ID get-licenses
// @Security CoderSessionToken
diff --git a/enterprise/coderd/licenses_test.go b/enterprise/coderd/licenses_test.go
index 59b2e7f306f11..be340e44d3c54 100644
--- a/enterprise/coderd/licenses_test.go
+++ b/enterprise/coderd/licenses_test.go
@@ -9,10 +9,10 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/testutil"
)
func TestPostLicense(t *testing.T) {
diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go
index 055704a6bcb11..b82c2a6c750f1 100644
--- a/enterprise/coderd/provisionerdaemons.go
+++ b/enterprise/coderd/provisionerdaemons.go
@@ -2,6 +2,7 @@ package coderd
import (
"context"
+ "crypto/subtle"
"database/sql"
"encoding/json"
"errors"
@@ -21,14 +22,14 @@ import (
"storj.io/drpc/drpcserver"
"cdr.dev/slog"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/provisionerdserver"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisionerd/proto"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/provisionerdserver"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisionerd/proto"
)
func (api *API) provisionerDaemonsEnabledMW(next http.Handler) http.Handler {
@@ -87,6 +88,40 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, apiDaemons)
}
+type provisionerDaemonAuth struct {
+ psk string
+ authorizer rbac.Authorizer
+}
+
+// authorize returns mutated tags and true if the given HTTP request is authorized to access the provisioner daemon
+// protobuf API, and returns nil, false otherwise.
+func (p *provisionerDaemonAuth) authorize(r *http.Request, tags map[string]string) (map[string]string, bool) {
+ ctx := r.Context()
+ apiKey, ok := httpmw.APIKeyOptional(r)
+ if ok {
+ tags = provisionerdserver.MutateTags(apiKey.UserID, tags)
+ if tags[provisionerdserver.TagScope] == provisionerdserver.ScopeUser {
+ // Any authenticated user can create provisioner daemons scoped
+ // for jobs that they own,
+ return tags, true
+ }
+ ua := httpmw.UserAuthorization(r)
+ if err := p.authorizer.Authorize(ctx, ua.Actor, rbac.ActionCreate, rbac.ResourceProvisionerDaemon); err == nil {
+ // User is allowed to create provisioner daemons
+ return tags, true
+ }
+ }
+
+ // Check for PSK
+ if p.psk != "" {
+ psk := r.Header.Get(codersdk.ProvisionerDaemonPSK)
+ if subtle.ConstantTimeCompare([]byte(p.psk), []byte(psk)) == 1 {
+ return tags, true
+ }
+ }
+ return nil, false
+}
+
// Serves the provisioner daemon protobuf API over a WebSocket.
//
// @Summary Serve provisioner daemon
@@ -134,19 +169,11 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
}
}
- // Any authenticated user can create provisioner daemons scoped
- // for jobs that they own, but only authorized users can create
- // globally scoped provisioners that attach to all jobs.
- apiKey := httpmw.APIKey(r)
- tags = provisionerdserver.MutateTags(apiKey.UserID, tags)
-
- if tags[provisionerdserver.TagScope] == provisionerdserver.ScopeOrganization {
- if !api.AGPL.Authorize(r, rbac.ActionCreate, rbac.ResourceProvisionerDaemon) {
- httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
- Message: "You aren't allowed to create provisioner daemons for the organization.",
- })
- return
- }
+ tags, authorized := api.provisionerDaemonAuth.authorize(r, tags)
+ if !authorized {
+ httpapi.Write(ctx, rw, http.StatusForbidden,
+ codersdk.Response{Message: "You aren't allowed to create provisioner daemons"})
+ return
}
provisioners := make([]database.ProvisionerType, 0)
diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go
index 28a89431b4f00..e190a3df90e20 100644
--- a/enterprise/coderd/provisionerdaemons_test.go
+++ b/enterprise/coderd/provisionerdaemons_test.go
@@ -9,14 +9,15 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/provisionerdserver"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/provisionerdserver"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func TestProvisionerDaemonServe(t *testing.T) {
@@ -28,23 +29,43 @@ func TestProvisionerDaemonServe(t *testing.T) {
codersdk.FeatureExternalProvisionerDaemons: 1,
},
}})
- srv, err := client.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{
- codersdk.ProvisionerTypeEcho,
- }, map[string]string{})
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+ srv, err := client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Organization: user.OrganizationID,
+ Provisioners: []codersdk.ProvisionerType{
+ codersdk.ProvisionerTypeEcho,
+ },
+ Tags: map[string]string{},
+ })
require.NoError(t, err)
srv.DRPCConn().Close()
+ daemons, err := client.ProvisionerDaemons(ctx)
+ require.NoError(t, err)
+ require.Len(t, daemons, 1)
})
t.Run("NoLicense", func(t *testing.T) {
t.Parallel()
client, user := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
- _, err := client.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{
- codersdk.ProvisionerTypeEcho,
- }, map[string]string{})
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+ _, err := client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Organization: user.OrganizationID,
+ Provisioners: []codersdk.ProvisionerType{
+ codersdk.ProvisionerTypeEcho,
+ },
+ Tags: map[string]string{},
+ })
require.Error(t, err)
var apiError *codersdk.Error
require.ErrorAs(t, err, &apiError)
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
+
+ // querying provisioner daemons is forbidden without license
+ _, err = client.ProvisionerDaemons(ctx)
+ require.ErrorAs(t, err, &apiError)
+ require.Equal(t, http.StatusForbidden, apiError.StatusCode())
})
t.Run("Organization", func(t *testing.T) {
@@ -55,15 +76,24 @@ func TestProvisionerDaemonServe(t *testing.T) {
},
}})
another, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleOrgAdmin(user.OrganizationID))
- _, err := another.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{
- codersdk.ProvisionerTypeEcho,
- }, map[string]string{
- provisionerdserver.TagScope: provisionerdserver.ScopeOrganization,
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Organization: user.OrganizationID,
+ Provisioners: []codersdk.ProvisionerType{
+ codersdk.ProvisionerTypeEcho,
+ },
+ Tags: map[string]string{
+ provisionerdserver.TagScope: provisionerdserver.ScopeOrganization,
+ },
})
require.Error(t, err)
var apiError *codersdk.Error
require.ErrorAs(t, err, &apiError)
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
+ daemons, err := client.ProvisionerDaemons(ctx)
+ require.NoError(t, err)
+ require.Len(t, daemons, 0)
})
t.Run("OrganizationNoPerms", func(t *testing.T) {
@@ -74,15 +104,24 @@ func TestProvisionerDaemonServe(t *testing.T) {
},
}})
another, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
- _, err := another.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{
- codersdk.ProvisionerTypeEcho,
- }, map[string]string{
- provisionerdserver.TagScope: provisionerdserver.ScopeOrganization,
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Organization: user.OrganizationID,
+ Provisioners: []codersdk.ProvisionerType{
+ codersdk.ProvisionerTypeEcho,
+ },
+ Tags: map[string]string{
+ provisionerdserver.TagScope: provisionerdserver.ScopeOrganization,
+ },
})
require.Error(t, err)
var apiError *codersdk.Error
require.ErrorAs(t, err, &apiError)
require.Equal(t, http.StatusForbidden, apiError.StatusCode())
+ daemons, err := client.ProvisionerDaemons(ctx)
+ require.NoError(t, err)
+ require.Len(t, daemons, 0)
})
t.Run("UserLocal", func(t *testing.T) {
@@ -100,9 +139,9 @@ func TestProvisionerDaemonServe(t *testing.T) {
authToken := uuid.NewString()
data, err := echo.Tar(&echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: []*proto.Response{{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -141,4 +180,129 @@ func TestProvisionerDaemonServe(t *testing.T) {
workspace := coderdtest.CreateWorkspace(t, another, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
})
+
+ t.Run("PSK", func(t *testing.T) {
+ t.Parallel()
+ client, user := coderdenttest.New(t, &coderdenttest.Options{
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ ProvisionerDaemonPSK: "provisionersftw",
+ })
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+ another := codersdk.New(client.URL)
+ srv, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Organization: user.OrganizationID,
+ Provisioners: []codersdk.ProvisionerType{
+ codersdk.ProvisionerTypeEcho,
+ },
+ Tags: map[string]string{
+ provisionerdserver.TagScope: provisionerdserver.ScopeOrganization,
+ },
+ PreSharedKey: "provisionersftw",
+ })
+ require.NoError(t, err)
+ err = srv.DRPCConn().Close()
+ require.NoError(t, err)
+ daemons, err := client.ProvisionerDaemons(ctx)
+ require.NoError(t, err)
+ require.Len(t, daemons, 1)
+ })
+
+ t.Run("BadPSK", func(t *testing.T) {
+ t.Parallel()
+ client, user := coderdenttest.New(t, &coderdenttest.Options{
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ ProvisionerDaemonPSK: "provisionersftw",
+ })
+ another := codersdk.New(client.URL)
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Organization: user.OrganizationID,
+ Provisioners: []codersdk.ProvisionerType{
+ codersdk.ProvisionerTypeEcho,
+ },
+ Tags: map[string]string{
+ provisionerdserver.TagScope: provisionerdserver.ScopeOrganization,
+ },
+ PreSharedKey: "the wrong key",
+ })
+ require.Error(t, err)
+ var apiError *codersdk.Error
+ require.ErrorAs(t, err, &apiError)
+ require.Equal(t, http.StatusForbidden, apiError.StatusCode())
+ daemons, err := client.ProvisionerDaemons(ctx)
+ require.NoError(t, err)
+ require.Len(t, daemons, 0)
+ })
+
+ t.Run("NoAuth", func(t *testing.T) {
+ t.Parallel()
+ client, user := coderdenttest.New(t, &coderdenttest.Options{
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ ProvisionerDaemonPSK: "provisionersftw",
+ })
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+ another := codersdk.New(client.URL)
+ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Organization: user.OrganizationID,
+ Provisioners: []codersdk.ProvisionerType{
+ codersdk.ProvisionerTypeEcho,
+ },
+ Tags: map[string]string{
+ provisionerdserver.TagScope: provisionerdserver.ScopeOrganization,
+ },
+ })
+ require.Error(t, err)
+ var apiError *codersdk.Error
+ require.ErrorAs(t, err, &apiError)
+ require.Equal(t, http.StatusForbidden, apiError.StatusCode())
+ daemons, err := client.ProvisionerDaemons(ctx)
+ require.NoError(t, err)
+ require.Len(t, daemons, 0)
+ })
+
+ t.Run("NoPSK", func(t *testing.T) {
+ t.Parallel()
+ client, user := coderdenttest.New(t, &coderdenttest.Options{
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureExternalProvisionerDaemons: 1,
+ },
+ },
+ })
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+ another := codersdk.New(client.URL)
+ _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
+ Organization: user.OrganizationID,
+ Provisioners: []codersdk.ProvisionerType{
+ codersdk.ProvisionerTypeEcho,
+ },
+ Tags: map[string]string{
+ provisionerdserver.TagScope: provisionerdserver.ScopeOrganization,
+ },
+ PreSharedKey: "provisionersftw",
+ })
+ require.Error(t, err)
+ var apiError *codersdk.Error
+ require.ErrorAs(t, err, &apiError)
+ require.Equal(t, http.StatusForbidden, apiError.StatusCode())
+ daemons, err := client.ProvisionerDaemons(ctx)
+ require.NoError(t, err)
+ require.Len(t, daemons, 0)
+ })
}
diff --git a/enterprise/coderd/proxyhealth/proxyhealth.go b/enterprise/coderd/proxyhealth/proxyhealth.go
index d95a44d5a0831..4b9e3f13982f8 100644
--- a/enterprise/coderd/proxyhealth/proxyhealth.go
+++ b/enterprise/coderd/proxyhealth/proxyhealth.go
@@ -17,10 +17,10 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/prometheusmetrics"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/prometheusmetrics"
+ "github.com/coder/coder/v2/codersdk"
)
type Status string
diff --git a/enterprise/coderd/proxyhealth/proxyhealth_test.go b/enterprise/coderd/proxyhealth/proxyhealth_test.go
index 10d8d69dba2ec..96502fa1f56e6 100644
--- a/enterprise/coderd/proxyhealth/proxyhealth_test.go
+++ b/enterprise/coderd/proxyhealth/proxyhealth_test.go
@@ -11,13 +11,13 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/proxyhealth"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/proxyhealth"
+ "github.com/coder/coder/v2/testutil"
)
func insertProxy(t *testing.T, db database.Store, url string) database.WorkspaceProxy {
diff --git a/enterprise/coderd/replicas.go b/enterprise/coderd/replicas.go
index 77e0c45aeff2c..536048aaac84a 100644
--- a/enterprise/coderd/replicas.go
+++ b/enterprise/coderd/replicas.go
@@ -3,10 +3,10 @@ package coderd
import (
"net/http"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
// replicas returns the number of replicas that are active in Coder.
diff --git a/enterprise/coderd/replicas_test.go b/enterprise/coderd/replicas_test.go
index 7133919c6715b..1081ec81e3d04 100644
--- a/enterprise/coderd/replicas_test.go
+++ b/enterprise/coderd/replicas_test.go
@@ -10,12 +10,12 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/testutil"
)
func TestReplicas(t *testing.T) {
diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go
index 278e315dda3af..f37d9ded8d187 100644
--- a/enterprise/coderd/schedule/template.go
+++ b/enterprise/coderd/schedule/template.go
@@ -6,10 +6,16 @@ import (
"time"
"github.com/google/uuid"
+ "go.opentelemetry.io/otel/attribute"
+ "go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- agpl "github.com/coder/coder/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ agpl "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
)
// EnterpriseTemplateScheduleStore provides an agpl.TemplateScheduleStore that
@@ -20,16 +26,35 @@ type EnterpriseTemplateScheduleStore struct {
// workspace build. This value is determined by a feature flag, licensing,
// and whether a default user quiet hours schedule is set.
UseRestartRequirement atomic.Bool
+
+ // UserQuietHoursScheduleStore is used when recalculating build deadlines on
+ // update.
+ UserQuietHoursScheduleStore *atomic.Pointer[agpl.UserQuietHoursScheduleStore]
+
+ // Custom time.Now() function to use in tests. Defaults to database.Now().
+ TimeNowFn func() time.Time
}
var _ agpl.TemplateScheduleStore = &EnterpriseTemplateScheduleStore{}
-func NewEnterpriseTemplateScheduleStore() *EnterpriseTemplateScheduleStore {
- return &EnterpriseTemplateScheduleStore{}
+func NewEnterpriseTemplateScheduleStore(userQuietHoursStore *atomic.Pointer[agpl.UserQuietHoursScheduleStore]) *EnterpriseTemplateScheduleStore {
+ return &EnterpriseTemplateScheduleStore{
+ UserQuietHoursScheduleStore: userQuietHoursStore,
+ }
+}
+
+func (s *EnterpriseTemplateScheduleStore) now() time.Time {
+ if s.TimeNowFn != nil {
+ return s.TimeNowFn()
+ }
+ return database.Now()
}
// Get implements agpl.TemplateScheduleStore.
func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.Store, templateID uuid.UUID) (agpl.TemplateScheduleOptions, error) {
+ ctx, span := tracing.StartSpan(ctx)
+ defer span.End()
+
tpl, err := db.GetTemplateByID(ctx, templateID)
if err != nil {
return agpl.TemplateScheduleOptions{}, err
@@ -58,21 +83,24 @@ func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.S
DaysOfWeek: uint8(tpl.RestartRequirementDaysOfWeek),
Weeks: tpl.RestartRequirementWeeks,
},
- FailureTTL: time.Duration(tpl.FailureTTL),
- InactivityTTL: time.Duration(tpl.InactivityTTL),
- LockedTTL: time.Duration(tpl.LockedTTL),
+ FailureTTL: time.Duration(tpl.FailureTTL),
+ TimeTilDormant: time.Duration(tpl.TimeTilDormant),
+ TimeTilDormantAutoDelete: time.Duration(tpl.TimeTilDormantAutoDelete),
}, nil
}
// Set implements agpl.TemplateScheduleStore.
-func (*EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Store, tpl database.Template, opts agpl.TemplateScheduleOptions) (database.Template, error) {
+func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Store, tpl database.Template, opts agpl.TemplateScheduleOptions) (database.Template, error) {
+ ctx, span := tracing.StartSpan(ctx)
+ defer span.End()
+
if int64(opts.DefaultTTL) == tpl.DefaultTTL &&
int64(opts.MaxTTL) == tpl.MaxTTL &&
int16(opts.RestartRequirement.DaysOfWeek) == tpl.RestartRequirementDaysOfWeek &&
opts.RestartRequirement.Weeks == tpl.RestartRequirementWeeks &&
int64(opts.FailureTTL) == tpl.FailureTTL &&
- int64(opts.InactivityTTL) == tpl.InactivityTTL &&
- int64(opts.LockedTTL) == tpl.LockedTTL &&
+ int64(opts.TimeTilDormant) == tpl.TimeTilDormant &&
+ int64(opts.TimeTilDormantAutoDelete) == tpl.TimeTilDormantAutoDelete &&
opts.UserAutostartEnabled == tpl.AllowUserAutostart &&
opts.UserAutostopEnabled == tpl.AllowUserAutostop {
// Avoid updating the UpdatedAt timestamp if nothing will be changed.
@@ -85,10 +113,13 @@ func (*EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Sto
}
var template database.Template
- err = db.InTx(func(db database.Store) error {
- err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
+ err = db.InTx(func(tx database.Store) error {
+ ctx, span := tracing.StartSpanWithName(ctx, "(*schedule.EnterpriseTemplateScheduleStore).Set()-InTx()")
+ defer span.End()
+
+ err := tx.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
ID: tpl.ID,
- UpdatedAt: database.Now(),
+ UpdatedAt: s.now(),
AllowUserAutostart: opts.UserAutostartEnabled,
AllowUserAutostop: opts.UserAutostopEnabled,
DefaultTTL: int64(opts.DefaultTTL),
@@ -96,31 +127,56 @@ func (*EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Sto
RestartRequirementDaysOfWeek: int16(opts.RestartRequirement.DaysOfWeek),
RestartRequirementWeeks: opts.RestartRequirement.Weeks,
FailureTTL: int64(opts.FailureTTL),
- InactivityTTL: int64(opts.InactivityTTL),
- LockedTTL: int64(opts.LockedTTL),
+ TimeTilDormant: int64(opts.TimeTilDormant),
+ TimeTilDormantAutoDelete: int64(opts.TimeTilDormantAutoDelete),
})
if err != nil {
return xerrors.Errorf("update template schedule: %w", err)
}
- // If we updated the locked_ttl we need to update all the workspaces deleting_at
+ var dormantAt time.Time
+ if opts.UpdateWorkspaceDormantAt {
+ dormantAt = database.Now()
+ }
+
+ // If we updated the time_til_dormant_autodelete we need to update all the workspaces deleting_at
// to ensure workspaces are being cleaned up correctly. Similarly if we are
// disabling it (by passing 0), then we want to delete nullify the deleting_at
// fields of all the template workspaces.
- err = db.UpdateWorkspacesDeletingAtByTemplateID(ctx, database.UpdateWorkspacesDeletingAtByTemplateIDParams{
- TemplateID: tpl.ID,
- LockedTtlMs: opts.LockedTTL.Milliseconds(),
+ err = tx.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx, database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams{
+ TemplateID: tpl.ID,
+ TimeTilDormantAutodeleteMs: opts.TimeTilDormantAutoDelete.Milliseconds(),
+ DormantAt: dormantAt,
})
if err != nil {
- return xerrors.Errorf("update deleting_at of all workspaces for new locked_ttl %q: %w", opts.LockedTTL, err)
+ return xerrors.Errorf("update deleting_at of all workspaces for new time_til_dormant_autodelete %q: %w", opts.TimeTilDormantAutoDelete, err)
+ }
+
+ if opts.UpdateWorkspaceLastUsedAt {
+ err = tx.UpdateTemplateWorkspacesLastUsedAt(ctx, database.UpdateTemplateWorkspacesLastUsedAtParams{
+ TemplateID: tpl.ID,
+ LastUsedAt: database.Now(),
+ })
+ if err != nil {
+ return xerrors.Errorf("update template workspaces last_used_at: %w", err)
+ }
}
// TODO: update all workspace max_deadlines to be within new bounds
- template, err = db.GetTemplateByID(ctx, tpl.ID)
+ template, err = tx.GetTemplateByID(ctx, tpl.ID)
if err != nil {
return xerrors.Errorf("get updated template schedule: %w", err)
}
+ // Recalculate max_deadline and deadline for all running workspace
+ // builds on this template.
+ if s.UseRestartRequirement.Load() {
+ err = s.updateWorkspaceBuilds(ctx, tx, template)
+ if err != nil {
+ return xerrors.Errorf("update workspace builds: %w", err)
+ }
+ }
+
return nil
}, nil)
if err != nil {
@@ -129,3 +185,98 @@ func (*EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Sto
return template, nil
}
+
+func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuilds(ctx context.Context, db database.Store, template database.Template) error {
+ ctx, span := tracing.StartSpan(ctx)
+ defer span.End()
+
+ //nolint:gocritic // This function will retrieve all workspace builds on
+ // the template and update their max deadline to be within the new
+ // policy parameters.
+ ctx = dbauthz.AsSystemRestricted(ctx)
+
+ builds, err := db.GetActiveWorkspaceBuildsByTemplateID(ctx, template.ID)
+ if err != nil {
+ return xerrors.Errorf("get active workspace builds: %w", err)
+ }
+
+ for _, build := range builds {
+ err := s.updateWorkspaceBuild(ctx, db, build)
+ if err != nil {
+ return xerrors.Errorf("update workspace build %q: %w", build.ID, err)
+ }
+ }
+
+ return nil
+}
+
+func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuild(ctx context.Context, db database.Store, build database.WorkspaceBuild) error {
+ ctx, span := tracing.StartSpan(ctx,
+ trace.WithAttributes(attribute.String("coder.workspace_id", build.WorkspaceID.String())),
+ trace.WithAttributes(attribute.String("coder.workspace_build_id", build.ID.String())),
+ )
+ defer span.End()
+
+ if !build.MaxDeadline.IsZero() && build.MaxDeadline.Before(s.now().Add(2*time.Hour)) {
+ // Skip this since it's already too close to the max_deadline.
+ return nil
+ }
+
+ workspace, err := db.GetWorkspaceByID(ctx, build.WorkspaceID)
+ if err != nil {
+ return xerrors.Errorf("get workspace %q: %w", build.WorkspaceID, err)
+ }
+
+ job, err := db.GetProvisionerJobByID(ctx, build.JobID)
+ if err != nil {
+ return xerrors.Errorf("get provisioner job %q: %w", build.JobID, err)
+ }
+ if db2sdk.ProvisionerJobStatus(job) != codersdk.ProvisionerJobSucceeded {
+ // Only touch builds that are completed.
+ return nil
+ }
+
+ // If the job completed before the autostop epoch, then it must be skipped
+ // to avoid failures below. Add a week to account for timezones.
+ if job.CompletedAt.Time.Before(agpl.TemplateRestartRequirementEpoch(time.UTC).Add(time.Hour * 7 * 24)) {
+ return nil
+ }
+
+ autostop, err := agpl.CalculateAutostop(ctx, agpl.CalculateAutostopParams{
+ Database: db,
+ TemplateScheduleStore: s,
+ UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
+ // Use the job completion time as the time we calculate autostop from.
+ Now: job.CompletedAt.Time,
+ Workspace: workspace,
+ })
+ if err != nil {
+ return xerrors.Errorf("calculate new autostop for workspace %q: %w", workspace.ID, err)
+ }
+
+ // If max deadline is before now()+2h, then set it to that.
+ now := s.now()
+ if autostop.MaxDeadline.Before(now.Add(2 * time.Hour)) {
+ autostop.MaxDeadline = now.Add(time.Hour * 2)
+ }
+
+ // If the current deadline on the build is after the new max_deadline, then
+ // set it to the max_deadline.
+ autostop.Deadline = build.Deadline
+ if autostop.Deadline.After(autostop.MaxDeadline) {
+ autostop.Deadline = autostop.MaxDeadline
+ }
+
+ // Update the workspace build.
+ err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
+ ID: build.ID,
+ UpdatedAt: now,
+ Deadline: autostop.Deadline,
+ MaxDeadline: autostop.MaxDeadline,
+ })
+ if err != nil {
+ return xerrors.Errorf("update workspace build %q: %w", build.ID, err)
+ }
+
+ return nil
+}
diff --git a/enterprise/coderd/schedule/template_test.go b/enterprise/coderd/schedule/template_test.go
new file mode 100644
index 0000000000000..14f7a384b0c12
--- /dev/null
+++ b/enterprise/coderd/schedule/template_test.go
@@ -0,0 +1,523 @@
+package schedule_test
+
+import (
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ agplschedule "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/enterprise/coderd/schedule"
+ "github.com/coder/coder/v2/testutil"
+)
+
+func TestTemplateUpdateBuildDeadlines(t *testing.T) {
+ t.Parallel()
+
+ db, _ := dbtestutil.NewDB(t)
+
+ var (
+ org = dbgen.Organization(t, db, database.Organization{})
+ user = dbgen.User(t, db, database.User{})
+ file = dbgen.File(t, db, database.File{
+ CreatedBy: user.ID,
+ })
+ templateJob = dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
+ OrganizationID: org.ID,
+ FileID: file.ID,
+ InitiatorID: user.ID,
+ Tags: database.StringMap{
+ "foo": "bar",
+ },
+ })
+ templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
+ OrganizationID: org.ID,
+ CreatedBy: user.ID,
+ JobID: templateJob.ID,
+ })
+ )
+
+ const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
+ ctx := testutil.Context(t, testutil.WaitLong)
+ user, err := db.UpdateUserQuietHoursSchedule(ctx, database.UpdateUserQuietHoursScheduleParams{
+ ID: user.ID,
+ QuietHoursSchedule: userQuietHoursSchedule,
+ })
+ require.NoError(t, err)
+
+ realNow := time.Now().UTC()
+ nowY, nowM, nowD := realNow.Date()
+ buildTime := time.Date(nowY, nowM, nowD, 12, 0, 0, 0, time.UTC) // noon today UTC
+ nextQuietHours := time.Date(nowY, nowM, nowD+1, 0, 0, 0, 0, time.UTC) // midnight tomorrow UTC
+
+ // Workspace old max_deadline too soon
+ cases := []struct {
+ name string
+ now time.Time
+ deadline time.Time
+ maxDeadline time.Time
+ newDeadline time.Time // 0 for no change
+ newMaxDeadline time.Time
+ }{
+ {
+ name: "SkippedWorkspaceMaxDeadlineTooSoon",
+ now: buildTime,
+ deadline: buildTime,
+ maxDeadline: buildTime.Add(1 * time.Hour),
+ // Unchanged since the max deadline is too soon.
+ newDeadline: time.Time{},
+ newMaxDeadline: buildTime.Add(1 * time.Hour),
+ },
+ {
+ name: "NewWorkspaceMaxDeadlineBeforeNow",
+ // After the new max deadline...
+ now: nextQuietHours.Add(6 * time.Hour),
+ deadline: buildTime,
+ // Far into the future...
+ maxDeadline: nextQuietHours.Add(24 * time.Hour),
+ newDeadline: time.Time{},
+ // We will use now() + 2 hours if the newly calculated max deadline
+ // from the workspace build time is before now.
+ newMaxDeadline: nextQuietHours.Add(8 * time.Hour),
+ },
+ {
+ name: "NewWorkspaceMaxDeadlineSoon",
+ // Right before the new max deadline...
+ now: nextQuietHours.Add(-1 * time.Hour),
+ deadline: buildTime,
+ // Far into the future...
+ maxDeadline: nextQuietHours.Add(24 * time.Hour),
+ newDeadline: time.Time{},
+ // We will use now() + 2 hours if the newly calculated max deadline
+ // from the workspace build time is within the next 2 hours.
+ newMaxDeadline: nextQuietHours.Add(1 * time.Hour),
+ },
+ {
+ name: "NewWorkspaceMaxDeadlineFuture",
+ // Well before the new max deadline...
+ now: nextQuietHours.Add(-6 * time.Hour),
+ deadline: buildTime,
+ // Far into the future...
+ maxDeadline: nextQuietHours.Add(24 * time.Hour),
+ newDeadline: time.Time{},
+ newMaxDeadline: nextQuietHours,
+ },
+ {
+ name: "DeadlineAfterNewWorkspaceMaxDeadline",
+ // Well before the new max deadline...
+ now: nextQuietHours.Add(-6 * time.Hour),
+ // Far into the future...
+ deadline: nextQuietHours.Add(24 * time.Hour),
+ maxDeadline: nextQuietHours.Add(24 * time.Hour),
+ // The deadline should match since it is after the new max deadline.
+ newDeadline: nextQuietHours,
+ newMaxDeadline: nextQuietHours,
+ },
+ }
+
+ for _, c := range cases {
+ c := c
+
+ t.Run(c.name, func(t *testing.T) {
+ t.Parallel()
+
+ t.Log("buildTime", buildTime)
+ t.Log("nextQuietHours", nextQuietHours)
+ t.Log("now", c.now)
+ t.Log("deadline", c.deadline)
+ t.Log("maxDeadline", c.maxDeadline)
+ t.Log("newDeadline", c.newDeadline)
+ t.Log("newMaxDeadline", c.newMaxDeadline)
+
+ var (
+ template = dbgen.Template(t, db, database.Template{
+ OrganizationID: org.ID,
+ ActiveVersionID: templateVersion.ID,
+ CreatedBy: user.ID,
+ })
+ ws = dbgen.Workspace(t, db, database.Workspace{
+ OrganizationID: org.ID,
+ OwnerID: user.ID,
+ TemplateID: template.ID,
+ })
+ job = dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
+ OrganizationID: org.ID,
+ FileID: file.ID,
+ InitiatorID: user.ID,
+ Provisioner: database.ProvisionerTypeEcho,
+ Tags: database.StringMap{
+ c.name: "yeah",
+ },
+ })
+ wsBuild = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
+ WorkspaceID: ws.ID,
+ BuildNumber: 1,
+ JobID: job.ID,
+ InitiatorID: user.ID,
+ TemplateVersionID: templateVersion.ID,
+ })
+ )
+
+ acquiredJob, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
+ StartedAt: sql.NullTime{
+ Time: buildTime,
+ Valid: true,
+ },
+ WorkerID: uuid.NullUUID{
+ UUID: uuid.New(),
+ Valid: true,
+ },
+ Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
+ Tags: json.RawMessage(fmt.Sprintf(`{%q: "yeah"}`, c.name)),
+ })
+ require.NoError(t, err)
+ require.Equal(t, job.ID, acquiredJob.ID)
+ err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
+ ID: job.ID,
+ CompletedAt: sql.NullTime{
+ Time: buildTime,
+ Valid: true,
+ },
+ UpdatedAt: buildTime,
+ })
+ require.NoError(t, err)
+
+ err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
+ ID: wsBuild.ID,
+ UpdatedAt: buildTime,
+ ProvisionerState: []byte{},
+ Deadline: c.deadline,
+ MaxDeadline: c.maxDeadline,
+ })
+ require.NoError(t, err)
+
+ wsBuild, err = db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
+ require.NoError(t, err)
+
+ userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule)
+ require.NoError(t, err)
+ userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
+ userQuietHoursStorePtr.Store(&userQuietHoursStore)
+
+ // Set the template policy.
+ templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr)
+ templateScheduleStore.UseRestartRequirement.Store(true)
+ templateScheduleStore.TimeNowFn = func() time.Time {
+ return c.now
+ }
+ _, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
+ UserAutostartEnabled: false,
+ UserAutostopEnabled: false,
+ DefaultTTL: 0,
+ MaxTTL: 0,
+ UseRestartRequirement: true,
+ RestartRequirement: agplschedule.TemplateRestartRequirement{
+ // Every day
+ DaysOfWeek: 0b01111111,
+ Weeks: 0,
+ },
+ FailureTTL: 0,
+ TimeTilDormant: 0,
+ TimeTilDormantAutoDelete: 0,
+ })
+ require.NoError(t, err)
+
+ // Check that the workspace build has the expected deadlines.
+ newBuild, err := db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
+ require.NoError(t, err)
+
+ if c.newDeadline.IsZero() {
+ c.newDeadline = wsBuild.Deadline
+ }
+ require.WithinDuration(t, c.newDeadline, newBuild.Deadline, time.Second)
+ require.WithinDuration(t, c.newMaxDeadline, newBuild.MaxDeadline, time.Second)
+ })
+ }
+}
+
+func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) {
+ t.Parallel()
+
+ db, _ := dbtestutil.NewDB(t)
+
+ var (
+ org = dbgen.Organization(t, db, database.Organization{})
+ user = dbgen.User(t, db, database.User{})
+ file = dbgen.File(t, db, database.File{
+ CreatedBy: user.ID,
+ })
+ templateJob = dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
+ OrganizationID: org.ID,
+ FileID: file.ID,
+ InitiatorID: user.ID,
+ Tags: database.StringMap{
+ "foo": "bar",
+ },
+ })
+ templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
+ OrganizationID: org.ID,
+ CreatedBy: user.ID,
+ JobID: templateJob.ID,
+ })
+ template = dbgen.Template(t, db, database.Template{
+ OrganizationID: org.ID,
+ ActiveVersionID: templateVersion.ID,
+ CreatedBy: user.ID,
+ })
+ otherTemplate = dbgen.Template(t, db, database.Template{
+ OrganizationID: org.ID,
+ ActiveVersionID: templateVersion.ID,
+ CreatedBy: user.ID,
+ })
+ )
+
+ // Create a workspace that will be shared by two builds.
+ ws := dbgen.Workspace(t, db, database.Workspace{
+ OrganizationID: org.ID,
+ OwnerID: user.ID,
+ TemplateID: template.ID,
+ })
+
+ const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
+ ctx := testutil.Context(t, testutil.WaitLong)
+ user, err := db.UpdateUserQuietHoursSchedule(ctx, database.UpdateUserQuietHoursScheduleParams{
+ ID: user.ID,
+ QuietHoursSchedule: userQuietHoursSchedule,
+ })
+ require.NoError(t, err)
+
+ realNow := time.Now().UTC()
+ nowY, nowM, nowD := realNow.Date()
+ buildTime := time.Date(nowY, nowM, nowD, 12, 0, 0, 0, time.UTC) // noon today UTC
+ now := time.Date(nowY, nowM, nowD, 18, 0, 0, 0, time.UTC) // 6pm today UTC
+ nextQuietHours := time.Date(nowY, nowM, nowD+1, 0, 0, 0, 0, time.UTC) // midnight tomorrow UTC
+
+ // A date very far in the future which would definitely be updated.
+ originalMaxDeadline := time.Date(nowY+1, nowM, nowD, 0, 0, 0, 0, time.UTC)
+
+ _ = otherTemplate
+
+ builds := []struct {
+ name string
+ templateID uuid.UUID
+ // Nil workspaceID means create a new workspace.
+ workspaceID uuid.UUID
+ buildNumber int32
+ buildStarted bool
+ buildCompleted bool
+ buildError bool
+
+ shouldBeUpdated bool
+
+ // Set below:
+ wsBuild database.WorkspaceBuild
+ }{
+ {
+ name: "DifferentTemplate",
+ templateID: otherTemplate.ID,
+ workspaceID: uuid.Nil,
+ buildNumber: 1,
+ buildStarted: true,
+ buildCompleted: true,
+ buildError: false,
+ shouldBeUpdated: false,
+ },
+ {
+ name: "NonStartedBuild",
+ templateID: template.ID,
+ workspaceID: uuid.Nil,
+ buildNumber: 1,
+ buildStarted: false,
+ buildCompleted: false,
+ buildError: false,
+ shouldBeUpdated: false,
+ },
+ {
+ name: "InProgressBuild",
+ templateID: template.ID,
+ workspaceID: uuid.Nil,
+ buildNumber: 1,
+ buildStarted: true,
+ buildCompleted: false,
+ buildError: false,
+ shouldBeUpdated: false,
+ },
+ {
+ name: "FailedBuild",
+ templateID: template.ID,
+ workspaceID: uuid.Nil,
+ buildNumber: 1,
+ buildStarted: true,
+ buildCompleted: true,
+ buildError: true,
+ shouldBeUpdated: false,
+ },
+ {
+ name: "NonLatestBuild",
+ templateID: template.ID,
+ workspaceID: ws.ID,
+ buildNumber: 1,
+ buildStarted: true,
+ buildCompleted: true,
+ buildError: false,
+ // This build was successful but is not the latest build for this
+ // workspace, see the next build.
+ shouldBeUpdated: false,
+ },
+ {
+ name: "LatestBuild",
+ templateID: template.ID,
+ workspaceID: ws.ID,
+ buildNumber: 2,
+ buildStarted: true,
+ buildCompleted: true,
+ buildError: false,
+ shouldBeUpdated: true,
+ },
+ {
+ name: "LatestBuildOtherWorkspace",
+ templateID: template.ID,
+ workspaceID: uuid.Nil,
+ buildNumber: 1,
+ buildStarted: true,
+ buildCompleted: true,
+ buildError: false,
+ shouldBeUpdated: true,
+ },
+ }
+
+ for i, b := range builds {
+ wsID := b.workspaceID
+ if wsID == uuid.Nil {
+ ws := dbgen.Workspace(t, db, database.Workspace{
+ OrganizationID: org.ID,
+ OwnerID: user.ID,
+ TemplateID: b.templateID,
+ })
+ wsID = ws.ID
+ }
+ job := dbgen.ProvisionerJob(t, db, database.ProvisionerJob{
+ OrganizationID: org.ID,
+ FileID: file.ID,
+ InitiatorID: user.ID,
+ Provisioner: database.ProvisionerTypeEcho,
+ Tags: database.StringMap{
+ wsID.String(): "yeah",
+ },
+ })
+ wsBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
+ WorkspaceID: wsID,
+ BuildNumber: b.buildNumber,
+ JobID: job.ID,
+ InitiatorID: user.ID,
+ TemplateVersionID: templateVersion.ID,
+ })
+
+ err := db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
+ ID: wsBuild.ID,
+ UpdatedAt: buildTime,
+ ProvisionerState: []byte{},
+ Deadline: originalMaxDeadline,
+ MaxDeadline: originalMaxDeadline,
+ })
+ require.NoError(t, err)
+
+ wsBuild, err = db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
+ require.NoError(t, err)
+
+ builds[i].wsBuild = wsBuild
+
+ if !b.buildStarted {
+ continue
+ }
+
+ acquiredJob, err := db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
+ StartedAt: sql.NullTime{
+ Time: buildTime,
+ Valid: true,
+ },
+ WorkerID: uuid.NullUUID{
+ UUID: uuid.New(),
+ Valid: true,
+ },
+ Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
+ Tags: json.RawMessage(fmt.Sprintf(`{%q: "yeah"}`, wsID)),
+ })
+ require.NoError(t, err)
+ require.Equal(t, job.ID, acquiredJob.ID)
+
+ if !b.buildCompleted {
+ continue
+ }
+
+ buildError := ""
+ if b.buildError {
+ buildError = "error"
+ }
+ err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
+ ID: job.ID,
+ CompletedAt: sql.NullTime{
+ Time: buildTime,
+ Valid: true,
+ },
+ Error: sql.NullString{
+ String: buildError,
+ Valid: b.buildError,
+ },
+ UpdatedAt: buildTime,
+ })
+ require.NoError(t, err)
+ }
+
+ userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule)
+ require.NoError(t, err)
+ userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
+ userQuietHoursStorePtr.Store(&userQuietHoursStore)
+
+ // Set the template policy.
+ templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr)
+ templateScheduleStore.UseRestartRequirement.Store(true)
+ templateScheduleStore.TimeNowFn = func() time.Time {
+ return now
+ }
+ _, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
+ UserAutostartEnabled: false,
+ UserAutostopEnabled: false,
+ DefaultTTL: 0,
+ MaxTTL: 0,
+ UseRestartRequirement: true,
+ RestartRequirement: agplschedule.TemplateRestartRequirement{
+ // Every day
+ DaysOfWeek: 0b01111111,
+ Weeks: 0,
+ },
+ FailureTTL: 0,
+ TimeTilDormant: 0,
+ TimeTilDormantAutoDelete: 0,
+ })
+ require.NoError(t, err)
+
+ // Check each build.
+ for i, b := range builds {
+ msg := fmt.Sprintf("build %d: %s", i, b.name)
+ newBuild, err := db.GetWorkspaceBuildByID(ctx, b.wsBuild.ID)
+ require.NoError(t, err)
+
+ if b.shouldBeUpdated {
+ assert.WithinDuration(t, nextQuietHours, newBuild.Deadline, time.Second, msg)
+ assert.WithinDuration(t, nextQuietHours, newBuild.MaxDeadline, time.Second, msg)
+ } else {
+ assert.WithinDuration(t, originalMaxDeadline, newBuild.Deadline, time.Second, msg)
+ assert.WithinDuration(t, originalMaxDeadline, newBuild.MaxDeadline, time.Second, msg)
+ }
+ }
+}
diff --git a/enterprise/coderd/schedule/user.go b/enterprise/coderd/schedule/user.go
index c7d76f86119c8..bb054305a6db2 100644
--- a/enterprise/coderd/schedule/user.go
+++ b/enterprise/coderd/schedule/user.go
@@ -7,8 +7,9 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/database"
- agpl "github.com/coder/coder/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/database"
+ agpl "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/tracing"
)
// enterpriseUserQuietHoursScheduleStore provides an
@@ -29,7 +30,8 @@ func NewEnterpriseUserQuietHoursScheduleStore(defaultSchedule string) (agpl.User
defaultSchedule: defaultSchedule,
}
- _, err := s.parseSchedule(defaultSchedule)
+ // The context is only used for tracing so using a background ctx is fine.
+ _, err := s.parseSchedule(context.Background(), defaultSchedule)
if err != nil {
return nil, xerrors.Errorf("parse default schedule: %w", err)
}
@@ -37,7 +39,10 @@ func NewEnterpriseUserQuietHoursScheduleStore(defaultSchedule string) (agpl.User
return s, nil
}
-func (s *enterpriseUserQuietHoursScheduleStore) parseSchedule(rawSchedule string) (agpl.UserQuietHoursScheduleOptions, error) {
+func (s *enterpriseUserQuietHoursScheduleStore) parseSchedule(ctx context.Context, rawSchedule string) (agpl.UserQuietHoursScheduleOptions, error) {
+ _, span := tracing.StartSpan(ctx)
+ defer span.End()
+
userSet := true
if strings.TrimSpace(rawSchedule) == "" {
userSet = false
@@ -64,16 +69,22 @@ func (s *enterpriseUserQuietHoursScheduleStore) parseSchedule(rawSchedule string
}
func (s *enterpriseUserQuietHoursScheduleStore) Get(ctx context.Context, db database.Store, userID uuid.UUID) (agpl.UserQuietHoursScheduleOptions, error) {
+ ctx, span := tracing.StartSpan(ctx)
+ defer span.End()
+
user, err := db.GetUserByID(ctx, userID)
if err != nil {
return agpl.UserQuietHoursScheduleOptions{}, xerrors.Errorf("get user by ID: %w", err)
}
- return s.parseSchedule(user.QuietHoursSchedule)
+ return s.parseSchedule(ctx, user.QuietHoursSchedule)
}
func (s *enterpriseUserQuietHoursScheduleStore) Set(ctx context.Context, db database.Store, userID uuid.UUID, rawSchedule string) (agpl.UserQuietHoursScheduleOptions, error) {
- opts, err := s.parseSchedule(rawSchedule)
+ ctx, span := tracing.StartSpan(ctx)
+ defer span.End()
+
+ opts, err := s.parseSchedule(ctx, rawSchedule)
if err != nil {
return opts, err
}
@@ -91,8 +102,12 @@ func (s *enterpriseUserQuietHoursScheduleStore) Set(ctx context.Context, db data
return agpl.UserQuietHoursScheduleOptions{}, xerrors.Errorf("update user quiet hours schedule: %w", err)
}
- // TODO(@dean): update max_deadline for all active builds for this user to clamp to
- // the new schedule.
+ // We don't update workspace build deadlines when the user changes their own
+ // quiet hours schedule, because they could potentially keep their workspace
+ // running forever.
+ //
+ // Workspace build deadlines are updated when the template admin changes the
+ // template's settings however.
return opts, nil
}
diff --git a/enterprise/coderd/scim.go b/enterprise/coderd/scim.go
index efba55b932684..b906010cc0b36 100644
--- a/enterprise/coderd/scim.go
+++ b/enterprise/coderd/scim.go
@@ -14,11 +14,11 @@ import (
"github.com/imulab/go-scim/pkg/v2/spec"
"golang.org/x/xerrors"
- agpl "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ agpl "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
func (api *API) scimEnabledMW(next http.Handler) http.Handler {
@@ -155,7 +155,7 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic
- user, err := api.Database.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{
+ dbUser, err := api.Database.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{
Email: email,
Username: sUser.UserName,
})
@@ -164,8 +164,22 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
return
}
if err == nil {
- sUser.ID = user.ID.String()
- sUser.UserName = user.Username
+ sUser.ID = dbUser.ID.String()
+ sUser.UserName = dbUser.Username
+
+ if sUser.Active && dbUser.Status == database.UserStatusSuspended {
+ //nolint:gocritic
+ _, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(r.Context()), database.UpdateUserStatusParams{
+ ID: dbUser.ID,
+ // The user will get transitioned to Active after logging in.
+ Status: database.UserStatusDormant,
+ UpdatedAt: database.Now(),
+ })
+ if err != nil {
+ _ = handlerutil.WriteError(rw, err)
+ return
+ }
+ }
httpapi.Write(ctx, rw, http.StatusOK, sUser)
return
@@ -201,7 +215,7 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
}
//nolint:gocritic // needed for SCIM
- user, _, err = api.AGPL.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, agpl.CreateUserRequest{
+ dbUser, _, err = api.AGPL.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, agpl.CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Username: sUser.UserName,
Email: email,
@@ -214,8 +228,8 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
return
}
- sUser.ID = user.ID.String()
- sUser.UserName = user.Username
+ sUser.ID = dbUser.ID.String()
+ sUser.UserName = dbUser.Username
httpapi.Write(ctx, rw, http.StatusOK, sUser)
}
@@ -263,7 +277,8 @@ func (api *API) scimPatchUser(rw http.ResponseWriter, r *http.Request) {
var status database.UserStatus
if sUser.Active {
- status = database.UserStatusActive
+ // The user will get transitioned to Active after logging in.
+ status = database.UserStatusDormant
} else {
status = database.UserStatusSuspended
}
diff --git a/enterprise/coderd/scim_test.go b/enterprise/coderd/scim_test.go
index f0778c26b51d0..3d12c3402ac41 100644
--- a/enterprise/coderd/scim_test.go
+++ b/enterprise/coderd/scim_test.go
@@ -4,18 +4,19 @@ import (
"context"
"encoding/json"
"fmt"
+ "io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/enterprise/coderd"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/enterprise/coderd"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/testutil"
)
//nolint:revive
@@ -164,6 +165,54 @@ func TestScim(t *testing.T) {
assert.Equal(t, sUser.UserName, userRes.Users[0].Username)
})
+ t.Run("Unsuspend", func(t *testing.T) {
+ t.Parallel()
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ scimAPIKey := []byte("hi")
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ SCIMAPIKey: scimAPIKey,
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ AccountID: "coolin",
+ Features: license.Features{
+ codersdk.FeatureSCIM: 1,
+ },
+ },
+ })
+
+ sUser := makeScimUser(t)
+ res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey))
+ require.NoError(t, err)
+ defer res.Body.Close()
+ assert.Equal(t, http.StatusOK, res.StatusCode)
+ err = json.NewDecoder(res.Body).Decode(&sUser)
+ require.NoError(t, err)
+
+ sUser.Active = false
+ res, err = client.Request(ctx, "PATCH", "/scim/v2/Users/"+sUser.ID, sUser, setScimAuth(scimAPIKey))
+ require.NoError(t, err)
+ _, _ = io.Copy(io.Discard, res.Body)
+ _ = res.Body.Close()
+ assert.Equal(t, http.StatusOK, res.StatusCode)
+
+ sUser.Active = true
+ res, err = client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey))
+ require.NoError(t, err)
+ _, _ = io.Copy(io.Discard, res.Body)
+ _ = res.Body.Close()
+ assert.Equal(t, http.StatusOK, res.StatusCode)
+
+ userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
+ require.NoError(t, err)
+ require.Len(t, userRes.Users, 1)
+
+ assert.Equal(t, sUser.Emails[0].Value, userRes.Users[0].Email)
+ assert.Equal(t, sUser.UserName, userRes.Users[0].Username)
+ assert.Equal(t, codersdk.UserStatusDormant, userRes.Users[0].Status)
+ })
+
t.Run("DomainStrips", func(t *testing.T) {
t.Parallel()
@@ -185,7 +234,8 @@ func TestScim(t *testing.T) {
sUser.UserName = sUser.UserName + "@coder.com"
res, err := client.Request(ctx, "POST", "/scim/v2/Users", sUser, setScimAuth(scimAPIKey))
require.NoError(t, err)
- defer res.Body.Close()
+ _, _ = io.Copy(io.Discard, res.Body)
+ _ = res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode)
userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
@@ -220,7 +270,8 @@ func TestScim(t *testing.T) {
res, err := client.Request(ctx, "PATCH", "/scim/v2/Users/bob", struct{}{})
require.NoError(t, err)
- defer res.Body.Close()
+ _, _ = io.Copy(io.Discard, res.Body)
+ _ = res.Body.Close()
assert.Equal(t, http.StatusNotFound, res.StatusCode)
})
@@ -242,7 +293,8 @@ func TestScim(t *testing.T) {
res, err := client.Request(ctx, "PATCH", "/scim/v2/Users/bob", struct{}{})
require.NoError(t, err)
- defer res.Body.Close()
+ _, _ = io.Copy(io.Discard, res.Body)
+ _ = res.Body.Close()
assert.Equal(t, http.StatusInternalServerError, res.StatusCode)
})
@@ -276,7 +328,8 @@ func TestScim(t *testing.T) {
res, err = client.Request(ctx, "PATCH", "/scim/v2/Users/"+sUser.ID, sUser, setScimAuth(scimAPIKey))
require.NoError(t, err)
- defer res.Body.Close()
+ _, _ = io.Copy(io.Discard, res.Body)
+ _ = res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode)
userRes, err := client.Users(ctx, codersdk.UsersRequest{Search: sUser.Emails[0].Value})
diff --git a/enterprise/coderd/templates.go b/enterprise/coderd/templates.go
index 5ba96712aaa89..4e7e0c669dfd6 100644
--- a/enterprise/coderd/templates.go
+++ b/enterprise/coderd/templates.go
@@ -9,13 +9,13 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
)
// @Summary Get template available acl users/groups
diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go
index 68e91a3ff0975..52d29acf76cd6 100644
--- a/enterprise/coderd/templates_test.go
+++ b/enterprise/coderd/templates_test.go
@@ -10,16 +10,16 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
func TestTemplates(t *testing.T) {
@@ -202,41 +202,41 @@ func TestTemplates(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
- require.EqualValues(t, 0, template.InactivityTTLMillis)
+ require.EqualValues(t, 0, template.TimeTilDormantMillis)
require.EqualValues(t, 0, template.FailureTTLMillis)
- require.EqualValues(t, 0, template.LockedTTLMillis)
+ require.EqualValues(t, 0, template.TimeTilDormantAutoDeleteMillis)
var (
failureTTL int64 = 1
inactivityTTL int64 = 2
- lockedTTL int64 = 3
+ dormantTTL int64 = 3
)
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
- Name: template.Name,
- DisplayName: template.DisplayName,
- Description: template.Description,
- Icon: template.Icon,
- AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
- InactivityTTLMillis: inactivityTTL,
- FailureTTLMillis: failureTTL,
- LockedTTLMillis: lockedTTL,
+ Name: template.Name,
+ DisplayName: template.DisplayName,
+ Description: template.Description,
+ Icon: template.Icon,
+ AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ TimeTilDormantMillis: inactivityTTL,
+ FailureTTLMillis: failureTTL,
+ TimeTilDormantAutoDeleteMillis: dormantTTL,
})
require.NoError(t, err)
require.Equal(t, failureTTL, updated.FailureTTLMillis)
- require.Equal(t, inactivityTTL, updated.InactivityTTLMillis)
- require.Equal(t, lockedTTL, updated.LockedTTLMillis)
+ require.Equal(t, inactivityTTL, updated.TimeTilDormantMillis)
+ require.Equal(t, dormantTTL, updated.TimeTilDormantAutoDeleteMillis)
// Validate fetching the template returns the same values as updating
// the template.
template, err = client.Template(ctx, template.ID)
require.NoError(t, err)
require.Equal(t, failureTTL, updated.FailureTTLMillis)
- require.Equal(t, inactivityTTL, updated.InactivityTTLMillis)
- require.Equal(t, lockedTTL, updated.LockedTTLMillis)
+ require.Equal(t, inactivityTTL, updated.TimeTilDormantMillis)
+ require.Equal(t, dormantTTL, updated.TimeTilDormantAutoDeleteMillis)
})
- t.Run("UpdateLockedTTL", func(t *testing.T) {
+ t.Run("UpdateTimeTilDormantAutoDelete", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
@@ -254,58 +254,172 @@ func TestTemplates(t *testing.T) {
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
- unlockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
- lockedWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
- require.Nil(t, unlockedWorkspace.DeletingAt)
- require.Nil(t, lockedWorkspace.DeletingAt)
+ activeWS := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ dormantWS := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ require.Nil(t, activeWS.DeletingAt)
+ require.Nil(t, dormantWS.DeletingAt)
- _ = coderdtest.AwaitWorkspaceBuildJob(t, client, unlockedWorkspace.LatestBuild.ID)
- _ = coderdtest.AwaitWorkspaceBuildJob(t, client, lockedWorkspace.LatestBuild.ID)
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, activeWS.LatestBuild.ID)
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWS.LatestBuild.ID)
- err := client.UpdateWorkspaceLock(ctx, lockedWorkspace.ID, codersdk.UpdateWorkspaceLock{
- Lock: true,
+ err := client.UpdateWorkspaceDormancy(ctx, dormantWS.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: true,
})
require.NoError(t, err)
- lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID)
- require.NotNil(t, lockedWorkspace.LockedAt)
- // The deleting_at field should be nil since there is no template locked_ttl set.
- require.Nil(t, lockedWorkspace.DeletingAt)
+ dormantWS = coderdtest.MustWorkspace(t, client, dormantWS.ID)
+ require.NotNil(t, dormantWS.DormantAt)
+ // The deleting_at field should be nil since there is no template time_til_dormant_autodelete set.
+ require.Nil(t, dormantWS.DeletingAt)
- lockedTTL := time.Minute
+ dormantTTL := time.Minute
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
- LockedTTLMillis: lockedTTL.Milliseconds(),
+ TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
})
require.NoError(t, err)
- require.Equal(t, lockedTTL.Milliseconds(), updated.LockedTTLMillis)
+ require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis)
- unlockedWorkspace = coderdtest.MustWorkspace(t, client, unlockedWorkspace.ID)
- require.Nil(t, unlockedWorkspace.LockedAt)
- require.Nil(t, unlockedWorkspace.DeletingAt)
+ activeWS = coderdtest.MustWorkspace(t, client, activeWS.ID)
+ require.Nil(t, activeWS.DormantAt)
+ require.Nil(t, activeWS.DeletingAt)
- lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID)
- require.NotNil(t, lockedWorkspace.LockedAt)
- require.NotNil(t, lockedWorkspace.DeletingAt)
- require.Equal(t, lockedWorkspace.LockedAt.Add(lockedTTL), *lockedWorkspace.DeletingAt)
+ updatedDormantWorkspace := coderdtest.MustWorkspace(t, client, dormantWS.ID)
+ require.NotNil(t, updatedDormantWorkspace.DormantAt)
+ require.NotNil(t, updatedDormantWorkspace.DeletingAt)
+ require.Equal(t, updatedDormantWorkspace.DormantAt.Add(dormantTTL), *updatedDormantWorkspace.DeletingAt)
+ require.Equal(t, updatedDormantWorkspace.DormantAt, dormantWS.DormantAt)
- // Disable the locked_ttl on the template, then we can assert that the workspaces
+ // Disable the time_til_dormant_auto_delete on the template, then we can assert that the workspaces
// no longer have a deleting_at field.
updated, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
- LockedTTLMillis: 0,
+ TimeTilDormantAutoDeleteMillis: 0,
})
require.NoError(t, err)
- require.EqualValues(t, 0, updated.LockedTTLMillis)
+ require.EqualValues(t, 0, updated.TimeTilDormantAutoDeleteMillis)
- // The unlocked workspace should remain unchanged.
- unlockedWorkspace = coderdtest.MustWorkspace(t, client, unlockedWorkspace.ID)
- require.Nil(t, unlockedWorkspace.LockedAt)
- require.Nil(t, unlockedWorkspace.DeletingAt)
+ // The active workspace should remain unchanged.
+ activeWS = coderdtest.MustWorkspace(t, client, activeWS.ID)
+ require.Nil(t, activeWS.DormantAt)
+ require.Nil(t, activeWS.DeletingAt)
- // Fetch the locked workspace. It should still be locked, but it should no
+ // Fetch the dormant workspace. It should still be dormant, but it should no
// longer be scheduled for deletion.
- lockedWorkspace = coderdtest.MustWorkspace(t, client, lockedWorkspace.ID)
- require.NotNil(t, lockedWorkspace.LockedAt)
- require.Nil(t, lockedWorkspace.DeletingAt)
+ dormantWS = coderdtest.MustWorkspace(t, client, dormantWS.ID)
+ require.NotNil(t, dormantWS.DormantAt)
+ require.Nil(t, dormantWS.DeletingAt)
+ })
+
+ t.Run("UpdateDormantAt", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitMedium)
+ client, user := coderdenttest.New(t, &coderdenttest.Options{
+ Options: &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ },
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureAdvancedTemplateScheduling: 1,
+ },
+ },
+ })
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+
+ activeWS := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ dormantWS := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ require.Nil(t, activeWS.DeletingAt)
+ require.Nil(t, dormantWS.DeletingAt)
+
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, activeWS.LatestBuild.ID)
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWS.LatestBuild.ID)
+
+ err := client.UpdateWorkspaceDormancy(ctx, dormantWS.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: true,
+ })
+ require.NoError(t, err)
+
+ dormantWS = coderdtest.MustWorkspace(t, client, dormantWS.ID)
+ require.NotNil(t, dormantWS.DormantAt)
+ // The deleting_at field should be nil since there is no template time_til_dormant_autodelete set.
+ require.Nil(t, dormantWS.DeletingAt)
+
+ dormantTTL := time.Minute
+ updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
+ TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
+ UpdateWorkspaceDormantAt: true,
+ })
+ require.NoError(t, err)
+ require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis)
+
+ activeWS = coderdtest.MustWorkspace(t, client, activeWS.ID)
+ require.Nil(t, activeWS.DormantAt)
+ require.Nil(t, activeWS.DeletingAt)
+
+ updatedDormantWorkspace := coderdtest.MustWorkspace(t, client, dormantWS.ID)
+ require.NotNil(t, updatedDormantWorkspace.DormantAt)
+ require.NotNil(t, updatedDormantWorkspace.DeletingAt)
+ // Validate that the workspace dormant_at value is updated.
+ require.True(t, updatedDormantWorkspace.DormantAt.After(*dormantWS.DormantAt))
+ require.Equal(t, updatedDormantWorkspace.DormantAt.Add(dormantTTL), *updatedDormantWorkspace.DeletingAt)
+ })
+
+ t.Run("UpdateLastUsedAt", func(t *testing.T) {
+ t.Parallel()
+
+ ctx := testutil.Context(t, testutil.WaitMedium)
+ client, user := coderdenttest.New(t, &coderdenttest.Options{
+ Options: &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ },
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureAdvancedTemplateScheduling: 1,
+ },
+ },
+ })
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+
+ activeWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ dormantWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ require.Nil(t, activeWorkspace.DeletingAt)
+ require.Nil(t, dormantWorkspace.DeletingAt)
+
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, activeWorkspace.LatestBuild.ID)
+ _ = coderdtest.AwaitWorkspaceBuildJob(t, client, dormantWorkspace.LatestBuild.ID)
+
+ err := client.UpdateWorkspaceDormancy(ctx, dormantWorkspace.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: true,
+ })
+ require.NoError(t, err)
+
+ dormantWorkspace = coderdtest.MustWorkspace(t, client, dormantWorkspace.ID)
+ require.NotNil(t, dormantWorkspace.DormantAt)
+ // The deleting_at field should be nil since there is no template time_til_dormant_autodelete set.
+ require.Nil(t, dormantWorkspace.DeletingAt)
+
+ inactivityTTL := time.Minute
+ updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
+ TimeTilDormantMillis: inactivityTTL.Milliseconds(),
+ UpdateWorkspaceLastUsedAt: true,
+ })
+ require.NoError(t, err)
+ require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis)
+
+ updatedActiveWS := coderdtest.MustWorkspace(t, client, activeWorkspace.ID)
+ require.Nil(t, updatedActiveWS.DormantAt)
+ require.Nil(t, updatedActiveWS.DeletingAt)
+ require.True(t, updatedActiveWS.LastUsedAt.After(activeWorkspace.LastUsedAt))
+
+ updatedDormantWS := coderdtest.MustWorkspace(t, client, dormantWorkspace.ID)
+ require.NotNil(t, updatedDormantWS.DormantAt)
+ require.Nil(t, updatedDormantWS.DeletingAt)
+ // Validate that the workspace dormant_at value is updated.
+ require.Equal(t, updatedDormantWS.DormantAt, dormantWorkspace.DormantAt)
+ require.True(t, updatedDormantWS.LastUsedAt.After(dormantWorkspace.LastUsedAt))
})
}
@@ -373,8 +487,7 @@ func TestTemplateACL(t *testing.T) {
require.NoError(t, err)
require.Len(t, acl.Groups, 1)
- // We don't return members for the 'Everyone' group.
- require.Len(t, acl.Groups[0].Members, 0)
+ require.Len(t, acl.Groups[0].Members, 2)
require.Len(t, acl.Users, 0)
})
diff --git a/enterprise/coderd/userauth.go b/enterprise/coderd/userauth.go
index 86aa9f0ddf88b..f504a6c0325c4 100644
--- a/enterprise/coderd/userauth.go
+++ b/enterprise/coderd/userauth.go
@@ -7,12 +7,14 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/codersdk"
)
-func (api *API) setUserGroups(ctx context.Context, db database.Store, userID uuid.UUID, groupNames []string) error {
+// nolint: revive
+func (api *API) setUserGroups(ctx context.Context, logger slog.Logger, db database.Store, userID uuid.UUID, groupNames []string, createMissingGroups bool) error {
api.entitlementsMu.RLock()
enabled := api.entitlements.Features[codersdk.FeatureTemplateRBAC].Enabled
api.entitlementsMu.RUnlock()
@@ -39,6 +41,25 @@ func (api *API) setUserGroups(ctx context.Context, db database.Store, userID uui
return xerrors.Errorf("delete user groups: %w", err)
}
+ if createMissingGroups {
+ // This is the system creating these additional groups, so we use the system restricted context.
+ // nolint:gocritic
+ created, err := tx.InsertMissingGroups(dbauthz.AsSystemRestricted(ctx), database.InsertMissingGroupsParams{
+ OrganizationID: orgs[0].ID,
+ GroupNames: groupNames,
+ Source: database.GroupSourceOidc,
+ })
+ if err != nil {
+ return xerrors.Errorf("insert missing groups: %w", err)
+ }
+ if len(created) > 0 {
+ logger.Debug(ctx, "auto created missing groups",
+ slog.F("org_id", orgs[0].ID),
+ slog.F("created", created),
+ )
+ }
+ }
+
// Re-add the user to all groups returned by the auth provider.
err = tx.InsertUserGroupsByName(ctx, database.InsertUserGroupsByNameParams{
UserID: userID,
@@ -53,13 +74,13 @@ func (api *API) setUserGroups(ctx context.Context, db database.Store, userID uui
}, nil)
}
-func (api *API) setUserSiteRoles(ctx context.Context, db database.Store, userID uuid.UUID, roles []string) error {
+func (api *API) setUserSiteRoles(ctx context.Context, logger slog.Logger, db database.Store, userID uuid.UUID, roles []string) error {
api.entitlementsMu.RLock()
enabled := api.entitlements.Features[codersdk.FeatureUserRoleManagement].Enabled
api.entitlementsMu.RUnlock()
if !enabled {
- api.Logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise entitlement, roles left unchanged",
+ logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise entitlement, roles left unchanged",
slog.F("user_id", userID), slog.F("roles", roles),
)
return nil
diff --git a/enterprise/coderd/userauth_test.go b/enterprise/coderd/userauth_test.go
index 428cf91a6fef2..8e76a36b1df14 100644
--- a/enterprise/coderd/userauth_test.go
+++ b/enterprise/coderd/userauth_test.go
@@ -1,25 +1,25 @@
package coderd_test
import (
- "context"
- "fmt"
- "io"
"net/http"
+ "regexp"
"testing"
- "github.com/coder/coder/enterprise/coderd/license"
-
- "github.com/golang-jwt/jwt"
- "github.com/google/uuid"
- "github.com/stretchr/testify/assert"
+ "github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/coderdtest/oidctest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "github.com/coder/coder/v2/codersdk"
+ coderden "github.com/coder/coder/v2/enterprise/coderd"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/testutil"
)
// nolint:bodyclose
@@ -28,126 +28,123 @@ func TestUserOIDC(t *testing.T) {
t.Run("RoleSync", func(t *testing.T) {
t.Parallel()
+ // NoRoles is the "control group". It has claims with 0 roles
+ // assigned, and asserts that the user has no roles.
t.Run("NoRoles", func(t *testing.T) {
t.Parallel()
- ctx := testutil.Context(t, testutil.WaitMedium)
- conf := coderdtest.NewOIDCConfig(t, "")
-
- oidcRoleName := "TemplateAuthor"
-
- config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) {
- cfg.UserRoleMapping = map[string][]string{oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}}
- })
- config.AllowSignups = true
- config.UserRoleField = "roles"
-
- client, _ := coderdenttest.New(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- OIDCConfig: config,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{codersdk.FeatureUserRoleManagement: 1},
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.UserRoleField = "roles"
},
})
- admin, err := client.User(ctx, "me")
- require.NoError(t, err)
- require.Len(t, admin.OrganizationIDs, 1)
-
- resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
+ claims := jwt.MapClaims{
"email": "alice@coder.com",
- }))
- require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
- user, err := client.User(ctx, "alice")
- require.NoError(t, err)
-
- require.Len(t, user.Roles, 0)
- roleNames := []string{}
- require.ElementsMatch(t, roleNames, []string{})
+ }
+ // Login a new client that signs up
+ client, resp := runner.Login(t, claims)
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ // User should be in 0 groups.
+ runner.AssertRoles(t, "alice", []string{})
+ // Force a refresh, and assert nothing has changes
+ runner.ForceRefresh(t, client, claims)
+ runner.AssertRoles(t, "alice", []string{})
})
- t.Run("NewUserAndRemoveRoles", func(t *testing.T) {
+ // A user has some roles, then on an oauth refresh will lose said
+ // roles from an updated claim.
+ t.Run("NewUserAndRemoveRolesOnRefresh", func(t *testing.T) {
+ // TODO: Implement new feature to update roles/groups on OIDC
+ // refresh tokens. https://github.com/coder/coder/issues/9312
+ t.Skip("Refreshing tokens does not update roles :(")
t.Parallel()
- ctx := testutil.Context(t, testutil.WaitMedium)
- conf := coderdtest.NewOIDCConfig(t, "")
+ const oidcRoleName = "TemplateAuthor"
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Userinfo: jwt.MapClaims{oidcRoleName: []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}},
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.UserRoleField = "roles"
+ cfg.UserRoleMapping = map[string][]string{
+ oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()},
+ }
+ },
+ })
- oidcRoleName := "TemplateAuthor"
+ // User starts with the owner role
+ client, resp := runner.Login(t, jwt.MapClaims{
+ "email": "alice@coder.com",
+ "roles": []string{"random", oidcRoleName, rbac.RoleOwner()},
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ runner.AssertRoles(t, "alice", []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(), rbac.RoleOwner()})
- config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) {
- cfg.UserRoleMapping = map[string][]string{oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}}
+ // Now refresh the oauth, and check the roles are removed.
+ // Force a refresh, and assert nothing has changes
+ runner.ForceRefresh(t, client, jwt.MapClaims{
+ "email": "alice@coder.com",
+ "roles": []string{"random"},
})
- config.AllowSignups = true
- config.UserRoleField = "roles"
+ runner.AssertRoles(t, "alice", []string{})
+ })
- client, _ := coderdenttest.New(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- OIDCConfig: config,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{codersdk.FeatureUserRoleManagement: 1},
+ // A user has some roles, then on another oauth login will lose said
+ // roles from an updated claim.
+ t.Run("NewUserAndRemoveRolesOnReAuth", func(t *testing.T) {
+ t.Parallel()
+
+ const oidcRoleName = "TemplateAuthor"
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Userinfo: jwt.MapClaims{oidcRoleName: []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()}},
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.UserRoleField = "roles"
+ cfg.UserRoleMapping = map[string][]string{
+ oidcRoleName: {rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin()},
+ }
},
})
- admin, err := client.User(ctx, "me")
- require.NoError(t, err)
- require.Len(t, admin.OrganizationIDs, 1)
-
- resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
+ // User starts with the owner role
+ _, resp := runner.Login(t, jwt.MapClaims{
"email": "alice@coder.com",
"roles": []string{"random", oidcRoleName, rbac.RoleOwner()},
- }))
- require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
- user, err := client.User(ctx, "alice")
- require.NoError(t, err)
-
- require.Len(t, user.Roles, 3)
- roleNames := []string{user.Roles[0].Name, user.Roles[1].Name, user.Roles[2].Name}
- require.ElementsMatch(t, roleNames, []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(), rbac.RoleOwner()})
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ runner.AssertRoles(t, "alice", []string{rbac.RoleTemplateAdmin(), rbac.RoleUserAdmin(), rbac.RoleOwner()})
- // Now remove the roles with a new oidc login
- resp = oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
+ // Now login with oauth again, and check the roles are removed.
+ _, resp = runner.Login(t, jwt.MapClaims{
"email": "alice@coder.com",
"roles": []string{"random"},
- }))
- require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
- user, err = client.User(ctx, "alice")
- require.NoError(t, err)
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
- require.Len(t, user.Roles, 0)
+ runner.AssertRoles(t, "alice", []string{})
})
+
+ // All manual role updates should fail when role sync is enabled.
t.Run("BlockAssignRoles", func(t *testing.T) {
t.Parallel()
- ctx := testutil.Context(t, testutil.WaitMedium)
- conf := coderdtest.NewOIDCConfig(t, "")
-
- config := conf.OIDCConfig(t, jwt.MapClaims{})
- config.AllowSignups = true
- config.UserRoleField = "roles"
-
- client, _ := coderdenttest.New(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- OIDCConfig: config,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{codersdk.FeatureUserRoleManagement: 1},
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.UserRoleField = "roles"
},
})
- admin, err := client.User(ctx, "me")
- require.NoError(t, err)
- require.Len(t, admin.OrganizationIDs, 1)
-
- resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
+ _, resp := runner.Login(t, jwt.MapClaims{
"email": "alice@coder.com",
"roles": []string{},
- }))
- require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
// Try to manually update user roles, even though controlled by oidc
// role sync.
- _, err = client.UpdateUserRoles(ctx, "alice", codersdk.UpdateRoles{
+ ctx := testutil.Context(t, testutil.WaitShort)
+ _, err := runner.AdminClient.UpdateUserRoles(ctx, "alice", codersdk.UpdateRoles{
Roles: []string{
rbac.RoleTemplateAdmin(),
},
@@ -159,217 +156,514 @@ func TestUserOIDC(t *testing.T) {
t.Run("Groups", func(t *testing.T) {
t.Parallel()
+
+ // Assigns does a simple test of assigning a user to a group based
+ // on the oidc claims.
t.Run("Assigns", func(t *testing.T) {
t.Parallel()
- ctx := testutil.Context(t, testutil.WaitLong)
- conf := coderdtest.NewOIDCConfig(t, "")
-
const groupClaim = "custom-groups"
- config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) {
- cfg.GroupField = groupClaim
- })
- config.AllowSignups = true
-
- client, _ := coderdenttest.New(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- OIDCConfig: config,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{codersdk.FeatureTemplateRBAC: 1},
+ const groupName = "bingbong"
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.GroupField = groupClaim
},
})
- admin, err := client.User(ctx, "me")
- require.NoError(t, err)
- require.Len(t, admin.OrganizationIDs, 1)
-
- groupName := "bingbong"
- group, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{
+ ctx := testutil.Context(t, testutil.WaitShort)
+ group, err := runner.AdminClient.CreateGroup(ctx, runner.AdminUser.OrganizationIDs[0], codersdk.CreateGroupRequest{
Name: groupName,
})
require.NoError(t, err)
require.Len(t, group.Members, 0)
- resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
- "email": "colin@coder.com",
+ _, resp := runner.Login(t, jwt.MapClaims{
+ "email": "alice@coder.com",
groupClaim: []string{groupName},
- }))
- assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
-
- group, err = client.Group(ctx, group.ID)
- require.NoError(t, err)
- require.Len(t, group.Members, 1)
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ runner.AssertGroups(t, "alice", []string{groupName})
})
+
+ // Tests the group mapping feature.
t.Run("AssignsMapped", func(t *testing.T) {
t.Parallel()
- ctx := testutil.Context(t, testutil.WaitMedium)
- conf := coderdtest.NewOIDCConfig(t, "")
+ const groupClaim = "custom-groups"
- oidcGroupName := "pingpong"
- coderGroupName := "bingbong"
+ const oidcGroupName = "pingpong"
+ const coderGroupName = "bingbong"
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.GroupField = groupClaim
+ cfg.GroupMapping = map[string]string{oidcGroupName: coderGroupName}
+ },
+ })
- config := conf.OIDCConfig(t, jwt.MapClaims{}, func(cfg *coderd.OIDCConfig) {
- cfg.GroupMapping = map[string]string{oidcGroupName: coderGroupName}
+ ctx := testutil.Context(t, testutil.WaitShort)
+ group, err := runner.AdminClient.CreateGroup(ctx, runner.AdminUser.OrganizationIDs[0], codersdk.CreateGroupRequest{
+ Name: coderGroupName,
})
- config.AllowSignups = true
+ require.NoError(t, err)
+ require.Len(t, group.Members, 0)
- client, _ := coderdenttest.New(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- OIDCConfig: config,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{codersdk.FeatureTemplateRBAC: 1},
- },
+ _, resp := runner.Login(t, jwt.MapClaims{
+ "email": "alice@coder.com",
+ groupClaim: []string{oidcGroupName},
})
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ runner.AssertGroups(t, "alice", []string{coderGroupName})
+ })
- admin, err := client.User(ctx, "me")
- require.NoError(t, err)
- require.Len(t, admin.OrganizationIDs, 1)
+ // User is in a group, then on an oauth refresh will lose said
+ // group.
+ t.Run("AddThenRemoveOnRefresh", func(t *testing.T) {
+ t.Parallel()
- group, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{
- Name: coderGroupName,
+ // TODO: Implement new feature to update roles/groups on OIDC
+ // refresh tokens. https://github.com/coder/coder/issues/9312
+ t.Skip("Refreshing tokens does not update groups :(")
+
+ const groupClaim = "custom-groups"
+ const groupName = "bingbong"
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.GroupField = groupClaim
+ },
+ })
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+ group, err := runner.AdminClient.CreateGroup(ctx, runner.AdminUser.OrganizationIDs[0], codersdk.CreateGroupRequest{
+ Name: groupName,
})
require.NoError(t, err)
require.Len(t, group.Members, 0)
- resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
- "email": "colin@coder.com",
- "groups": []string{oidcGroupName},
- }))
- assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
+ client, resp := runner.Login(t, jwt.MapClaims{
+ "email": "alice@coder.com",
+ groupClaim: []string{groupName},
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ runner.AssertGroups(t, "alice", []string{groupName})
- group, err = client.Group(ctx, group.ID)
- require.NoError(t, err)
- require.Len(t, group.Members, 1)
+ // Refresh without the group claim
+ runner.ForceRefresh(t, client, jwt.MapClaims{
+ "email": "alice@coder.com",
+ })
+ runner.AssertGroups(t, "alice", []string{})
})
- t.Run("AddThenRemove", func(t *testing.T) {
+ t.Run("AddThenRemoveOnReAuth", func(t *testing.T) {
t.Parallel()
- ctx := testutil.Context(t, testutil.WaitLong)
- conf := coderdtest.NewOIDCConfig(t, "")
-
- config := conf.OIDCConfig(t, jwt.MapClaims{})
- config.AllowSignups = true
-
- client, firstUser := coderdenttest.New(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- OIDCConfig: config,
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{codersdk.FeatureTemplateRBAC: 1},
+ const groupClaim = "custom-groups"
+ const groupName = "bingbong"
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.GroupField = groupClaim
},
})
- // Add some extra users/groups that should be asserted after.
- // Adding this user as there was a bug that removing 1 user removed
- // all users from the group.
- _, extra := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
- groupName := "bingbong"
- group, err := client.CreateGroup(ctx, firstUser.OrganizationID, codersdk.CreateGroupRequest{
+ ctx := testutil.Context(t, testutil.WaitShort)
+ group, err := runner.AdminClient.CreateGroup(ctx, runner.AdminUser.OrganizationIDs[0], codersdk.CreateGroupRequest{
Name: groupName,
})
- require.NoError(t, err, "create group")
+ require.NoError(t, err)
+ require.Len(t, group.Members, 0)
+
+ _, resp := runner.Login(t, jwt.MapClaims{
+ "email": "alice@coder.com",
+ groupClaim: []string{groupName},
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ runner.AssertGroups(t, "alice", []string{groupName})
- group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
- AddUsers: []string{
- firstUser.UserID.String(),
- extra.ID.String(),
+ // Refresh without the group claim
+ _, resp = runner.Login(t, jwt.MapClaims{
+ "email": "alice@coder.com",
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ runner.AssertGroups(t, "alice", []string{})
+ })
+
+ // Updating groups where the claimed group does not exist.
+ t.Run("NoneMatch", func(t *testing.T) {
+ t.Parallel()
+
+ const groupClaim = "custom-groups"
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.GroupField = groupClaim
},
})
- require.NoError(t, err, "patch group")
- require.Len(t, group.Members, 2, "expect both members")
- // Now add OIDC user into the group
- resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
- "email": "colin@coder.com",
- "groups": []string{groupName},
- }))
- assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
+ _, resp := runner.Login(t, jwt.MapClaims{
+ "email": "alice@coder.com",
+ groupClaim: []string{"not-exists"},
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ runner.AssertGroups(t, "alice", []string{})
+ })
- group, err = client.Group(ctx, group.ID)
- require.NoError(t, err)
- require.Len(t, group.Members, 3)
+ // Updating groups where the claimed group does not exist creates
+ // the group.
+ t.Run("AutoCreate", func(t *testing.T) {
+ t.Parallel()
- // Login to remove the OIDC user from the group
- resp = oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
- "email": "colin@coder.com",
- "groups": []string{},
- }))
- assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
+ const groupClaim = "custom-groups"
+ const groupName = "make-me"
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.GroupField = groupClaim
+ cfg.CreateMissingGroups = true
+ },
+ })
- group, err = client.Group(ctx, group.ID)
- require.NoError(t, err)
- require.Len(t, group.Members, 2)
- var expected []uuid.UUID
- for _, mem := range group.Members {
- expected = append(expected, mem.ID)
- }
- require.ElementsMatchf(t, expected, []uuid.UUID{firstUser.UserID, extra.ID}, "expected members")
+ _, resp := runner.Login(t, jwt.MapClaims{
+ "email": "alice@coder.com",
+ groupClaim: []string{groupName},
+ })
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ runner.AssertGroups(t, "alice", []string{groupName})
})
+ })
- t.Run("NoneMatch", func(t *testing.T) {
+ t.Run("Refresh", func(t *testing.T) {
+ t.Run("RefreshTokensMultiple", func(t *testing.T) {
t.Parallel()
- ctx := testutil.Context(t, testutil.WaitLong)
- conf := coderdtest.NewOIDCConfig(t, "")
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.AllowSignups = true
+ cfg.UserRoleField = "roles"
+ },
+ })
+
+ claims := jwt.MapClaims{
+ "email": "alice@coder.com",
+ }
+ // Login a new client that signs up
+ client, resp := runner.Login(t, claims)
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ // Refresh multiple times.
+ for i := 0; i < 3; i++ {
+ runner.ForceRefresh(t, client, claims)
+ }
+ })
+ })
+}
- config := conf.OIDCConfig(t, jwt.MapClaims{})
- config.AllowSignups = true
+// nolint:bodyclose
+func TestGroupSync(t *testing.T) {
+ t.Parallel()
- client, _ := coderdenttest.New(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- OIDCConfig: config,
+ testCases := []struct {
+ name string
+ modCfg func(cfg *coderd.OIDCConfig)
+ // initialOrgGroups is initial groups in the org
+ initialOrgGroups []string
+ // initialUserGroups is initial groups for the user
+ initialUserGroups []string
+ // expectedUserGroups is expected groups for the user
+ expectedUserGroups []string
+ // expectedOrgGroups is expected all groups on the system
+ expectedOrgGroups []string
+ claims jwt.MapClaims
+ }{
+ {
+ name: "NoGroups",
+ modCfg: func(cfg *coderd.OIDCConfig) {
+ },
+ initialOrgGroups: []string{},
+ expectedUserGroups: []string{},
+ expectedOrgGroups: []string{},
+ claims: jwt.MapClaims{},
+ },
+ {
+ name: "GroupSyncDisabled",
+ modCfg: func(cfg *coderd.OIDCConfig) {
+ // Disable group sync
+ cfg.GroupField = ""
+ cfg.GroupFilter = regexp.MustCompile(".*")
+ },
+ initialOrgGroups: []string{"a", "b", "c", "d"},
+ initialUserGroups: []string{"b", "c", "d"},
+ expectedUserGroups: []string{"b", "c", "d"},
+ expectedOrgGroups: []string{"a", "b", "c", "d"},
+ claims: jwt.MapClaims{},
+ },
+ {
+ // From a,c,b -> b,c,d
+ name: "ChangeUserGroups",
+ modCfg: func(cfg *coderd.OIDCConfig) {
+ cfg.GroupMapping = map[string]string{
+ "D": "d",
+ }
+ },
+ initialOrgGroups: []string{"a", "b", "c", "d"},
+ initialUserGroups: []string{"a", "b", "c"},
+ expectedUserGroups: []string{"b", "c", "d"},
+ expectedOrgGroups: []string{"a", "b", "c", "d"},
+ claims: jwt.MapClaims{
+ // D -> d mapped
+ "groups": []string{"b", "c", "D"},
+ },
+ },
+ {
+ // From a,c,b -> []
+ name: "RemoveAllGroups",
+ modCfg: func(cfg *coderd.OIDCConfig) {
+ cfg.GroupFilter = regexp.MustCompile(".*")
+ },
+ initialOrgGroups: []string{"a", "b", "c", "d"},
+ initialUserGroups: []string{"a", "b", "c"},
+ expectedUserGroups: []string{},
+ expectedOrgGroups: []string{"a", "b", "c", "d"},
+ claims: jwt.MapClaims{
+ // No claim == no groups
+ },
+ },
+ {
+ // From a,c,b -> b,c,d,e,f
+ name: "CreateMissingGroups",
+ modCfg: func(cfg *coderd.OIDCConfig) {
+ cfg.CreateMissingGroups = true
+ },
+ initialOrgGroups: []string{"a", "b", "c", "d"},
+ initialUserGroups: []string{"a", "b", "c"},
+ expectedUserGroups: []string{"b", "c", "d", "e", "f"},
+ expectedOrgGroups: []string{"a", "b", "c", "d", "e", "f"},
+ claims: jwt.MapClaims{
+ "groups": []string{"b", "c", "d", "e", "f"},
+ },
+ },
+ {
+ // From a,c,b -> b,c,d,e,f
+ name: "CreateMissingGroupsFilter",
+ modCfg: func(cfg *coderd.OIDCConfig) {
+ cfg.CreateMissingGroups = true
+ // Only single letter groups
+ cfg.GroupFilter = regexp.MustCompile("^[a-z]$")
+ cfg.GroupMapping = map[string]string{
+ // Does not match the filter, but does after being mapped!
+ "zebra": "z",
+ }
+ },
+ initialOrgGroups: []string{"a", "b", "c", "d"},
+ initialUserGroups: []string{"a", "b", "c"},
+ expectedUserGroups: []string{"b", "c", "d", "e", "f", "z"},
+ expectedOrgGroups: []string{"a", "b", "c", "d", "e", "f", "z"},
+ claims: jwt.MapClaims{
+ "groups": []string{
+ "b", "c", "d", "e", "f",
+ // These groups are ignored
+ "excess", "ignore", "dumb", "foobar", "zebra",
},
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{codersdk.FeatureTemplateRBAC: 1},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ runner := setupOIDCTest(t, oidcTestConfig{
+ Config: func(cfg *coderd.OIDCConfig) {
+ cfg.GroupField = "groups"
+ tc.modCfg(cfg)
},
})
- admin, err := client.User(ctx, "me")
- require.NoError(t, err)
- require.Len(t, admin.OrganizationIDs, 1)
+ // Setup
+ ctx := testutil.Context(t, testutil.WaitLong)
+ org := runner.AdminUser.OrganizationIDs[0]
+
+ initialGroups := make(map[string]codersdk.Group)
+ for _, group := range tc.initialOrgGroups {
+ newGroup, err := runner.AdminClient.CreateGroup(ctx, org, codersdk.CreateGroupRequest{
+ Name: group,
+ })
+ require.NoError(t, err)
+ require.Len(t, newGroup.Members, 0)
+ initialGroups[group] = newGroup
+ }
- groupName := "bingbong"
- group, err := client.CreateGroup(ctx, admin.OrganizationIDs[0], codersdk.CreateGroupRequest{
- Name: groupName,
+ // Create the user and add them to their initial groups
+ _, user := coderdtest.CreateAnotherUser(t, runner.AdminClient, org)
+ for _, group := range tc.initialUserGroups {
+ _, err := runner.AdminClient.PatchGroup(ctx, initialGroups[group].ID, codersdk.PatchGroupRequest{
+ AddUsers: []string{user.ID.String()},
+ })
+ require.NoError(t, err)
+ }
+
+ // nolint:gocritic
+ _, err := runner.API.Database.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{
+ NewLoginType: database.LoginTypeOIDC,
+ UserID: user.ID,
})
- require.NoError(t, err)
- require.Len(t, group.Members, 0)
+ require.NoError(t, err, "user must be oidc type")
- resp := oidcCallback(t, client, conf.EncodeClaims(t, jwt.MapClaims{
- "email": "colin@coder.com",
- "groups": []string{"coolin"},
- }))
- assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
+ // Log in the new user
+ tc.claims["email"] = user.Email
+ _, resp := runner.Login(t, tc.claims)
+ require.Equal(t, http.StatusOK, resp.StatusCode)
- group, err = client.Group(ctx, group.ID)
+ // Check group sources
+ orgGroups, err := runner.AdminClient.GroupsByOrganization(ctx, org)
require.NoError(t, err)
- require.Len(t, group.Members, 0)
+
+ for _, group := range orgGroups {
+ if slice.Contains(tc.initialOrgGroups, group.Name) || group.IsEveryone() {
+ require.Equal(t, group.Source, codersdk.GroupSourceUser)
+ } else {
+ require.Equal(t, group.Source, codersdk.GroupSourceOIDC)
+ }
+ }
+
+ orgGroupsMap := make(map[string]struct{})
+ for _, group := range orgGroups {
+ orgGroupsMap[group.Name] = struct{}{}
+ }
+
+ for _, expected := range tc.expectedOrgGroups {
+ if _, ok := orgGroupsMap[expected]; !ok {
+ t.Errorf("expected group %s not found", expected)
+ }
+ delete(orgGroupsMap, expected)
+ }
+ delete(orgGroupsMap, database.EveryoneGroup)
+ require.Empty(t, orgGroupsMap, "unexpected groups found")
+
+ expectedUserGroups := make(map[string]struct{})
+ for _, group := range tc.expectedUserGroups {
+ expectedUserGroups[group] = struct{}{}
+ }
+
+ for _, group := range orgGroups {
+ userInGroup := slice.ContainsCompare(group.Members, codersdk.User{Email: user.Email}, func(a, b codersdk.User) bool {
+ return a.Email == b.Email
+ })
+ if group.IsEveryone() {
+ require.True(t, userInGroup, "user cannot be removed from 'Everyone' group")
+ } else if _, ok := expectedUserGroups[group.Name]; ok {
+ require.Truef(t, userInGroup, "user should be in group %s", group.Name)
+ } else {
+ require.Falsef(t, userInGroup, "user should not be in group %s", group.Name)
+ }
+ }
})
- })
+ }
+}
+
+// oidcTestRunner is just a helper to setup and run oidc tests.
+// An actual Coderd instance is used to run the tests.
+type oidcTestRunner struct {
+ AdminClient *codersdk.Client
+ AdminUser codersdk.User
+ API *coderden.API
+
+ // Login will call the OIDC flow with an unauthenticated client.
+ // The IDP will return the idToken claims.
+ Login func(t *testing.T, idToken jwt.MapClaims) (*codersdk.Client, *http.Response)
+ // ForceRefresh will use an authenticated codersdk.Client, and force their
+ // OIDC token to be expired and require a refresh. The refresh will use the claims provided.
+ // It just calls the /users/me endpoint to trigger the refresh.
+ ForceRefresh func(t *testing.T, client *codersdk.Client, idToken jwt.MapClaims)
}
-func oidcCallback(t *testing.T, client *codersdk.Client, code string) *http.Response {
+type oidcTestConfig struct {
+ Userinfo jwt.MapClaims
+
+ // Config allows modifying the Coderd OIDC configuration.
+ Config func(cfg *coderd.OIDCConfig)
+}
+
+func (r *oidcTestRunner) AssertRoles(t *testing.T, userIdent string, roles []string) {
t.Helper()
- client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
- return http.ErrUseLastResponse
+
+ ctx := testutil.Context(t, testutil.WaitMedium)
+ user, err := r.AdminClient.User(ctx, userIdent)
+ require.NoError(t, err)
+
+ roleNames := []string{}
+ for _, role := range user.Roles {
+ roleNames = append(roleNames, role.Name)
+ }
+ require.ElementsMatch(t, roles, roleNames, "expected roles")
+}
+
+func (r *oidcTestRunner) AssertGroups(t *testing.T, userIdent string, groups []string) {
+ t.Helper()
+
+ if !slice.Contains(groups, database.EveryoneGroup) {
+ var cpy []string
+ cpy = append(cpy, groups...)
+ // always include everyone group
+ cpy = append(cpy, database.EveryoneGroup)
+ groups = cpy
}
- oauthURL, err := client.URL.Parse(fmt.Sprintf("/api/v2/users/oidc/callback?code=%s&state=somestate", code))
+ ctx := testutil.Context(t, testutil.WaitMedium)
+ user, err := r.AdminClient.User(ctx, userIdent)
require.NoError(t, err)
- req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
+
+ allGroups, err := r.AdminClient.GroupsByOrganization(ctx, user.OrganizationIDs[0])
require.NoError(t, err)
- req.AddCookie(&http.Cookie{
- Name: codersdk.OAuth2StateCookie,
- Value: "somestate",
+
+ userInGroups := []string{}
+ for _, g := range allGroups {
+ for _, mem := range g.Members {
+ if mem.ID == user.ID {
+ userInGroups = append(userInGroups, g.Name)
+ }
+ }
+ }
+
+ require.ElementsMatch(t, groups, userInGroups, "expected groups")
+}
+
+func setupOIDCTest(t *testing.T, settings oidcTestConfig) *oidcTestRunner {
+ t.Helper()
+
+ fake := oidctest.NewFakeIDP(t,
+ oidctest.WithStaticUserInfo(settings.Userinfo),
+ oidctest.WithLogging(t, nil),
+ // Run fake IDP on a real webserver
+ oidctest.WithServing(),
+ )
+
+ ctx := testutil.Context(t, testutil.WaitMedium)
+ cfg := fake.OIDCConfig(t, nil, settings.Config)
+ owner, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
+ Options: &coderdtest.Options{
+ OIDCConfig: cfg,
+ },
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureUserRoleManagement: 1,
+ codersdk.FeatureTemplateRBAC: 1,
+ },
+ },
})
- res, err := client.HTTPClient.Do(req)
- require.NoError(t, err)
- defer res.Body.Close()
- data, err := io.ReadAll(res.Body)
+ admin, err := owner.User(ctx, "me")
require.NoError(t, err)
- t.Log(string(data))
- return res
+
+ helper := oidctest.NewLoginHelper(owner, fake)
+
+ return &oidcTestRunner{
+ AdminClient: owner,
+ AdminUser: admin,
+ API: api,
+ Login: helper.Login,
+ ForceRefresh: func(t *testing.T, client *codersdk.Client, idToken jwt.MapClaims) {
+ helper.ForceRefresh(t, api.Database, client, idToken)
+ },
+ }
}
diff --git a/enterprise/coderd/users.go b/enterprise/coderd/users.go
index 3f1e61fddb10d..4b576c3856464 100644
--- a/enterprise/coderd/users.go
+++ b/enterprise/coderd/users.go
@@ -4,11 +4,11 @@ import (
"net/http"
"time"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
func (api *API) restartRequirementEnabledMW(next http.Handler) http.Handler {
diff --git a/enterprise/coderd/users_test.go b/enterprise/coderd/users_test.go
index b908dfd9600bf..bd8080a6b052a 100644
--- a/enterprise/coderd/users_test.go
+++ b/enterprise/coderd/users_test.go
@@ -7,12 +7,12 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/testutil"
)
func TestUserQuietHours(t *testing.T) {
diff --git a/enterprise/coderd/workspaceagents.go b/enterprise/coderd/workspaceagents.go
index 05590342aec5b..d14aa9580bbd4 100644
--- a/enterprise/coderd/workspaceagents.go
+++ b/enterprise/coderd/workspaceagents.go
@@ -4,8 +4,8 @@ import (
"context"
"net/http"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
func (api *API) shouldBlockNonBrowserConnections(rw http.ResponseWriter) bool {
diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go
index 0395d71c3ca4e..5b7ecbe1bcfd8 100644
--- a/enterprise/coderd/workspaceagents_test.go
+++ b/enterprise/coderd/workspaceagents_test.go
@@ -11,15 +11,15 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
// App names for each app sharing level.
@@ -73,9 +73,9 @@ func setupWorkspaceAgent(t *testing.T, client *codersdk.Client, user codersdk.Cr
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
diff --git a/enterprise/coderd/workspaceproxy.go b/enterprise/coderd/workspaceproxy.go
index 591f2fa33374f..22ab937e7f3ce 100644
--- a/enterprise/coderd/workspaceproxy.go
+++ b/enterprise/coderd/workspaceproxy.go
@@ -15,20 +15,21 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/buildinfo"
- agpl "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/enterprise/coderd/proxyhealth"
- "github.com/coder/coder/enterprise/replicasync"
- "github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
+ "github.com/coder/coder/v2/buildinfo"
+ agpl "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/audit"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/telemetry"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/enterprise/coderd/proxyhealth"
+ "github.com/coder/coder/v2/enterprise/replicasync"
+ "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
)
// forceWorkspaceProxyHealthUpdate forces an update of the proxy health.
@@ -369,6 +370,10 @@ func (api *API) postWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
return
}
+ api.Telemetry.Report(&telemetry.Snapshot{
+ WorkspaceProxies: []telemetry.WorkspaceProxy{telemetry.ConvertWorkspaceProxy(proxy)},
+ })
+
aReq.New = proxy
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.UpdateWorkspaceProxyResponse{
Proxy: convertProxy(proxy, proxyhealth.ProxyStatus{
@@ -492,6 +497,36 @@ func (api *API) workspaceProxyIssueSignedAppToken(rw http.ResponseWriter, r *htt
})
}
+// @Summary Report workspace app stats
+// @ID report-workspace-app-stats
+// @Security CoderSessionToken
+// @Accept json
+// @Tags Enterprise
+// @Param request body wsproxysdk.ReportAppStatsRequest true "Report app stats request"
+// @Success 204
+// @Router /workspaceproxies/me/app-stats [post]
+// @x-apidocgen {"skip": true}
+func (api *API) workspaceProxyReportAppStats(rw http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ _ = httpmw.WorkspaceProxy(r) // Ensure the proxy is authenticated.
+
+ var req wsproxysdk.ReportAppStatsRequest
+ if !httpapi.Read(ctx, rw, r, &req) {
+ return
+ }
+
+ api.Logger.Debug(ctx, "report app stats", slog.F("stats", req.Stats))
+
+ reporter := api.WorkspaceAppsStatsCollectorOptions.Reporter
+ if err := reporter.Report(ctx, req.Stats); err != nil {
+ api.Logger.Error(ctx, "report app stats failed", slog.Error(err))
+ httpapi.InternalServerError(rw, err)
+ return
+ }
+
+ httpapi.Write(ctx, rw, http.StatusNoContent, nil)
+}
+
// workspaceProxyRegister is used to register a new workspace proxy. When a proxy
// comes online, it will announce itself to this endpoint. This updates its values
// in the database and returns a signed token that can be used to authenticate
@@ -682,10 +717,12 @@ func (api *API) workspaceProxyRegister(rw http.ResponseWriter, r *http.Request)
// aReq.New = updatedProxy
httpapi.Write(ctx, rw, http.StatusCreated, wsproxysdk.RegisterWorkspaceProxyResponse{
- AppSecurityKey: api.AppSecurityKey.String(),
- DERPMeshKey: api.DERPServer.MeshKey(),
- DERPRegionID: regionID,
- SiblingReplicas: siblingsRes,
+ AppSecurityKey: api.AppSecurityKey.String(),
+ DERPMeshKey: api.DERPServer.MeshKey(),
+ DERPRegionID: regionID,
+ DERPMap: api.AGPL.DERPMap(),
+ DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
+ SiblingReplicas: siblingsRes,
})
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
diff --git a/enterprise/coderd/workspaceproxy_test.go b/enterprise/coderd/workspaceproxy_test.go
index 781ef3974ed15..745a1af2c11bc 100644
--- a/enterprise/coderd/workspaceproxy_test.go
+++ b/enterprise/coderd/workspaceproxy_test.go
@@ -16,19 +16,19 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
func TestRegions(t *testing.T) {
diff --git a/enterprise/coderd/workspaceproxycoordinate.go b/enterprise/coderd/workspaceproxycoordinate.go
index 919098a3d8b6a..ec454d73a870a 100644
--- a/enterprise/coderd/workspaceproxycoordinate.go
+++ b/enterprise/coderd/workspaceproxycoordinate.go
@@ -6,11 +6,11 @@ import (
"github.com/google/uuid"
"nhooyr.io/websocket"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/tailnet"
- "github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/tailnet"
+ "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
)
// @Summary Agent is legacy
@@ -68,7 +68,7 @@ func (api *API) workspaceProxyCoordinate(rw http.ResponseWriter, r *http.Request
id := uuid.New()
sub := (*api.AGPL.TailnetCoordinator.Load()).ServeMultiAgent(id)
- nc := websocket.NetConn(ctx, conn, websocket.MessageText)
+ ctx, nc := websocketNetConn(ctx, conn, websocket.MessageText)
defer nc.Close()
err = tailnet.ServeWorkspaceProxy(ctx, nc, sub)
diff --git a/enterprise/coderd/workspaceproxycoordinator_test.go b/enterprise/coderd/workspaceproxycoordinator_test.go
index 6a2df0d6cd279..de72c288b2eee 100644
--- a/enterprise/coderd/workspaceproxycoordinator_test.go
+++ b/enterprise/coderd/workspaceproxycoordinator_test.go
@@ -13,14 +13,14 @@ import (
"tailscale.com/types/key"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
- agpl "github.com/coder/coder/tailnet"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
+ agpl "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/testutil"
)
// workspaceProxyCoordinate and agentIsLegacy are both tested by wsproxy tests.
diff --git a/enterprise/coderd/workspacequota.go b/enterprise/coderd/workspacequota.go
index 28198ca9a3570..44ea3f302ff37 100644
--- a/enterprise/coderd/workspacequota.go
+++ b/enterprise/coderd/workspacequota.go
@@ -3,20 +3,23 @@ package coderd
import (
"context"
"database/sql"
+ "errors"
"net/http"
"github.com/google/uuid"
- "golang.org/x/xerrors"
-
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/rbac"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisionerd/proto"
+
+ "cdr.dev/slog"
+
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisionerd/proto"
)
type committer struct {
+ Log slog.Logger
Database database.Store
}
@@ -28,12 +31,12 @@ func (c *committer) CommitQuota(
return nil, err
}
- build, err := c.Database.GetWorkspaceBuildByJobID(ctx, jobID)
+ nextBuild, err := c.Database.GetWorkspaceBuildByJobID(ctx, jobID)
if err != nil {
return nil, err
}
- workspace, err := c.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
+ workspace, err := c.Database.GetWorkspaceByID(ctx, nextBuild.WorkspaceID)
if err != nil {
return nil, err
}
@@ -58,25 +61,35 @@ func (c *committer) CommitQuota(
// If the new build will reduce overall quota consumption, then we
// allow it even if the user is over quota.
netIncrease := true
- previousBuild, err := s.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{
+ prevBuild, err := s.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{
WorkspaceID: workspace.ID,
- BuildNumber: build.BuildNumber - 1,
+ BuildNumber: nextBuild.BuildNumber - 1,
})
if err == nil {
- if build.DailyCost < previousBuild.DailyCost {
- netIncrease = false
- }
- } else if !xerrors.Is(err, sql.ErrNoRows) {
+ netIncrease = request.DailyCost >= prevBuild.DailyCost
+ c.Log.Debug(
+ ctx, "previous build cost",
+ slog.F("prev_cost", prevBuild.DailyCost),
+ slog.F("next_cost", request.DailyCost),
+ slog.F("net_increase", netIncrease),
+ )
+ } else if !errors.Is(err, sql.ErrNoRows) {
return err
}
newConsumed := int64(request.DailyCost) + consumed
if newConsumed > budget && netIncrease {
+ c.Log.Debug(
+ ctx, "over quota, rejecting",
+ slog.F("prev_consumed", consumed),
+ slog.F("next_consumed", newConsumed),
+ slog.F("budget", budget),
+ )
return nil
}
err = s.UpdateWorkspaceBuildCostByID(ctx, database.UpdateWorkspaceBuildCostByIDParams{
- ID: build.ID,
+ ID: nextBuild.ID,
DailyCost: request.DailyCost,
})
if err != nil {
diff --git a/enterprise/coderd/workspacequota_test.go b/enterprise/coderd/workspacequota_test.go
index a142c86535c4d..69c9a4bc3de88 100644
--- a/enterprise/coderd/workspacequota_test.go
+++ b/enterprise/coderd/workspacequota_test.go
@@ -9,13 +9,15 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func verifyQuota(ctx context.Context, t *testing.T, client *codersdk.Client, consumed, total int) {
@@ -30,12 +32,13 @@ func verifyQuota(ctx context.Context, t *testing.T, client *codersdk.Client, con
}
func TestWorkspaceQuota(t *testing.T) {
- // TODO: refactor for new impl
-
t.Parallel()
- t.Run("BlocksBuild", func(t *testing.T) {
+ // This first test verifies the behavior of creating and deleting workspaces.
+ // It also tests multi-group quota stacking and the everyone group.
+ t.Run("CreateDelete", func(t *testing.T) {
t.Parallel()
+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
max := 1
@@ -48,12 +51,17 @@ func TestWorkspaceQuota(t *testing.T) {
},
})
coderdtest.NewProvisionerDaemon(t, api.AGPL)
- coderdtest.NewProvisionerDaemon(t, api.AGPL)
- coderdtest.NewProvisionerDaemon(t, api.AGPL)
verifyQuota(ctx, t, client, 0, 0)
- // Add user to two groups, granting them a total budget of 3.
+ // Patch the 'Everyone' group to verify its quota allowance is being accounted for.
+ _, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
+ QuotaAllowance: ptr.Ref(1),
+ })
+ require.NoError(t, err)
+ verifyQuota(ctx, t, client, 0, 1)
+
+ // Add user to two groups, granting them a total budget of 4.
group1, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "test-1",
QuotaAllowance: 1,
@@ -76,14 +84,14 @@ func TestWorkspaceQuota(t *testing.T) {
})
require.NoError(t, err)
- verifyQuota(ctx, t, client, 0, 3)
+ verifyQuota(ctx, t, client, 0, 4)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -105,7 +113,7 @@ func TestWorkspaceQuota(t *testing.T) {
// Spin up three workspaces fine
var wg sync.WaitGroup
- for i := 0; i < 3; i++ {
+ for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
@@ -115,14 +123,14 @@ func TestWorkspaceQuota(t *testing.T) {
}()
}
wg.Wait()
- verifyQuota(ctx, t, client, 3, 3)
+ verifyQuota(ctx, t, client, 4, 4)
// Next one must fail
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
// Consumed shouldn't bump
- verifyQuota(ctx, t, client, 3, 3)
+ verifyQuota(ctx, t, client, 4, 4)
require.Equal(t, codersdk.WorkspaceStatusFailed, build.Status)
require.Contains(t, build.Job.Error, "quota")
@@ -138,7 +146,7 @@ func TestWorkspaceQuota(t *testing.T) {
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
- verifyQuota(ctx, t, client, 2, 3)
+ verifyQuota(ctx, t, client, 3, 4)
break
}
@@ -146,7 +154,111 @@ func TestWorkspaceQuota(t *testing.T) {
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
build = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
- verifyQuota(ctx, t, client, 3, 3)
+ verifyQuota(ctx, t, client, 4, 4)
+ require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
+ })
+
+ t.Run("StartStop", func(t *testing.T) {
+ t.Parallel()
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+ max := 1
+ client, _, api, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
+ UserWorkspaceQuota: max,
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureTemplateRBAC: 1,
+ },
+ },
+ })
+ coderdtest.NewProvisionerDaemon(t, api.AGPL)
+
+ verifyQuota(ctx, t, client, 0, 0)
+
+ // Patch the 'Everyone' group to verify its quota allowance is being accounted for.
+ _, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
+ QuotaAllowance: ptr.Ref(4),
+ })
+ require.NoError(t, err)
+ verifyQuota(ctx, t, client, 0, 4)
+
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Response{
+ proto.WorkspaceTransition_START: planWithCost(2),
+ proto.WorkspaceTransition_STOP: planWithCost(1),
+ },
+ ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{
+ proto.WorkspaceTransition_START: applyWithCost(2),
+ proto.WorkspaceTransition_STOP: applyWithCost(1),
+ },
+ })
+
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+
+ // Spin up two workspaces.
+ var wg sync.WaitGroup
+ var workspaces []codersdk.Workspace
+ for i := 0; i < 2; i++ {
+ workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ workspaces = append(workspaces, workspace)
+ build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
+ assert.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
+ }
+ wg.Wait()
+ verifyQuota(ctx, t, client, 4, 4)
+
+ // Next one must fail
+ workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
+ require.Contains(t, build.Job.Error, "quota")
+
+ // Consumed shouldn't bump
+ verifyQuota(ctx, t, client, 4, 4)
+ require.Equal(t, codersdk.WorkspaceStatusFailed, build.Status)
+
+ build = coderdtest.CreateWorkspaceBuild(t, client, workspaces[0], database.WorkspaceTransitionStop)
+ build = coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
+
+ // Quota goes down one
+ verifyQuota(ctx, t, client, 3, 4)
+ require.Equal(t, codersdk.WorkspaceStatusStopped, build.Status)
+
+ build = coderdtest.CreateWorkspaceBuild(t, client, workspaces[0], database.WorkspaceTransitionStart)
+ build = coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
+
+ // Quota goes back up
+ verifyQuota(ctx, t, client, 4, 4)
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
})
}
+
+func planWithCost(cost int32) []*proto.Response {
+ return []*proto.Response{{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Resources: []*proto.Resource{{
+ Name: "example",
+ Type: "aws_instance",
+ DailyCost: cost,
+ }},
+ },
+ },
+ }}
+}
+
+func applyWithCost(cost int32) []*proto.Response {
+ return []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
+ Resources: []*proto.Resource{{
+ Name: "example",
+ Type: "aws_instance",
+ DailyCost: cost,
+ }},
+ },
+ },
+ }}
+}
diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go
index 07e501032f9e0..db14ae96f1100 100644
--- a/enterprise/coderd/workspaces_test.go
+++ b/enterprise/coderd/workspaces_test.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
+ "sync/atomic"
"testing"
"time"
@@ -11,19 +12,29 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/autobuild"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/database"
- agplschedule "github.com/coder/coder/coderd/schedule"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/enterprise/coderd/schedule"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/autobuild"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/database"
+ agplschedule "github.com/coder/coder/v2/coderd/schedule"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/enterprise/coderd/schedule"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
+// agplUserQuietHoursScheduleStore is passed to
+// NewEnterpriseTemplateScheduleStore as we don't care about updating the
+// schedule and having it recalculate the build deadline in these tests.
+func agplUserQuietHoursScheduleStore() *atomic.Pointer[agplschedule.UserQuietHoursScheduleStore] {
+ store := agplschedule.NewAGPLUserQuietHoursScheduleStore()
+ p := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
+ p.Store(&store)
+ return p
+}
+
func TestCreateWorkspace(t *testing.T) {
t.Parallel()
@@ -100,7 +111,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: ticker,
IncludeProvisionerDaemon: true,
AutobuildStats: statCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -109,8 +120,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionFailed,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyFailed,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
@@ -147,7 +158,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: ticker,
IncludeProvisionerDaemon: true,
AutobuildStats: statCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -155,8 +166,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionFailed,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyFailed,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
@@ -193,7 +204,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: ticker,
IncludeProvisionerDaemon: true,
AutobuildStats: statCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -201,14 +212,14 @@ func TestWorkspaceAutobuild(t *testing.T) {
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
})
// Create a template without setting a failure_ttl.
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
- require.Zero(t, template.InactivityTTLMillis)
+ require.Zero(t, template.TimeTilDormantMillis)
require.Zero(t, template.FailureTTLMillis)
- require.Zero(t, template.LockedTTLMillis)
+ require.Zero(t, template.TimeTilDormantAutoDeleteMillis)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
@@ -235,7 +246,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: ticker,
IncludeProvisionerDaemon: true,
AutobuildStats: statCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -244,11 +255,11 @@ func TestWorkspaceAutobuild(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
- ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
+ ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
@@ -264,12 +275,12 @@ func TestWorkspaceAutobuild(t *testing.T) {
require.Len(t, stats.Transitions, 1)
require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionStop)
- // The workspace should be locked.
+ // The workspace should be dormant.
ws = coderdtest.MustWorkspace(t, client, ws.ID)
- require.NotNil(t, ws.LockedAt)
+ require.NotNil(t, ws.DormantAt)
lastUsedAt := ws.LastUsedAt
- err := client.UpdateWorkspaceLock(ctx, ws.ID, codersdk.UpdateWorkspaceLock{Lock: false})
+ err := client.UpdateWorkspaceDormancy(ctx, ws.ID, codersdk.UpdateWorkspaceDormancy{Dormant: false})
require.NoError(t, err)
// Assert that we updated our last_used_at so that we don't immediately
@@ -292,7 +303,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: ticker,
IncludeProvisionerDaemon: true,
AutobuildStats: statCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -300,11 +311,11 @@ func TestWorkspaceAutobuild(t *testing.T) {
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
- ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
+ ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
@@ -320,13 +331,13 @@ func TestWorkspaceAutobuild(t *testing.T) {
// This is kind of a dumb test but it exists to offer some marginal
// confidence that a bug in the auto-deletion logic doesn't delete running
// workspaces.
- t.Run("UnlockedWorkspacesNotDeleted", func(t *testing.T) {
+ t.Run("ActiveWorkspacesNotDeleted", func(t *testing.T) {
t.Parallel()
var (
- ticker = make(chan time.Time)
- statCh = make(chan autobuild.Stats)
- lockedTTL = time.Minute
+ ticker = make(chan time.Time)
+ statCh = make(chan autobuild.Stats)
+ autoDeleteTTL = time.Minute
)
client, user := coderdenttest.New(t, &coderdenttest.Options{
@@ -334,7 +345,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: ticker,
IncludeProvisionerDaemon: true,
AutobuildStats: statCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -342,20 +353,20 @@ func TestWorkspaceAutobuild(t *testing.T) {
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
- ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds())
+ ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](autoDeleteTTL.Milliseconds())
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
build := coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
- require.Nil(t, ws.LockedAt)
+ require.Nil(t, ws.DormantAt)
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
- ticker <- ws.LastUsedAt.Add(lockedTTL * 2)
+ ticker <- ws.LastUsedAt.Add(autoDeleteTTL * 2)
stats := <-statCh
- // Expect no transitions since workspace is unlocked.
+ // Expect no transitions since workspace is active.
require.Len(t, stats.Transitions, 0)
})
@@ -376,7 +387,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: ticker,
IncludeProvisionerDaemon: true,
AutobuildStats: statCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -384,11 +395,11 @@ func TestWorkspaceAutobuild(t *testing.T) {
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
- ctr.InactivityTTLMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
+ ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
@@ -406,13 +417,13 @@ func TestWorkspaceAutobuild(t *testing.T) {
// Expect no transitions since workspace is stopped.
require.Len(t, stats.Transitions, 0)
ws = coderdtest.MustWorkspace(t, client, ws.ID)
- // The workspace should still be locked even though we didn't
+ // The workspace should still be dormant even though we didn't
// transition the workspace.
- require.NotNil(t, ws.LockedAt)
+ require.NotNil(t, ws.DormantAt)
})
// Test the flow of a workspace transitioning from
- // inactive -> locked -> deleted.
+ // inactive -> dormant -> deleted.
t.Run("WorkspaceInactiveDeleteTransition", func(t *testing.T) {
t.Parallel()
@@ -427,7 +438,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: ticker,
IncludeProvisionerDaemon: true,
AutobuildStats: statCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -436,12 +447,12 @@ func TestWorkspaceAutobuild(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
- ctr.InactivityTTLMillis = ptr.Ref[int64](transitionTTL.Milliseconds())
- ctr.LockedTTLMillis = ptr.Ref[int64](transitionTTL.Milliseconds())
+ ctr.TimeTilDormantMillis = ptr.Ref[int64](transitionTTL.Milliseconds())
+ ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](transitionTTL.Milliseconds())
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
@@ -458,14 +469,14 @@ func TestWorkspaceAutobuild(t *testing.T) {
require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionStop)
ws = coderdtest.MustWorkspace(t, client, ws.ID)
- // The workspace should be locked.
- require.NotNil(t, ws.LockedAt)
+ // The workspace should be dormant.
+ require.NotNil(t, ws.DormantAt)
// Wait for the autobuilder to stop the workspace.
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
- // Simulate the workspace being locked beyond the threshold.
- ticker <- ws.LockedAt.Add(2 * transitionTTL)
+ // Simulate the workspace being dormant beyond the threshold.
+ ticker <- ws.DormantAt.Add(2 * transitionTTL)
stats = <-statCh
require.Len(t, stats.Transitions, 1)
// The workspace should be scheduled for deletion.
@@ -483,13 +494,13 @@ func TestWorkspaceAutobuild(t *testing.T) {
require.Equal(t, http.StatusGone, cerr.StatusCode())
})
- t.Run("LockedTTTooEarly", func(t *testing.T) {
+ t.Run("DormantTTLTooEarly", func(t *testing.T) {
t.Parallel()
var (
- ticker = make(chan time.Time)
- statCh = make(chan autobuild.Stats)
- lockedTTL = time.Minute
+ ticker = make(chan time.Time)
+ statCh = make(chan autobuild.Stats)
+ dormantTTL = time.Minute
)
client, user := coderdenttest.New(t, &coderdenttest.Options{
@@ -497,7 +508,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: ticker,
IncludeProvisionerDaemon: true,
AutobuildStats: statCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -505,11 +516,11 @@ func TestWorkspaceAutobuild(t *testing.T) {
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
- ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds())
+ ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](dormantTTL.Milliseconds())
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
@@ -517,34 +528,34 @@ func TestWorkspaceAutobuild(t *testing.T) {
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
ctx := testutil.Context(t, testutil.WaitMedium)
- err := client.UpdateWorkspaceLock(ctx, ws.ID, codersdk.UpdateWorkspaceLock{
- Lock: true,
+ err := client.UpdateWorkspaceDormancy(ctx, ws.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: true,
})
require.NoError(t, err)
ws = coderdtest.MustWorkspace(t, client, ws.ID)
- require.NotNil(t, ws.LockedAt)
+ require.NotNil(t, ws.DormantAt)
// Ensure we haven't breached our threshold.
- ticker <- ws.LockedAt.Add(-lockedTTL * 2)
+ ticker <- ws.DormantAt.Add(-dormantTTL * 2)
stats := <-statCh
// Expect no transitions since not enough time has elapsed.
require.Len(t, stats.Transitions, 0)
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
- LockedTTLMillis: lockedTTL.Milliseconds(),
+ TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
})
require.NoError(t, err)
// Simlute the workspace breaching the threshold.
- ticker <- ws.LockedAt.Add(lockedTTL * 2)
+ ticker <- ws.DormantAt.Add(dormantTTL * 2)
stats = <-statCh
require.Len(t, stats.Transitions, 1)
require.Equal(t, database.WorkspaceTransitionDelete, stats.Transitions[ws.ID])
})
- // Assert that a locked workspace does not autostart.
- t.Run("LockedNoAutostart", func(t *testing.T) {
+ // Assert that a dormant workspace does not autostart.
+ t.Run("DormantNoAutostart", func(t *testing.T) {
t.Parallel()
var (
@@ -558,7 +569,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
AutobuildTicker: tickCh,
IncludeProvisionerDaemon: true,
AutobuildStats: statsCh,
- TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(),
+ TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
@@ -567,8 +578,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: echo.ProvisionComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: echo.ApplyComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
@@ -583,7 +594,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
- // Assert that autostart works when the workspace isn't locked..
+ // Assert that autostart works when the workspace isn't dormant..
tickCh <- sched.Next(ws.LatestBuild.CreatedAt)
stats := <-statsCh
require.NoError(t, stats.Error)
@@ -595,9 +606,9 @@ func TestWorkspaceAutobuild(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
// Now that we've validated that the workspace is eligible for autostart
- // lets cause it to become locked.
+ // lets cause it to become dormant.
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
- InactivityTTLMillis: inactiveTTL.Milliseconds(),
+ TimeTilDormantMillis: inactiveTTL.Milliseconds(),
})
require.NoError(t, err)
@@ -609,12 +620,12 @@ func TestWorkspaceAutobuild(t *testing.T) {
require.Contains(t, stats.Transitions, ws.ID)
require.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[ws.ID])
- // The workspace should be locked now.
+ // The workspace should be dormant now.
ws = coderdtest.MustWorkspace(t, client, ws.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
- require.NotNil(t, ws.LockedAt)
+ require.NotNil(t, ws.DormantAt)
- // Assert that autostart is no longer triggered since workspace is locked.
+ // Assert that autostart is no longer triggered since workspace is dormant.
tickCh <- sched.Next(ws.LatestBuild.CreatedAt)
stats = <-statsCh
require.Len(t, stats.Transitions, 0)
@@ -627,7 +638,7 @@ func TestWorkspacesFiltering(t *testing.T) {
t.Run("DeletingBy", func(t *testing.T) {
t.Parallel()
- lockedTTL := 24 * time.Hour
+ dormantTTL := 24 * time.Hour
client, user := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
@@ -649,10 +660,10 @@ func TestWorkspacesFiltering(t *testing.T) {
defer cancel()
template, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
- LockedTTLMillis: lockedTTL.Milliseconds(),
+ TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
})
require.NoError(t, err)
- require.Equal(t, lockedTTL.Milliseconds(), template.LockedTTLMillis)
+ require.Equal(t, dormantTTL.Milliseconds(), template.TimeTilDormantAutoDeleteMillis)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
@@ -660,8 +671,8 @@ func TestWorkspacesFiltering(t *testing.T) {
// stop build so workspace is inactive
stopBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
coderdtest.AwaitWorkspaceBuildJob(t, client, stopBuild.ID)
- err = client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
- Lock: true,
+ err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: true,
})
require.NoError(t, err)
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
@@ -669,7 +680,7 @@ func TestWorkspacesFiltering(t *testing.T) {
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
// adding a second to time.Now() to give some buffer in case test runs quickly
- FilterQuery: fmt.Sprintf("deleting_by:%s", time.Now().Add(time.Second).Add(lockedTTL).Format("2006-01-02")),
+ FilterQuery: fmt.Sprintf("deleting_by:%s", time.Now().Add(time.Second).Add(dormantTTL).Format("2006-01-02")),
})
require.NoError(t, err)
require.Len(t, res.Workspaces, 1)
@@ -677,10 +688,65 @@ func TestWorkspacesFiltering(t *testing.T) {
})
}
+// TestWorkspacesWithoutTemplatePerms creates a workspace for a user, then drops
+// the user's perms to the underlying template.
+func TestWorkspacesWithoutTemplatePerms(t *testing.T) {
+ t.Parallel()
+
+ client, first := coderdenttest.New(t, &coderdenttest.Options{
+ Options: &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ },
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureTemplateRBAC: 1,
+ },
+ },
+ })
+
+ version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
+
+ user, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
+ workspace := coderdtest.CreateWorkspace(t, user, first.OrganizationID, template.ID)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ // Remove everyone access
+ err := client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
+ GroupPerms: map[string]codersdk.TemplateRole{
+ first.OrganizationID.String(): codersdk.TemplateRoleDeleted,
+ },
+ })
+ require.NoError(t, err, "remove everyone access")
+
+ // This should fail as the user cannot read the template
+ _, err = user.Workspace(ctx, workspace.ID)
+ require.Error(t, err, "fetch workspace")
+ var sdkError *codersdk.Error
+ require.ErrorAs(t, err, &sdkError)
+ require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
+
+ _, err = user.Workspaces(ctx, codersdk.WorkspaceFilter{})
+ require.NoError(t, err, "fetch workspaces should not fail")
+
+ // Now create another workspace the user can read.
+ version2 := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
+ coderdtest.AwaitTemplateVersionJob(t, client, version2.ID)
+ template2 := coderdtest.CreateTemplate(t, client, first.OrganizationID, version2.ID)
+ _ = coderdtest.CreateWorkspace(t, user, first.OrganizationID, template2.ID)
+
+ workspaces, err := user.Workspaces(ctx, codersdk.WorkspaceFilter{})
+ require.NoError(t, err, "fetch workspaces should not fail")
+ require.Len(t, workspaces.Workspaces, 1)
+}
+
func TestWorkspaceLock(t *testing.T) {
t.Parallel()
- t.Run("TemplateLockedTTL", func(t *testing.T) {
+ t.Run("TemplateTimeTilDormantAutoDelete", func(t *testing.T) {
t.Parallel()
var (
client, user = coderdenttest.New(t, &coderdenttest.Options{
@@ -695,13 +761,13 @@ func TestWorkspaceLock(t *testing.T) {
},
})
- version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
- _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
- lockedTTL = time.Minute
+ version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ dormantTTL = time.Minute
)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
- ctr.LockedTTLMillis = ptr.Ref[int64](lockedTTL.Milliseconds())
+ ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](dormantTTL.Milliseconds())
})
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
@@ -711,30 +777,30 @@ func TestWorkspaceLock(t *testing.T) {
defer cancel()
lastUsedAt := workspace.LastUsedAt
- err := client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
- Lock: true,
+ err := client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: true,
})
require.NoError(t, err)
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
require.NoError(t, err, "fetch provisioned workspace")
require.NotNil(t, workspace.DeletingAt)
- require.NotNil(t, workspace.LockedAt)
- require.Equal(t, workspace.LockedAt.Add(lockedTTL), *workspace.DeletingAt)
- require.WithinRange(t, *workspace.LockedAt, time.Now().Add(-time.Second*10), time.Now())
+ require.NotNil(t, workspace.DormantAt)
+ require.Equal(t, workspace.DormantAt.Add(dormantTTL), *workspace.DeletingAt)
+ require.WithinRange(t, *workspace.DormantAt, time.Now().Add(-time.Second*10), time.Now())
// Locking a workspace shouldn't update the last_used_at.
require.Equal(t, lastUsedAt, workspace.LastUsedAt)
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
lastUsedAt = workspace.LastUsedAt
- err = client.UpdateWorkspaceLock(ctx, workspace.ID, codersdk.UpdateWorkspaceLock{
- Lock: false,
+ err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
+ Dormant: false,
})
require.NoError(t, err)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch provisioned workspace")
- require.Nil(t, workspace.LockedAt)
+ require.Nil(t, workspace.DormantAt)
// Unlocking a workspace should cause the deleting_at to be unset.
require.Nil(t, workspace.DeletingAt)
// The last_used_at should get updated when we unlock the workspace.
diff --git a/enterprise/derpmesh/derpmesh.go b/enterprise/derpmesh/derpmesh.go
index c3f322e9707e9..d5d7b17e09b94 100644
--- a/enterprise/derpmesh/derpmesh.go
+++ b/enterprise/derpmesh/derpmesh.go
@@ -12,7 +12,7 @@ import (
"tailscale.com/derp/derphttp"
"tailscale.com/types/key"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/tailnet"
"cdr.dev/slog"
)
diff --git a/enterprise/derpmesh/derpmesh_test.go b/enterprise/derpmesh/derpmesh_test.go
index a4a7373c9aab0..9f24ba7b1c971 100644
--- a/enterprise/derpmesh/derpmesh_test.go
+++ b/enterprise/derpmesh/derpmesh_test.go
@@ -19,9 +19,9 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/enterprise/derpmesh"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/enterprise/derpmesh"
+ "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
diff --git a/enterprise/replicasync/replicasync.go b/enterprise/replicasync/replicasync.go
index 42bf402a6682e..d756d991aa00a 100644
--- a/enterprise/replicasync/replicasync.go
+++ b/enterprise/replicasync/replicasync.go
@@ -17,10 +17,10 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/pubsub"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
)
var PubsubEvent = "replica"
@@ -375,7 +375,13 @@ func (m *Manager) InRegion(regionID int32) []database.Replica {
// Regional returns all replicas in the same region excluding itself.
func (m *Manager) Regional() []database.Replica {
- return m.InRegion(m.self.RegionID)
+ return m.InRegion(m.regionID())
+}
+
+func (m *Manager) regionID() int32 {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+ return m.self.RegionID
}
// SetCallback sets a function to execute whenever new peers
diff --git a/enterprise/replicasync/replicasync_test.go b/enterprise/replicasync/replicasync_test.go
index 1f33075fd44b3..7c4b26e385291 100644
--- a/enterprise/replicasync/replicasync_test.go
+++ b/enterprise/replicasync/replicasync_test.go
@@ -15,12 +15,12 @@ import (
"go.uber.org/goleak"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/enterprise/replicasync"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/enterprise/replicasync"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
diff --git a/enterprise/tailnet/coordinator.go b/enterprise/tailnet/coordinator.go
index fd0d05149ccf7..d97bf2cce7a6c 100644
--- a/enterprise/tailnet/coordinator.go
+++ b/enterprise/tailnet/coordinator.go
@@ -15,9 +15,9 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/codersdk"
- agpl "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/codersdk"
+ agpl "github.com/coder/coder/v2/tailnet"
)
// NewCoordinator creates a new high availability coordinator
diff --git a/enterprise/tailnet/coordinator_test.go b/enterprise/tailnet/coordinator_test.go
index a29bf2ad273a9..367b07c586faa 100644
--- a/enterprise/tailnet/coordinator_test.go
+++ b/enterprise/tailnet/coordinator_test.go
@@ -10,11 +10,11 @@ import (
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/enterprise/tailnet"
- agpl "github.com/coder/coder/tailnet"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/enterprise/tailnet"
+ agpl "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/testutil"
)
func TestCoordinatorSingle(t *testing.T) {
diff --git a/enterprise/tailnet/pgcoord.go b/enterprise/tailnet/pgcoord.go
index 8693d8e9a5bdd..5b91361ae2c18 100644
--- a/enterprise/tailnet/pgcoord.go
+++ b/enterprise/tailnet/pgcoord.go
@@ -18,11 +18,12 @@ import (
"nhooyr.io/websocket"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/database/pubsub"
- "github.com/coder/coder/coderd/rbac"
- agpl "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/coderd/rbac"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ agpl "github.com/coder/coder/v2/tailnet"
)
const (
@@ -402,11 +403,15 @@ func (b *binder) writeOne(bnd binding) error {
CoordinatorID: b.coordinatorID,
Node: nodeRaw,
})
+ b.logger.Debug(b.ctx, "upserted agent binding",
+ slog.F("agent_id", bnd.agent), slog.F("node", nodeRaw), slog.Error(err))
case bnd.isAgent() && len(nodeRaw) == 0:
_, err = b.store.DeleteTailnetAgent(b.ctx, database.DeleteTailnetAgentParams{
ID: bnd.agent,
CoordinatorID: b.coordinatorID,
})
+ b.logger.Debug(b.ctx, "deleted agent binding",
+ slog.F("agent_id", bnd.agent), slog.Error(err))
if xerrors.Is(err, sql.ErrNoRows) {
// treat deletes as idempotent
err = nil
@@ -418,11 +423,16 @@ func (b *binder) writeOne(bnd binding) error {
AgentID: bnd.agent,
Node: nodeRaw,
})
+ b.logger.Debug(b.ctx, "upserted client binding",
+ slog.F("agent_id", bnd.agent), slog.F("client_id", bnd.client),
+ slog.F("node", nodeRaw), slog.Error(err))
case bnd.isClient() && len(nodeRaw) == 0:
_, err = b.store.DeleteTailnetClient(b.ctx, database.DeleteTailnetClientParams{
ID: bnd.client,
CoordinatorID: b.coordinatorID,
})
+ b.logger.Debug(b.ctx, "deleted client binding",
+ slog.F("agent_id", bnd.agent), slog.F("client_id", bnd.client), slog.Error(err))
if xerrors.Is(err, sql.ErrNoRows) {
// treat deletes as idempotent
err = nil
@@ -585,10 +595,12 @@ type querier struct {
workQ *workQ[mKey]
heartbeats *heartbeats
- updates <-chan struct{}
+ updates <-chan hbUpdate
mu sync.Mutex
mappers map[mKey]*countedMapper
+ conns map[*connIO]struct{}
+ healthy bool
}
type countedMapper struct {
@@ -603,7 +615,7 @@ func newQuerier(
self uuid.UUID, newConnections chan *connIO, numWorkers int,
firstHeartbeat chan<- struct{},
) *querier {
- updates := make(chan struct{})
+ updates := make(chan hbUpdate)
q := &querier{
ctx: ctx,
logger: logger.Named("querier"),
@@ -613,9 +625,11 @@ func newQuerier(
workQ: newWorkQ[mKey](ctx),
heartbeats: newHeartbeats(ctx, logger, ps, store, self, updates, firstHeartbeat),
mappers: make(map[mKey]*countedMapper),
+ conns: make(map[*connIO]struct{}),
updates: updates,
+ healthy: true, // assume we start healthy
}
- go q.subscribe()
+ q.subscribe()
go q.handleConnIO()
for i := 0; i < numWorkers; i++ {
go q.worker()
@@ -638,6 +652,15 @@ func (q *querier) handleConnIO() {
func (q *querier) newConn(c *connIO) {
q.mu.Lock()
defer q.mu.Unlock()
+ if !q.healthy {
+ err := c.updates.Close()
+ q.logger.Info(q.ctx, "closed incoming connection while unhealthy",
+ slog.Error(err),
+ slog.F("agent_id", c.agent),
+ slog.F("client_id", c.client),
+ )
+ return
+ }
mk := mKey{
agent: c.agent,
// if client is Nil, this is an agent connection, and it wants the mappings for all the clients of itself
@@ -660,6 +683,7 @@ func (q *querier) newConn(c *connIO) {
return
}
cm.count++
+ q.conns[c] = struct{}{}
go q.cleanupConn(c)
}
@@ -667,6 +691,7 @@ func (q *querier) cleanupConn(c *connIO) {
<-c.ctx.Done()
q.mu.Lock()
defer q.mu.Unlock()
+ delete(q.conns, c)
mk := mKey{
agent: c.agent,
// if client is Nil, this is an agent connection, and it wants the mappings for all the clients of itself
@@ -732,6 +757,8 @@ func (q *querier) query(mk mKey) error {
func (q *querier) queryClientsOfAgent(agent uuid.UUID) ([]mapping, error) {
clients, err := q.store.GetTailnetClientsForAgent(q.ctx, agent)
+ q.logger.Debug(q.ctx, "queried clients of agent",
+ slog.F("agent_id", agent), slog.F("num_clients", len(clients)), slog.Error(err))
if err != nil {
return nil, err
}
@@ -756,6 +783,8 @@ func (q *querier) queryClientsOfAgent(agent uuid.UUID) ([]mapping, error) {
func (q *querier) queryAgent(agentID uuid.UUID) ([]mapping, error) {
agents, err := q.store.GetTailnetAgents(q.ctx, agentID)
+ q.logger.Debug(q.ctx, "queried agents",
+ slog.F("agent_id", agentID), slog.F("num_agents", len(agents)), slog.Error(err))
if err != nil {
return nil, err
}
@@ -777,50 +806,62 @@ func (q *querier) queryAgent(agentID uuid.UUID) ([]mapping, error) {
return mappings, nil
}
+// subscribe starts our subscriptions to client and agent updates in a new goroutine, and returns once we are subscribed
+// or the querier context is canceled.
func (q *querier) subscribe() {
- eb := backoff.NewExponentialBackOff()
- eb.MaxElapsedTime = 0 // retry indefinitely
- eb.MaxInterval = dbMaxBackoff
- bkoff := backoff.WithContext(eb, q.ctx)
- var cancelClient context.CancelFunc
- err := backoff.Retry(func() error {
- cancelFn, err := q.pubsub.SubscribeWithErr(eventClientUpdate, q.listenClient)
+ subscribed := make(chan struct{})
+ go func() {
+ defer close(subscribed)
+ eb := backoff.NewExponentialBackOff()
+ eb.MaxElapsedTime = 0 // retry indefinitely
+ eb.MaxInterval = dbMaxBackoff
+ bkoff := backoff.WithContext(eb, q.ctx)
+ var cancelClient context.CancelFunc
+ err := backoff.Retry(func() error {
+ cancelFn, err := q.pubsub.SubscribeWithErr(eventClientUpdate, q.listenClient)
+ if err != nil {
+ q.logger.Warn(q.ctx, "failed to subscribe to client updates", slog.Error(err))
+ return err
+ }
+ cancelClient = cancelFn
+ return nil
+ }, bkoff)
if err != nil {
- q.logger.Warn(q.ctx, "failed to subscribe to client updates", slog.Error(err))
- return err
- }
- cancelClient = cancelFn
- return nil
- }, bkoff)
- if err != nil {
- if q.ctx.Err() == nil {
- q.logger.Error(q.ctx, "code bug: retry failed before context canceled", slog.Error(err))
+ if q.ctx.Err() == nil {
+ q.logger.Error(q.ctx, "code bug: retry failed before context canceled", slog.Error(err))
+ }
+ return
}
- return
- }
- defer cancelClient()
- bkoff.Reset()
+ defer cancelClient()
+ bkoff.Reset()
+ q.logger.Debug(q.ctx, "subscribed to client updates")
- var cancelAgent context.CancelFunc
- err = backoff.Retry(func() error {
- cancelFn, err := q.pubsub.SubscribeWithErr(eventAgentUpdate, q.listenAgent)
+ var cancelAgent context.CancelFunc
+ err = backoff.Retry(func() error {
+ cancelFn, err := q.pubsub.SubscribeWithErr(eventAgentUpdate, q.listenAgent)
+ if err != nil {
+ q.logger.Warn(q.ctx, "failed to subscribe to agent updates", slog.Error(err))
+ return err
+ }
+ cancelAgent = cancelFn
+ return nil
+ }, bkoff)
if err != nil {
- q.logger.Warn(q.ctx, "failed to subscribe to agent updates", slog.Error(err))
- return err
- }
- cancelAgent = cancelFn
- return nil
- }, bkoff)
- if err != nil {
- if q.ctx.Err() == nil {
- q.logger.Error(q.ctx, "code bug: retry failed before context canceled", slog.Error(err))
+ if q.ctx.Err() == nil {
+ q.logger.Error(q.ctx, "code bug: retry failed before context canceled", slog.Error(err))
+ }
+ return
}
- return
- }
- defer cancelAgent()
+ defer cancelAgent()
+ q.logger.Debug(q.ctx, "subscribed to agent updates")
- // hold subscriptions open until context is canceled
- <-q.ctx.Done()
+ // unblock the outer function from returning
+ subscribed <- struct{}{}
+
+ // hold subscriptions open until context is canceled
+ <-q.ctx.Done()
+ }()
+ <-subscribed
}
func (q *querier) listenClient(_ context.Context, msg []byte, err error) {
@@ -910,8 +951,18 @@ func (q *querier) handleUpdates() {
select {
case <-q.ctx.Done():
return
- case <-q.updates:
- q.updateAll()
+ case u := <-q.updates:
+ if u.filter == filterUpdateUpdated {
+ q.updateAll()
+ }
+ if u.health == healthUpdateUnhealthy {
+ q.unhealthyCloseAll()
+ continue
+ }
+ if u.health == healthUpdateHealthy {
+ q.setHealthy()
+ continue
+ }
}
}
}
@@ -931,6 +982,30 @@ func (q *querier) updateAll() {
}
}
+// unhealthyCloseAll marks the coordinator unhealthy and closes all connections. We do this so that clients and agents
+// are forced to reconnect to the coordinator, and will hopefully land on a healthy coordinator.
+func (q *querier) unhealthyCloseAll() {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+ q.healthy = false
+ for c := range q.conns {
+ // close connections async so that we don't block the querier routine that responds to updates
+ go func(c *connIO) {
+ err := c.updates.Close()
+ if err != nil {
+ q.logger.Debug(q.ctx, "error closing conn while unhealthy", slog.Error(err))
+ }
+ }(c)
+ // NOTE: we don't need to remove the connection from the map, as that will happen async in q.cleanupConn()
+ }
+}
+
+func (q *querier) setHealthy() {
+ q.mu.Lock()
+ defer q.mu.Unlock()
+ q.healthy = true
+}
+
func (q *querier) getAll(ctx context.Context) (map[uuid.UUID]database.TailnetAgent, map[uuid.UUID][]database.TailnetClient, error) {
agents, err := q.store.GetAllTailnetAgents(ctx)
if err != nil {
@@ -1077,6 +1152,28 @@ func (q *workQ[K]) done(key K) {
q.cond.Signal()
}
+type filterUpdate int
+
+const (
+ filterUpdateNone filterUpdate = iota
+ filterUpdateUpdated
+)
+
+type healthUpdate int
+
+const (
+ healthUpdateNone healthUpdate = iota
+ healthUpdateHealthy
+ healthUpdateUnhealthy
+)
+
+// hbUpdate is an update sent from the heartbeats to the querier. Zero values of the fields mean no update of that
+// kind.
+type hbUpdate struct {
+ filter filterUpdate
+ health healthUpdate
+}
+
// heartbeats sends heartbeats for this coordinator on a timer, and monitors heartbeats from other coordinators. If a
// coordinator misses their heartbeat, we remove it from our map of "valid" coordinators, such that we will filter out
// any mappings for it when filter() is called, and we send a signal on the update channel, which triggers all mappers
@@ -1088,8 +1185,9 @@ type heartbeats struct {
store database.Store
self uuid.UUID
- update chan<- struct{}
- firstHeartbeat chan<- struct{}
+ update chan<- hbUpdate
+ firstHeartbeat chan<- struct{}
+ failedHeartbeats int
lock sync.RWMutex
coordinators map[uuid.UUID]time.Time
@@ -1102,7 +1200,7 @@ type heartbeats struct {
func newHeartbeats(
ctx context.Context, logger slog.Logger,
ps pubsub.Pubsub, store database.Store,
- self uuid.UUID, update chan<- struct{},
+ self uuid.UUID, update chan<- hbUpdate,
firstHeartbeat chan<- struct{},
) *heartbeats {
h := &heartbeats{
@@ -1193,7 +1291,7 @@ func (h *heartbeats) recvBeat(id uuid.UUID) {
h.logger.Info(h.ctx, "heartbeats (re)started", slog.F("other_coordinator_id", id))
// send on a separate goroutine to avoid holding lock. Triggering update can be async
go func() {
- _ = sendCtx(h.ctx, h.update, struct{}{})
+ _ = sendCtx(h.ctx, h.update, hbUpdate{filter: filterUpdateUpdated})
}()
}
h.coordinators[id] = time.Now()
@@ -1240,7 +1338,7 @@ func (h *heartbeats) checkExpiry() {
if expired {
// send on a separate goroutine to avoid holding lock. Triggering update can be async
go func() {
- _ = sendCtx(h.ctx, h.update, struct{}{})
+ _ = sendCtx(h.ctx, h.update, hbUpdate{filter: filterUpdateUpdated})
}()
}
// we need to reset the timer for when the next oldest coordinator will expire, if any.
@@ -1268,11 +1366,20 @@ func (h *heartbeats) sendBeats() {
func (h *heartbeats) sendBeat() {
_, err := h.store.UpsertTailnetCoordinator(h.ctx, h.self)
if err != nil {
- // just log errors, heartbeats are rescheduled on a timer
h.logger.Error(h.ctx, "failed to send heartbeat", slog.Error(err))
+ h.failedHeartbeats++
+ if h.failedHeartbeats == 3 {
+ h.logger.Error(h.ctx, "coordinator failed 3 heartbeats and is unhealthy")
+ _ = sendCtx(h.ctx, h.update, hbUpdate{health: healthUpdateUnhealthy})
+ }
return
}
h.logger.Debug(h.ctx, "sent heartbeat")
+ if h.failedHeartbeats >= 3 {
+ h.logger.Info(h.ctx, "coordinator sent heartbeat and is healthy")
+ _ = sendCtx(h.ctx, h.update, hbUpdate{health: healthUpdateHealthy})
+ }
+ h.failedHeartbeats = 0
}
func (h *heartbeats) sendDelete() {
@@ -1351,8 +1458,8 @@ func (c *pgCoord) htmlDebug(ctx context.Context) (agpl.HTMLDebug, error) {
Node: conn.Node,
})
}
- slices.SortFunc(htmlAgent.Connections, func(a, b *agpl.HTMLClient) bool {
- return a.Name < b.Name
+ slices.SortFunc(htmlAgent.Connections, func(a, b *agpl.HTMLClient) int {
+ return slice.Ascending(a.Name, b.Name)
})
data.Agents = append(data.Agents, htmlAgent)
@@ -1362,8 +1469,8 @@ func (c *pgCoord) htmlDebug(ctx context.Context) (agpl.HTMLDebug, error) {
Node: agent.Node,
})
}
- slices.SortFunc(data.Agents, func(a, b *agpl.HTMLAgent) bool {
- return a.Name < b.Name
+ slices.SortFunc(data.Agents, func(a, b *agpl.HTMLAgent) int {
+ return slice.Ascending(a.Name, b.Name)
})
for agentID, conns := range clients {
@@ -1389,14 +1496,14 @@ func (c *pgCoord) htmlDebug(ctx context.Context) (agpl.HTMLDebug, error) {
Node: conn.Node,
})
}
- slices.SortFunc(agent.Connections, func(a, b *agpl.HTMLClient) bool {
- return a.Name < b.Name
+ slices.SortFunc(agent.Connections, func(a, b *agpl.HTMLClient) int {
+ return slice.Ascending(a.Name, b.Name)
})
data.MissingAgents = append(data.MissingAgents, agent)
}
- slices.SortFunc(data.MissingAgents, func(a, b *agpl.HTMLAgent) bool {
- return a.Name < b.Name
+ slices.SortFunc(data.MissingAgents, func(a, b *agpl.HTMLAgent) int {
+ return slice.Ascending(a.Name, b.Name)
})
return data, nil
diff --git a/enterprise/tailnet/pgcoord_internal_test.go b/enterprise/tailnet/pgcoord_internal_test.go
index 86666f8105582..95481e6af3cc4 100644
--- a/enterprise/tailnet/pgcoord_internal_test.go
+++ b/enterprise/tailnet/pgcoord_internal_test.go
@@ -10,8 +10,8 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database/dbmock"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database/dbmock"
+ "github.com/coder/coder/v2/testutil"
)
// TestHeartbeat_Cleanup is internal so that we can overwrite the cleanup period and not wait an hour for the timed
diff --git a/enterprise/tailnet/pgcoord_test.go b/enterprise/tailnet/pgcoord_test.go
index 25d80ca854566..9112cd95a0791 100644
--- a/enterprise/tailnet/pgcoord_test.go
+++ b/enterprise/tailnet/pgcoord_test.go
@@ -10,6 +10,7 @@ import (
"testing"
"time"
+ "github.com/golang/mock/gomock"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -20,11 +21,13 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/dbtestutil"
- "github.com/coder/coder/enterprise/tailnet"
- agpl "github.com/coder/coder/tailnet"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/dbmock"
+ "github.com/coder/coder/v2/coderd/database/dbtestutil"
+ "github.com/coder/coder/v2/coderd/database/pubsub"
+ "github.com/coder/coder/v2/enterprise/tailnet"
+ agpl "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
@@ -36,11 +39,11 @@ func TestPGCoordinatorSingle_ClientWithoutAgent(t *testing.T) {
if !dbtestutil.WillUsePostgres() {
t.Skip("test only with postgres")
}
- store, pubsub := dbtestutil.NewDB(t)
+ store, ps := dbtestutil.NewDB(t)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
defer cancel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
- coordinator, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coordinator, err := tailnet.NewPGCoord(ctx, logger, ps, store)
require.NoError(t, err)
defer coordinator.Close()
@@ -75,15 +78,15 @@ func TestPGCoordinatorSingle_AgentWithoutClients(t *testing.T) {
if !dbtestutil.WillUsePostgres() {
t.Skip("test only with postgres")
}
- store, pubsub := dbtestutil.NewDB(t)
+ store, ps := dbtestutil.NewDB(t)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
defer cancel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
- coordinator, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coordinator, err := tailnet.NewPGCoord(ctx, logger, ps, store)
require.NoError(t, err)
defer coordinator.Close()
- agent := newTestAgent(t, coordinator)
+ agent := newTestAgent(t, coordinator, "agent")
defer agent.close()
agent.sendNode(&agpl.Node{PreferredDERP: 10})
require.Eventually(t, func() bool {
@@ -112,15 +115,15 @@ func TestPGCoordinatorSingle_AgentWithClient(t *testing.T) {
if !dbtestutil.WillUsePostgres() {
t.Skip("test only with postgres")
}
- store, pubsub := dbtestutil.NewDB(t)
+ store, ps := dbtestutil.NewDB(t)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
defer cancel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
- coordinator, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coordinator, err := tailnet.NewPGCoord(ctx, logger, ps, store)
require.NoError(t, err)
defer coordinator.Close()
- agent := newTestAgent(t, coordinator)
+ agent := newTestAgent(t, coordinator, "original")
defer agent.close()
agent.sendNode(&agpl.Node{PreferredDERP: 10})
@@ -148,7 +151,7 @@ func TestPGCoordinatorSingle_AgentWithClient(t *testing.T) {
agent.waitForClose(ctx, t)
// Create a new agent connection. This is to simulate a reconnect!
- agent = newTestAgent(t, coordinator, agent.id)
+ agent = newTestAgent(t, coordinator, "reconnection", agent.id)
// Ensure the existing listening connIO sends its node immediately!
clientNodes = agent.recvNodes(ctx, t)
require.Len(t, clientNodes, 1)
@@ -189,15 +192,15 @@ func TestPGCoordinatorSingle_MissedHeartbeats(t *testing.T) {
if !dbtestutil.WillUsePostgres() {
t.Skip("test only with postgres")
}
- store, pubsub := dbtestutil.NewDB(t)
+ store, ps := dbtestutil.NewDB(t)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
defer cancel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
- coordinator, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coordinator, err := tailnet.NewPGCoord(ctx, logger, ps, store)
require.NoError(t, err)
defer coordinator.Close()
- agent := newTestAgent(t, coordinator)
+ agent := newTestAgent(t, coordinator, "agent")
defer agent.close()
agent.sendNode(&agpl.Node{PreferredDERP: 10})
@@ -276,14 +279,14 @@ func TestPGCoordinatorSingle_SendsHeartbeats(t *testing.T) {
if !dbtestutil.WillUsePostgres() {
t.Skip("test only with postgres")
}
- store, pubsub := dbtestutil.NewDB(t)
+ store, ps := dbtestutil.NewDB(t)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
defer cancel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
mu := sync.Mutex{}
heartbeats := []time.Time{}
- unsub, err := pubsub.SubscribeWithErr(tailnet.EventHeartbeats, func(_ context.Context, msg []byte, err error) {
+ unsub, err := ps.SubscribeWithErr(tailnet.EventHeartbeats, func(_ context.Context, msg []byte, err error) {
assert.NoError(t, err)
mu.Lock()
defer mu.Unlock()
@@ -293,7 +296,7 @@ func TestPGCoordinatorSingle_SendsHeartbeats(t *testing.T) {
defer unsub()
start := time.Now()
- coordinator, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coordinator, err := tailnet.NewPGCoord(ctx, logger, ps, store)
require.NoError(t, err)
defer coordinator.Close()
@@ -326,20 +329,20 @@ func TestPGCoordinatorDual_Mainline(t *testing.T) {
if !dbtestutil.WillUsePostgres() {
t.Skip("test only with postgres")
}
- store, pubsub := dbtestutil.NewDB(t)
+ store, ps := dbtestutil.NewDB(t)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
defer cancel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
- coord1, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coord1, err := tailnet.NewPGCoord(ctx, logger.Named("coord1"), ps, store)
require.NoError(t, err)
defer coord1.Close()
- coord2, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coord2, err := tailnet.NewPGCoord(ctx, logger.Named("coord2"), ps, store)
require.NoError(t, err)
defer coord2.Close()
- agent1 := newTestAgent(t, coord1)
+ agent1 := newTestAgent(t, coord1, "agent1")
defer agent1.close()
- agent2 := newTestAgent(t, coord2)
+ agent2 := newTestAgent(t, coord2, "agent2")
defer agent2.close()
client11 := newTestClient(t, coord1, agent1.id)
@@ -453,23 +456,23 @@ func TestPGCoordinator_MultiAgent(t *testing.T) {
if !dbtestutil.WillUsePostgres() {
t.Skip("test only with postgres")
}
- store, pubsub := dbtestutil.NewDB(t)
+ store, ps := dbtestutil.NewDB(t)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
defer cancel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
- coord1, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coord1, err := tailnet.NewPGCoord(ctx, logger.Named("coord1"), ps, store)
require.NoError(t, err)
defer coord1.Close()
- coord2, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coord2, err := tailnet.NewPGCoord(ctx, logger.Named("coord2"), ps, store)
require.NoError(t, err)
defer coord2.Close()
- coord3, err := tailnet.NewPGCoord(ctx, logger, pubsub, store)
+ coord3, err := tailnet.NewPGCoord(ctx, logger.Named("coord3"), ps, store)
require.NoError(t, err)
defer coord3.Close()
- agent1 := newTestAgent(t, coord1)
+ agent1 := newTestAgent(t, coord1, "agent1")
defer agent1.close()
- agent2 := newTestAgent(t, coord2, agent1.id)
+ agent2 := newTestAgent(t, coord2, "agent2", agent1.id)
defer agent2.close()
client := newTestClient(t, coord3, agent1.id)
@@ -516,6 +519,76 @@ func TestPGCoordinator_MultiAgent(t *testing.T) {
assertEventuallyNoAgents(ctx, t, store, agent1.id)
}
+func TestPGCoordinator_Unhealthy(t *testing.T) {
+ t.Parallel()
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
+ defer cancel()
+ ctrl := gomock.NewController(t)
+ mStore := dbmock.NewMockStore(ctrl)
+ ps := pubsub.NewInMemory()
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
+
+ calls := make(chan struct{})
+ threeMissed := mStore.EXPECT().UpsertTailnetCoordinator(gomock.Any(), gomock.Any()).
+ Times(3).
+ Do(func(_ context.Context, _ uuid.UUID) { <-calls }).
+ Return(database.TailnetCoordinator{}, xerrors.New("test disconnect"))
+ mStore.EXPECT().UpsertTailnetCoordinator(gomock.Any(), gomock.Any()).
+ MinTimes(1).
+ After(threeMissed).
+ Do(func(_ context.Context, _ uuid.UUID) { <-calls }).
+ Return(database.TailnetCoordinator{}, nil)
+ // extra calls we don't particularly care about for this test
+ mStore.EXPECT().CleanTailnetCoordinators(gomock.Any()).AnyTimes().Return(nil)
+ mStore.EXPECT().GetTailnetClientsForAgent(gomock.Any(), gomock.Any()).AnyTimes().Return(nil, nil)
+ mStore.EXPECT().DeleteTailnetAgent(gomock.Any(), gomock.Any()).
+ AnyTimes().Return(database.DeleteTailnetAgentRow{}, nil)
+ mStore.EXPECT().DeleteCoordinator(gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
+
+ uut, err := tailnet.NewPGCoord(ctx, logger, ps, mStore)
+ require.NoError(t, err)
+ defer func() {
+ err := uut.Close()
+ require.NoError(t, err)
+ }()
+ agent1 := newTestAgent(t, uut, "agent1")
+ defer agent1.close()
+ for i := 0; i < 3; i++ {
+ select {
+ case <-ctx.Done():
+ t.Fatal("timeout")
+ case calls <- struct{}{}:
+ // OK
+ }
+ }
+ // connected agent should be disconnected
+ agent1.waitForClose(ctx, t)
+
+ // new agent should immediately disconnect
+ agent2 := newTestAgent(t, uut, "agent2")
+ defer agent2.close()
+ agent2.waitForClose(ctx, t)
+
+ // next heartbeats succeed, so we are healthy
+ for i := 0; i < 2; i++ {
+ select {
+ case <-ctx.Done():
+ t.Fatal("timeout")
+ case calls <- struct{}{}:
+ // OK
+ }
+ }
+ agent3 := newTestAgent(t, uut, "agent3")
+ defer agent3.close()
+ select {
+ case <-agent3.closeChan:
+ t.Fatal("agent conn closed after we are healthy")
+ case <-time.After(time.Second):
+ // OK
+ }
+}
+
type testConn struct {
ws, serverWS net.Conn
nodeChan chan []*agpl.Node
@@ -545,10 +618,10 @@ func newTestConn(ids []uuid.UUID) *testConn {
return a
}
-func newTestAgent(t *testing.T, coord agpl.Coordinator, id ...uuid.UUID) *testConn {
+func newTestAgent(t *testing.T, coord agpl.Coordinator, name string, id ...uuid.UUID) *testConn {
a := newTestConn(id)
go func() {
- err := coord.ServeAgent(a.serverWS, a.id, "")
+ err := coord.ServeAgent(a.serverWS, a.id, name)
assert.NoError(t, err)
close(a.closeChan)
}()
@@ -563,7 +636,7 @@ func (c *testConn) recvNodes(ctx context.Context, t *testing.T) []*agpl.Node {
t.Helper()
select {
case <-ctx.Done():
- t.Fatal("timeout receiving nodes")
+ t.Fatalf("testConn id %s: timeout receiving nodes ", c.id)
return nil
case nodes := <-c.nodeChan:
return nodes
diff --git a/enterprise/tailnet/workspaceproxy.go b/enterprise/tailnet/workspaceproxy.go
index 13e1f6663a2c0..3150890c13fa9 100644
--- a/enterprise/tailnet/workspaceproxy.go
+++ b/enterprise/tailnet/workspaceproxy.go
@@ -4,13 +4,14 @@ import (
"bytes"
"context"
"encoding/json"
+ "errors"
"net"
"time"
"golang.org/x/xerrors"
- "github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
- agpl "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
+ agpl "github.com/coder/coder/v2/tailnet"
)
func ServeWorkspaceProxy(ctx context.Context, conn net.Conn, ma agpl.MultiAgentConn) error {
@@ -26,6 +27,9 @@ func ServeWorkspaceProxy(ctx context.Context, conn net.Conn, ma agpl.MultiAgentC
var msg wsproxysdk.CoordinateMessage
err := decoder.Decode(&msg)
if err != nil {
+ if errors.Is(err, net.ErrClosed) {
+ return nil
+ }
return xerrors.Errorf("read json: %w", err)
}
diff --git a/enterprise/trialer/trialer.go b/enterprise/trialer/trialer.go
index 69d395e5a3e16..16918797ac098 100644
--- a/enterprise/trialer/trialer.go
+++ b/enterprise/trialer/trialer.go
@@ -13,8 +13,8 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/enterprise/coderd/license"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
)
type request struct {
diff --git a/enterprise/trialer/trialer_test.go b/enterprise/trialer/trialer_test.go
index 1a6ce695b7491..6a160e1ab53ed 100644
--- a/enterprise/trialer/trialer_test.go
+++ b/enterprise/trialer/trialer_test.go
@@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/trialer"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/trialer"
)
func TestTrialer(t *testing.T) {
diff --git a/enterprise/wsproxy/appstatsreporter.go b/enterprise/wsproxy/appstatsreporter.go
new file mode 100644
index 0000000000000..44ffe87e1a5e3
--- /dev/null
+++ b/enterprise/wsproxy/appstatsreporter.go
@@ -0,0 +1,21 @@
+package wsproxy
+
+import (
+ "context"
+
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
+)
+
+var _ workspaceapps.StatsReporter = (*appStatsReporter)(nil)
+
+type appStatsReporter struct {
+ Client *wsproxysdk.Client
+}
+
+func (r *appStatsReporter) Report(ctx context.Context, stats []workspaceapps.StatsReport) error {
+ err := r.Client.ReportAppStats(ctx, wsproxysdk.ReportAppStatsRequest{
+ Stats: stats,
+ })
+ return err
+}
diff --git a/enterprise/wsproxy/tokenprovider.go b/enterprise/wsproxy/tokenprovider.go
index 8efef6d979db3..38822a4e7a22d 100644
--- a/enterprise/wsproxy/tokenprovider.go
+++ b/enterprise/wsproxy/tokenprovider.go
@@ -7,8 +7,8 @@ import (
"cdr.dev/slog"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
)
var _ workspaceapps.SignedTokenProvider = (*TokenProvider)(nil)
diff --git a/enterprise/wsproxy/wsproxy.go b/enterprise/wsproxy/wsproxy.go
index 843b79ee394bf..b0194d69d3f26 100644
--- a/enterprise/wsproxy/wsproxy.go
+++ b/enterprise/wsproxy/wsproxy.go
@@ -11,6 +11,7 @@ import (
"reflect"
"regexp"
"strings"
+ "sync/atomic"
"time"
"github.com/go-chi/chi/v5"
@@ -21,21 +22,22 @@ import (
"golang.org/x/xerrors"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
+ "tailscale.com/tailcfg"
"tailscale.com/types/key"
"cdr.dev/slog"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/coderd/wsconncache"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/enterprise/derpmesh"
- "github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
- "github.com/coder/coder/site"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/coderd/wsconncache"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/derpmesh"
+ "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
+ "github.com/coder/coder/v2/site"
+ "github.com/coder/coder/v2/tailnet"
)
type Options struct {
@@ -79,6 +81,8 @@ type Options struct {
// By default, CORs is set to accept external requests
// from the dashboardURL. This should only be used in development.
AllowAllCors bool
+
+ StatsCollectorOptions workspaceapps.StatsCollectorOptions
}
func (o *Options) Validate() error {
@@ -117,7 +121,8 @@ type Server struct {
SDKClient *wsproxysdk.Client
// DERP
- derpMesh *derpmesh.Mesh
+ derpMesh *derpmesh.Mesh
+ latestDERPMap atomic.Pointer[tailcfg.DERPMap]
// Used for graceful shutdown. Required for the dialer.
ctx context.Context
@@ -237,19 +242,18 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
return nil, xerrors.Errorf("parse app security key: %w", err)
}
- connInfo, err := client.SDKClient.WorkspaceAgentConnectionInfoGeneric(ctx)
- if err != nil {
- return nil, xerrors.Errorf("get derpmap: %w", err)
- }
-
var agentProvider workspaceapps.AgentProvider
if opts.Experiments.Enabled(codersdk.ExperimentSingleTailnet) {
stn, err := coderd.NewServerTailnet(ctx,
s.Logger,
nil,
- connInfo.DERPMap,
+ func() *tailcfg.DERPMap {
+ return s.latestDERPMap.Load()
+ },
+ regResp.DERPForceWebSockets,
s.DialCoordinator,
wsconncache.New(s.DialWorkspaceAgent, 0),
+ s.TracerProvider,
)
if err != nil {
return nil, xerrors.Errorf("create server tailnet: %w", err)
@@ -261,8 +265,17 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
}
}
+ workspaceAppsLogger := opts.Logger.Named("workspaceapps")
+ if opts.StatsCollectorOptions.Logger == nil {
+ named := workspaceAppsLogger.Named("stats_collector")
+ opts.StatsCollectorOptions.Logger = &named
+ }
+ if opts.StatsCollectorOptions.Reporter == nil {
+ opts.StatsCollectorOptions.Reporter = &appStatsReporter{Client: client}
+ }
+
s.AppServer = &workspaceapps.Server{
- Logger: opts.Logger.Named("workspaceapps"),
+ Logger: workspaceAppsLogger,
DashboardURL: opts.DashboardURL,
AccessURL: opts.AccessURL,
Hostname: opts.AppHostname,
@@ -278,9 +291,11 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
},
AppSecurityKey: secKey,
- AgentProvider: agentProvider,
DisablePathApps: opts.DisablePathApps,
SecureAuthCookie: opts.SecureAuthCookie,
+
+ AgentProvider: agentProvider,
+ StatsCollector: workspaceapps.NewStatsCollector(opts.StatsCollectorOptions),
}
derpHandler := derphttp.Handler(derpServer)
@@ -442,6 +457,8 @@ func (s *Server) handleRegister(_ context.Context, res wsproxysdk.RegisterWorksp
}
s.derpMesh.SetAddresses(addresses, false)
+ s.latestDERPMap.Store(res.DERPMap)
+
return nil
}
diff --git a/enterprise/wsproxy/wsproxy_test.go b/enterprise/wsproxy/wsproxy_test.go
index afcb3d1f16143..57132f1a80b12 100644
--- a/enterprise/wsproxy/wsproxy_test.go
+++ b/enterprise/wsproxy/wsproxy_test.go
@@ -15,20 +15,19 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/coderd"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/healthcheck"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/workspaceapps/apptest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/enterprise/coderd/coderdenttest"
- "github.com/coder/coder/enterprise/coderd/license"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/tailnet"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/coderd"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/healthcheck"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/workspaceapps/apptest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/testutil"
)
func TestDERPOnly(t *testing.T) {
@@ -203,10 +202,10 @@ resourceLoop:
connInfo, err := client.WorkspaceAgentConnectionInfo(ctx, agentID)
require.NoError(t, err)
- // There should be three DERP servers in the map: the primary, and each
- // of the two running proxies.
+ // There should be three DERP regions in the map: the primary, and each
+ // of the two running proxies. Also the STUN-only regions.
require.NotNil(t, connInfo.DERPMap)
- require.Len(t, connInfo.DERPMap.Regions, 3)
+ require.Len(t, connInfo.DERPMap.Regions, 3+len(api.DeploymentValues.DERP.Server.STUNAddresses.Value()))
var (
primaryRegion *tailcfg.DERPRegion
@@ -230,6 +229,11 @@ resourceLoop:
// The last region is never started, which means it's never healthy,
// which means it's never added to the DERP map.
+ if len(r.Nodes) == 1 && r.Nodes[0].STUNOnly {
+ // Skip STUN-only regions.
+ continue
+ }
+
t.Fatalf("unexpected region: %+v", r)
}
@@ -244,24 +248,24 @@ resourceLoop:
require.Equal(t, "coder_best-proxy", proxy1Region.RegionCode)
require.Equal(t, 10001, proxy1Region.RegionID)
require.False(t, proxy1Region.EmbeddedRelay)
- require.Len(t, proxy1Region.Nodes, 2) // proxy + stun
- require.Equal(t, "10001a", proxy1Region.Nodes[1].Name)
- require.Equal(t, 10001, proxy1Region.Nodes[1].RegionID)
- require.Equal(t, proxyAPI1.Options.AccessURL.Hostname(), proxy1Region.Nodes[1].HostName)
- require.Equal(t, proxyAPI1.Options.AccessURL.Port(), fmt.Sprint(proxy1Region.Nodes[1].DERPPort))
- require.Equal(t, proxyAPI1.Options.AccessURL.Scheme == "http", proxy1Region.Nodes[1].ForceHTTP)
+ require.Len(t, proxy1Region.Nodes, 1)
+ require.Equal(t, "10001a", proxy1Region.Nodes[0].Name)
+ require.Equal(t, 10001, proxy1Region.Nodes[0].RegionID)
+ require.Equal(t, proxyAPI1.Options.AccessURL.Hostname(), proxy1Region.Nodes[0].HostName)
+ require.Equal(t, proxyAPI1.Options.AccessURL.Port(), fmt.Sprint(proxy1Region.Nodes[0].DERPPort))
+ require.Equal(t, proxyAPI1.Options.AccessURL.Scheme == "http", proxy1Region.Nodes[0].ForceHTTP)
// The second proxy region:
require.Equal(t, "worst-proxy", proxy2Region.RegionName)
require.Equal(t, "coder_worst-proxy", proxy2Region.RegionCode)
require.Equal(t, 10002, proxy2Region.RegionID)
require.False(t, proxy2Region.EmbeddedRelay)
- require.Len(t, proxy2Region.Nodes, 2) // proxy + stun
- require.Equal(t, "10002a", proxy2Region.Nodes[1].Name)
- require.Equal(t, 10002, proxy2Region.Nodes[1].RegionID)
- require.Equal(t, proxyAPI2.Options.AccessURL.Hostname(), proxy2Region.Nodes[1].HostName)
- require.Equal(t, proxyAPI2.Options.AccessURL.Port(), fmt.Sprint(proxy2Region.Nodes[1].DERPPort))
- require.Equal(t, proxyAPI2.Options.AccessURL.Scheme == "http", proxy2Region.Nodes[1].ForceHTTP)
+ require.Len(t, proxy2Region.Nodes, 1)
+ require.Equal(t, "10002a", proxy2Region.Nodes[0].Name)
+ require.Equal(t, 10002, proxy2Region.Nodes[0].RegionID)
+ require.Equal(t, proxyAPI2.Options.AccessURL.Hostname(), proxy2Region.Nodes[0].HostName)
+ require.Equal(t, proxyAPI2.Options.AccessURL.Port(), fmt.Sprint(proxy2Region.Nodes[0].DERPPort))
+ require.Equal(t, proxyAPI2.Options.AccessURL.Scheme == "http", proxy2Region.Nodes[0].ForceHTTP)
})
t.Run("ConnectDERP", func(t *testing.T) {
@@ -270,11 +274,15 @@ resourceLoop:
connInfo, err := client.WorkspaceAgentConnectionInfo(testutil.Context(t, testutil.WaitLong), agentID)
require.NoError(t, err)
require.NotNil(t, connInfo.DERPMap)
- require.Len(t, connInfo.DERPMap.Regions, 3)
+ require.Len(t, connInfo.DERPMap.Regions, 3+len(api.DeploymentValues.DERP.Server.STUNAddresses.Value()))
// Connect to each region.
for _, r := range connInfo.DERPMap.Regions {
r := r
+ if len(r.Nodes) == 1 && r.Nodes[0].STUNOnly {
+ // Skip STUN-only regions.
+ continue
+ }
t.Run(r.RegionName, func(t *testing.T) {
t.Parallel()
@@ -311,132 +319,6 @@ resourceLoop:
})
}
-func TestDERPMapStunNodes(t *testing.T) {
- t.Parallel()
-
- deploymentValues := coderdtest.DeploymentValues(t)
- deploymentValues.Experiments = []string{
- string(codersdk.ExperimentMoons),
- "*",
- }
- stunAddresses := []string{
- "stun.l.google.com:19302",
- "stun1.l.google.com:19302",
- "stun2.l.google.com:19302",
- "stun3.l.google.com:19302",
- "stun4.l.google.com:19302",
- }
- deploymentValues.DERP.Server.STUNAddresses = stunAddresses
-
- client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
- Options: &coderdtest.Options{
- DeploymentValues: deploymentValues,
- AppHostname: "*.primary.test.coder.com",
- IncludeProvisionerDaemon: true,
- RealIPConfig: &httpmw.RealIPConfig{
- TrustedOrigins: []*net.IPNet{{
- IP: net.ParseIP("127.0.0.1"),
- Mask: net.CIDRMask(8, 32),
- }},
- TrustedHeaders: []string{
- "CF-Connecting-IP",
- },
- },
- },
- LicenseOptions: &coderdenttest.LicenseOptions{
- Features: license.Features{
- codersdk.FeatureWorkspaceProxy: 1,
- },
- },
- })
- t.Cleanup(func() {
- _ = closer.Close()
- })
-
- // Create a running external proxy.
- _ = coderdenttest.NewWorkspaceProxy(t, api, client, &coderdenttest.ProxyOptions{
- Name: "cool-proxy",
- })
-
- // Wait for both running proxies to become healthy.
- require.Eventually(t, func() bool {
- healthCtx := testutil.Context(t, testutil.WaitLong)
- err := api.ProxyHealth.ForceUpdate(healthCtx)
- if !assert.NoError(t, err) {
- return false
- }
-
- regions, err := client.Regions(healthCtx)
- if !assert.NoError(t, err) {
- return false
- }
- if !assert.Len(t, regions, 2) {
- return false
- }
-
- // All regions should be healthy.
- for _, r := range regions {
- if !r.Healthy {
- return false
- }
- }
- return true
- }, testutil.WaitLong, testutil.IntervalMedium)
-
- // Get the DERP map and ensure that the built-in region and the proxy region
- // both have the STUN nodes.
- ctx := testutil.Context(t, testutil.WaitLong)
- connInfo, err := client.WorkspaceAgentConnectionInfoGeneric(ctx)
- require.NoError(t, err)
-
- // There should be two DERP servers in the map: the primary and the
- // proxy.
- require.NotNil(t, connInfo.DERPMap)
- require.Len(t, connInfo.DERPMap.Regions, 2)
-
- var (
- primaryRegion *tailcfg.DERPRegion
- proxyRegion *tailcfg.DERPRegion
- )
- for _, r := range connInfo.DERPMap.Regions {
- if r.EmbeddedRelay {
- primaryRegion = r
- continue
- }
- if r.RegionName == "cool-proxy" {
- proxyRegion = r
- continue
- }
-
- t.Fatalf("unexpected region: %+v", r)
- }
-
- // The primary region:
- require.Equal(t, "Coder Embedded Relay", primaryRegion.RegionName)
- require.Equal(t, "coder", primaryRegion.RegionCode)
- require.Equal(t, 999, primaryRegion.RegionID)
- require.True(t, primaryRegion.EmbeddedRelay)
- require.Len(t, primaryRegion.Nodes, len(stunAddresses)+1)
-
- // The proxy region:
- require.Equal(t, "cool-proxy", proxyRegion.RegionName)
- require.Equal(t, "coder_cool-proxy", proxyRegion.RegionCode)
- require.Equal(t, 10001, proxyRegion.RegionID)
- require.False(t, proxyRegion.EmbeddedRelay)
- require.Len(t, proxyRegion.Nodes, len(stunAddresses)+1)
-
- for _, region := range []*tailcfg.DERPRegion{primaryRegion, proxyRegion} {
- stunNodes, err := tailnet.STUNNodes(region.RegionID, stunAddresses)
- require.NoError(t, err)
- require.Len(t, stunNodes, len(stunAddresses))
-
- require.Equal(t, stunNodes, region.Nodes[:len(stunNodes)])
-
- // The last node should be the Coder server.
- require.NotZero(t, region.Nodes[len(region.Nodes)-1].DERPPort)
- }
-}
-
func TestDERPEndToEnd(t *testing.T) {
t.Parallel()
@@ -596,6 +478,7 @@ func TestWorkspaceProxyWorkspaceApps_Wsconncache(t *testing.T) {
"CF-Connecting-IP",
},
},
+ WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
@@ -654,6 +537,7 @@ func TestWorkspaceProxyWorkspaceApps_SingleTailnet(t *testing.T) {
"CF-Connecting-IP",
},
},
+ WorkspaceAppsStatsCollectorOptions: opts.StatsCollectorOptions,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
diff --git a/enterprise/wsproxy/wsproxysdk/wsproxysdk.go b/enterprise/wsproxy/wsproxysdk/wsproxysdk.go
index 6d1a4b1227b5d..74c381c2d8b4a 100644
--- a/enterprise/wsproxy/wsproxysdk/wsproxysdk.go
+++ b/enterprise/wsproxy/wsproxysdk/wsproxysdk.go
@@ -14,14 +14,15 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
+ "tailscale.com/tailcfg"
"tailscale.com/util/singleflight"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/tailnet"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/tailnet"
)
// Client is a HTTP client for a subset of Coder API routes that external
@@ -152,6 +153,25 @@ func (c *Client) IssueSignedAppTokenHTML(ctx context.Context, rw http.ResponseWr
return res, true
}
+type ReportAppStatsRequest struct {
+ Stats []workspaceapps.StatsReport `json:"stats"`
+}
+
+// ReportAppStats reports the given app stats to the primary coder server.
+func (c *Client) ReportAppStats(ctx context.Context, req ReportAppStatsRequest) error {
+ resp, err := c.Request(ctx, http.MethodPost, "/api/v2/workspaceproxies/me/app-stats", req)
+ if err != nil {
+ return xerrors.Errorf("make request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent {
+ return codersdk.ReadBodyAsError(resp)
+ }
+
+ return nil
+}
+
type RegisterWorkspaceProxyRequest struct {
// AccessURL that hits the workspace proxy api.
AccessURL string `json:"access_url"`
@@ -187,9 +207,11 @@ type RegisterWorkspaceProxyRequest struct {
}
type RegisterWorkspaceProxyResponse struct {
- AppSecurityKey string `json:"app_security_key"`
- DERPMeshKey string `json:"derp_mesh_key"`
- DERPRegionID int32 `json:"derp_region_id"`
+ AppSecurityKey string `json:"app_security_key"`
+ DERPMeshKey string `json:"derp_mesh_key"`
+ DERPRegionID int32 `json:"derp_region_id"`
+ DERPMap *tailcfg.DERPMap `json:"derp_map"`
+ DERPForceWebSockets bool `json:"derp_force_websockets"`
// SiblingReplicas is a list of all other replicas of the proxy that have
// not timed out.
SiblingReplicas []codersdk.Replica `json:"sibling_replicas"`
diff --git a/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go b/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go
index 207283a098532..4be8d510fb723 100644
--- a/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go
+++ b/enterprise/wsproxy/wsproxysdk/wsproxysdk_test.go
@@ -24,14 +24,14 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/workspaceapps"
- "github.com/coder/coder/enterprise/tailnet"
- "github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
- agpl "github.com/coder/coder/tailnet"
- "github.com/coder/coder/tailnet/tailnettest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/coderd/workspaceapps"
+ "github.com/coder/coder/v2/enterprise/tailnet"
+ "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
+ agpl "github.com/coder/coder/v2/tailnet"
+ "github.com/coder/coder/v2/tailnet/tailnettest"
+ "github.com/coder/coder/v2/testutil"
)
func Test_IssueSignedAppTokenHTML(t *testing.T) {
diff --git a/examples/examples.go b/examples/examples.go
index 5f8f37bb5075e..401fd2f76ab59 100644
--- a/examples/examples.go
+++ b/examples/examples.go
@@ -14,7 +14,7 @@ import (
"golang.org/x/sync/singleflight"
"golang.org/x/xerrors"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
var (
diff --git a/examples/examples_test.go b/examples/examples_test.go
index e4a3c38a0ea79..76a32242a52d4 100644
--- a/examples/examples_test.go
+++ b/examples/examples_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/examples"
+ "github.com/coder/coder/v2/examples"
)
func TestTemplate(t *testing.T) {
diff --git a/examples/lima/README.md b/examples/lima/README.md
index 9828201ae9d80..aac38a8ec24ba 100644
--- a/examples/lima/README.md
+++ b/examples/lima/README.md
@@ -20,7 +20,7 @@ This will:
- Start an Ubuntu 22.04 VM
- Install Docker and Terraform from the official repos
-- Install Coder using the [installation script](https://coder.com/docs/coder-oss/latest/install#installsh)
+- Install Coder using the [installation script](../../docs/install/install.sh.md)
- Generates an initial user account `admin@coder.com` with a randomly generated password (stored in the VM under `/home/${USER}.linux/.config/coderv2/password`)
- Initializes a [sample Docker template](https://github.com/coder/coder/tree/main/examples/templates/docker) for creating workspaces
diff --git a/examples/parameters/README.md b/examples/parameters/README.md
new file mode 100644
index 0000000000000..8ebd3ee3c8b50
--- /dev/null
+++ b/examples/parameters/README.md
@@ -0,0 +1,19 @@
+---
+name: Sample Template with Parameters
+description: Review the sample template and introduce parameters to your template
+tags: [local, docker, parameters]
+icon: /icon/docker.png
+---
+
+# Overview
+
+This Coder template presents various features of [rich parameters](https://coder.com/docs/v2/latest/templates/parameters), including types, validation constraints,
+mutability, ephemeral (one-time) parameters, etc.
+
+## Development
+
+Update the template and push it using the following command:
+
+```bash
+./scripts/coder-dev.sh templates push examples-parameters -d examples/parameters --create
+```
diff --git a/examples/templates/jfrog-docker/build/Dockerfile b/examples/parameters/build/Dockerfile
similarity index 82%
rename from examples/templates/jfrog-docker/build/Dockerfile
rename to examples/parameters/build/Dockerfile
index 1dfaa77015f32..a443b5d07100e 100644
--- a/examples/templates/jfrog-docker/build/Dockerfile
+++ b/examples/parameters/build/Dockerfile
@@ -8,14 +8,11 @@ RUN apt-get update \
sudo \
vim \
wget \
- npm \
&& rm -rf /var/lib/apt/lists/*
ARG USER=coder
RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \
&& echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \
&& chmod 0440 /etc/sudoers.d/${USER}
-RUN curl -fL https://install-cli.jfrog.io | sh
-RUN chmod 755 $(which jf)
USER ${USER}
WORKDIR /home/${USER}
diff --git a/examples/parameters/main.tf b/examples/parameters/main.tf
new file mode 100644
index 0000000000000..0903a2d2e6475
--- /dev/null
+++ b/examples/parameters/main.tf
@@ -0,0 +1,281 @@
+terraform {
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = "~> 0.11.1"
+ }
+ docker = {
+ source = "kreuzwerker/docker"
+ version = "~> 3.0.1"
+ }
+ }
+}
+
+locals {
+ username = data.coder_workspace.me.owner
+}
+
+data "coder_provisioner" "me" {
+}
+
+provider "docker" {
+}
+
+data "coder_workspace" "me" {
+}
+
+resource "coder_agent" "main" {
+ arch = data.coder_provisioner.me.arch
+ os = "linux"
+ startup_script_timeout = 180
+ startup_script = <<-EOT
+ set -e
+
+ # install and start code-server
+ curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.11.0
+ /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &
+ EOT
+}
+
+resource "coder_app" "code-server" {
+ agent_id = coder_agent.main.id
+ slug = "code-server"
+ display_name = "code-server"
+ url = "http://localhost:13337/?folder=/home/${local.username}"
+ icon = "/icon/code.svg"
+ subdomain = false
+ share = "owner"
+
+ healthcheck {
+ url = "http://localhost:13337/healthz"
+ interval = 5
+ threshold = 6
+ }
+}
+
+resource "docker_volume" "home_volume" {
+ name = "coder-${data.coder_workspace.me.id}-home"
+ # Protect the volume from being deleted due to changes in attributes.
+ lifecycle {
+ ignore_changes = all
+ }
+ # Add labels in Docker to keep track of orphan resources.
+ labels {
+ label = "coder.owner"
+ value = data.coder_workspace.me.owner
+ }
+ labels {
+ label = "coder.owner_id"
+ value = data.coder_workspace.me.owner_id
+ }
+ labels {
+ label = "coder.workspace_id"
+ value = data.coder_workspace.me.id
+ }
+ # This field becomes outdated if the workspace is renamed but can
+ # be useful for debugging or cleaning out dangling volumes.
+ labels {
+ label = "coder.workspace_name_at_creation"
+ value = data.coder_workspace.me.name
+ }
+}
+
+resource "docker_image" "main" {
+ name = "coder-${data.coder_workspace.me.id}"
+ build {
+ context = "./build"
+ build_args = {
+ USER = local.username
+ }
+ }
+ triggers = {
+ dir_sha1 = sha1(join("", [for f in fileset(path.module, "build/*") : filesha1(f)]))
+ }
+}
+
+resource "docker_container" "workspace" {
+ count = data.coder_workspace.me.start_count
+ image = docker_image.main.name
+ # Uses lower() to avoid Docker restriction on container names.
+ name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}"
+ # Hostname makes the shell more user friendly: coder@my-workspace:~$
+ hostname = data.coder_workspace.me.name
+ # Use the docker gateway if the access URL is 127.0.0.1
+ entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
+ env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
+ host {
+ host = "host.docker.internal"
+ ip = "host-gateway"
+ }
+ volumes {
+ container_path = "/home/${local.username}"
+ volume_name = docker_volume.home_volume.name
+ read_only = false
+ }
+ # Add labels in Docker to keep track of orphan resources.
+ labels {
+ label = "coder.owner"
+ value = data.coder_workspace.me.owner
+ }
+ labels {
+ label = "coder.owner_id"
+ value = data.coder_workspace.me.owner_id
+ }
+ labels {
+ label = "coder.workspace_id"
+ value = data.coder_workspace.me.id
+ }
+ labels {
+ label = "coder.workspace_name"
+ value = data.coder_workspace.me.name
+ }
+}
+
+// Rich parameters
+// See: https://coder.com/docs/v2/latest/templates/parameters
+
+data "coder_parameter" "project_id" {
+ name = "project_id"
+ display_name = "My Project ID"
+ icon = "/emojis/1fab5.png"
+ description = "Specify the project ID to deploy in workspace."
+ default = "A1B2C3"
+ mutable = true
+ validation {
+ regex = "^[A-Z0-9]+$"
+ error = "Project ID is incorrect"
+ }
+
+ order = 1
+}
+
+data "coder_parameter" "region" {
+ name = "region"
+ display_name = "Region"
+ icon = "/emojis/1f30e.png"
+ description = "Select the region in which you would like to deploy your workspace."
+ default = "eu-helsinki"
+ option {
+ icon = "/emojis/1f1fa-1f1f8.png"
+ name = "Pittsburgh"
+ description = "Pittsburgh is a city in the Commonwealth of Pennsylvania and the county seat of Allegheny County."
+ value = "us-pittsburgh"
+ }
+ option {
+ icon = "/emojis/1f1eb-1f1ee.png"
+ name = "Helsinki"
+ description = "Helsinki, the capital city of Finland, is renowned for its vibrant cultural scene, stunning waterfront architecture, and a harmonious blend of modernity and natural beauty."
+ value = "eu-helsinki"
+ }
+ option {
+ icon = "/emojis/1f1e6-1f1fa.png"
+ name = "Sydney"
+ description = "Sydney, the largest city in Australia, captivates with its iconic Sydney Opera House, picturesque harbor, and diverse neighborhoods, making it a captivating blend of urban sophistication and coastal charm."
+ value = "ap-sydney"
+ }
+
+ order = 1
+}
+
+data "coder_parameter" "apps_dir" {
+ name = "apps_dir"
+ display_name = "Apps Directory"
+ icon = "/emojis/1f9ba.png"
+ type = "string"
+ description = "Specify the directory to install project applications and tools."
+ default = "/var/apps"
+
+ order = 2
+}
+
+data "coder_parameter" "worker_instances" {
+ name = "worker_instances"
+ display_name = "Worker Instances"
+ icon = "/emojis/2697.png"
+ type = "number"
+ description = "Specify the number of worker instances to spawn."
+ default = "3"
+ mutable = true
+ validation {
+ min = 3
+ max = 12
+ monotonic = "increasing"
+ }
+ order = 2
+}
+
+data "coder_parameter" "security_groups" {
+ name = "security_groups"
+ display_name = "Security Groups"
+ icon = "/emojis/26f4.png"
+ type = "list(string)"
+ description = "Select relevant security groups."
+ mutable = true
+ default = jsonencode([
+ "Web Server Security Group",
+ "Database Security Group",
+ "Backend Security Group"
+ ])
+ order = 2
+}
+
+data "coder_parameter" "docker_image" {
+ name = "docker_image"
+ display_name = "Docker Image"
+ mutable = true
+ type = "string"
+ description = "Docker image for the development container"
+ default = "ghcr.io/coder/coder-preview:main"
+
+ order = 3
+}
+
+data "coder_parameter" "command_line_args" {
+ name = "command_line_args"
+ display_name = "Extra command line args"
+ type = "string"
+ default = ""
+ description = "Provide extra command line args for the startup script."
+ mutable = true
+ order = 80
+}
+
+data "coder_parameter" "enable_monitoring" {
+ name = "enable_monitoring"
+ display_name = "Enable Workspace Monitoring"
+ type = "bool"
+ description = "This monitoring functionality empowers you to closely track the health and resource utilization of your instance in real-time."
+ mutable = true
+ order = 90
+}
+
+// Build options (ephemeral parameters)
+// See: https://coder.com/docs/v2/latest/templates/parameters#ephemeral-parameters
+
+data "coder_parameter" "pause-startup" {
+ name = "pause-startup"
+ display_name = "Pause startup script"
+ type = "number"
+ description = "Pause the startup script (seconds)"
+ default = "1"
+ mutable = true
+ ephemeral = true
+ validation {
+ min = 0
+ max = 300
+ }
+
+ order = 4
+}
+
+data "coder_parameter" "force-rebuild" {
+ name = "force-rebuild"
+ display_name = "Force rebuild project"
+ type = "bool"
+ description = "Rebuild the workspace project"
+ default = "false"
+ mutable = true
+ ephemeral = true
+
+ order = 4
+}
diff --git a/examples/templates/aws-linux/main.tf b/examples/templates/aws-linux/main.tf
index 96d9136dfe758..f1f41024d938a 100644
--- a/examples/templates/aws-linux/main.tf
+++ b/examples/templates/aws-linux/main.tf
@@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
- version = "~> 0.7.0"
+ version = "~> 0.11.0"
}
aws = {
source = "hashicorp/aws"
@@ -30,24 +30,24 @@ data "coder_parameter" "region" {
icon = "/emojis/1f1f0-1f1f7.png"
}
option {
- name = "Asia Pacific (Osaka-Local)"
+ name = "Asia Pacific (Osaka)"
value = "ap-northeast-3"
- icon = "/emojis/1f1f0-1f1f7.png"
+ icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Asia Pacific (Mumbai)"
value = "ap-south-1"
- icon = "/emojis/1f1f0-1f1f7.png"
+ icon = "/emojis/1f1ee-1f1f3.png"
}
option {
name = "Asia Pacific (Singapore)"
value = "ap-southeast-1"
- icon = "/emojis/1f1f0-1f1f7.png"
+ icon = "/emojis/1f1f8-1f1ec.png"
}
option {
name = "Asia Pacific (Sydney)"
value = "ap-southeast-2"
- icon = "/emojis/1f1f0-1f1f7.png"
+ icon = "/emojis/1f1e6-1f1fa.png"
}
option {
name = "Canada (Central)"
@@ -176,33 +176,21 @@ resource "coder_agent" "main" {
display_name = "CPU Usage"
interval = 5
timeout = 5
- script = <<-EOT
- #!/bin/bash
- set -e
- top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "%"}'
- EOT
+ script = "coder stat cpu"
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
- script = <<-EOT
- #!/bin/bash
- set -e
- free -m | awk 'NR==2{printf "%.2f%%\t", $3*100/$2 }'
- EOT
+ script = "coder stat mem"
}
metadata {
key = "disk"
display_name = "Disk Usage"
interval = 600 # every 10 minutes
timeout = 30 # df can take a while on large filesystems
- script = <<-EOT
- #!/bin/bash
- set -e
- df /home/coder | awk '$NF=="/"{printf "%s", $5}'
- EOT
+ script = "coder stat disk --path $HOME"
}
}
@@ -223,11 +211,9 @@ resource "coder_app" "code-server" {
}
locals {
-
- # User data is used to stop/start AWS instances. See:
- # https://github.com/hashicorp/terraform-provider-aws/issues/22
-
- user_data_start = < If you have created a template, see one that's missing or one that's no longer
> maintained, please submit a pull request to improve this list. Thank you!
## Templates
-- [ntimo/coder-hetzner-cloud-template](https://github.com/ntimo/coder-hetzner-cloud-template) - Setup a Hetzner Cloud instance as dev environment with or without vscode.
-- [matifali/coder-templates](https://github.com/matifali/coder-templates) - Deeplearning with Jupyter Notebook/Lab and Matlab in browser.
-- [m.lan/coder-templates](https://gitlab.com/m.lan/coder-templates) - Kubernetes template with DinD.
-- [jsjoeio/coder-templates](https://github.com/jsjoeio/coder-templates) - Docker templates that prompt for dotfiles and base Docker image.
-- [sharkymark/v2-templates](https://github.com/sharkymark/v2-templates) - Kubernetes, Docker, AWS, Google Cloud, Azure templates, videos, emoji links, and API examples.
-- [kozmiknano/vscode-server-template](https://github.com/KozmikNano/vscode-server-template) - Run the full VS Code server within docker! (Built-in settings sync and Microsoft Marketplace enabled)
-- [atnomoverflow/coder-template](https://github.com/atnomoverflow/coder-template) - Kubernetes template that install VS code server Rstudio jupyter and also set ssh access to gitlab (Works also on self managed gitlab).
+- [ntimo/coder-hetzner-cloud-template](https://github.com/ntimo/coder-hetzner-cloud-template) -
+ Setup a Hetzner Cloud instance as dev environment with or without vscode.
+- [matifali/coder-templates](https://github.com/matifali/coder-templates) -
+ Deeplearning with Jupyter Notebook/Lab and Matlab in browser.
+- [m.lan/coder-templates](https://gitlab.com/m.lan/coder-templates) - Kubernetes
+ template with DinD.
+- [jsjoeio/coder-templates](https://github.com/jsjoeio/coder-templates) - Docker
+ templates that prompt for dotfiles and base Docker image.
+- [sharkymark/v2-templates](https://github.com/sharkymark/v2-templates) -
+ Kubernetes, Docker, AWS, Google Cloud, Azure templates, videos, emoji links,
+ and API examples.
+- [bpmct/coder-templates](https://github.com/bpmct/coder-templates) -
+ Kubernetes, OpenStack, podman, Docker, VM, AWS, Google Cloud, Azure templates.
+- [kozmiknano/vscode-server-template](https://github.com/KozmikNano/vscode-server-template) -
+ Run the full VS Code server within docker! (Built-in settings sync and
+ Microsoft Marketplace enabled)
+- [atnomoverflow/coder-template](https://github.com/atnomoverflow/coder-template) -
+ Kubernetes template that install VS code server Rstudio jupyter and also set
+ ssh access to gitlab (Works also on self managed gitlab).
## Automation
-- [Update Coder Template](https://github.com/marketplace/actions/update-coder-template) - A GitHub action to automate coder template changes.
+- [Update Coder Template](https://github.com/marketplace/actions/update-coder-template) -
+ A GitHub action to automate coder template changes.
diff --git a/examples/templates/devcontainer-docker/main.tf b/examples/templates/devcontainer-docker/main.tf
index 8769ff1f07078..5941b0c6b9450 100644
--- a/examples/templates/devcontainer-docker/main.tf
+++ b/examples/templates/devcontainer-docker/main.tf
@@ -206,7 +206,9 @@ data "coder_parameter" "custom_repo_url" {
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
- image = "ghcr.io/coder/envbuilder:0.1.3"
+ # Find the latest version here:
+ # https://github.com/coder/envbuilder/tags
+ image = "ghcr.io/coder/envbuilder:0.2.1"
# Uses lower() to avoid Docker restriction on container names.
name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}"
# Hostname makes the shell more user friendly: coder@my-workspace:~$
diff --git a/examples/templates/devcontainer-kubernetes/main.tf b/examples/templates/devcontainer-kubernetes/main.tf
index 65694c25989a9..58c183e5be173 100644
--- a/examples/templates/devcontainer-kubernetes/main.tf
+++ b/examples/templates/devcontainer-kubernetes/main.tf
@@ -187,8 +187,10 @@ resource "kubernetes_deployment" "workspace" {
}
spec {
container {
- name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}"
- image = "ghcr.io/coder/envbuilder:0.1.3"
+ name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}"
+ # Find the latest version here:
+ # https://github.com/coder/envbuilder/tags
+ image = "ghcr.io/coder/envbuilder:0.2.1"
env {
name = "CODER_AGENT_TOKEN"
value = coder_agent.main.token
diff --git a/examples/templates/jfrog-docker/README.md b/examples/templates/jfrog/docker/README.md
similarity index 97%
rename from examples/templates/jfrog-docker/README.md
rename to examples/templates/jfrog/docker/README.md
index ac1a3a128643f..4db4676e8a43d 100644
--- a/examples/templates/jfrog-docker/README.md
+++ b/examples/templates/jfrog/docker/README.md
@@ -5,7 +5,7 @@ tags: [local, docker, jfrog]
icon: /icon/docker.png
---
-# jfrog-docker
+# docker
To get started, run `coder templates init`. When prompted, select this template.
Follow the on-screen instructions to proceed.
diff --git a/examples/templates/jfrog/docker/build/Dockerfile b/examples/templates/jfrog/docker/build/Dockerfile
new file mode 100644
index 0000000000000..2d966c10cffa2
--- /dev/null
+++ b/examples/templates/jfrog/docker/build/Dockerfile
@@ -0,0 +1,28 @@
+FROM ubuntu
+
+RUN apt-get update \
+ && apt-get install -y \
+ curl \
+ git \
+ python3-pip \
+ sudo \
+ vim \
+ wget \
+ npm \
+ && rm -rf /var/lib/apt/lists/*
+
+ARG GO_VERSION=1.20.7
+RUN mkdir --parents /usr/local/go && curl --silent --show-error --location \
+ "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /usr/local/go.tar.gz && \
+ tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1
+
+ENV PATH=$PATH:/usr/local/go/bin
+
+ARG USER=coder
+RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \
+ && echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \
+ && chmod 0440 /etc/sudoers.d/${USER}
+RUN curl -fL https://install-cli.jfrog.io | sh
+RUN chmod 755 $(which jf)
+USER ${USER}
+WORKDIR /home/${USER}
diff --git a/examples/templates/jfrog-docker/main.tf b/examples/templates/jfrog/docker/main.tf
similarity index 51%
rename from examples/templates/jfrog-docker/main.tf
rename to examples/templates/jfrog/docker/main.tf
index 0c409d5ebe54b..f5bcb6728cf59 100644
--- a/examples/templates/jfrog-docker/main.tf
+++ b/examples/templates/jfrog/docker/main.tf
@@ -10,13 +10,21 @@ terraform {
}
artifactory = {
source = "registry.terraform.io/jfrog/artifactory"
- version = "6.22.3"
+ version = "~> 8.4.0"
}
}
}
locals {
- username = data.coder_workspace.me.owner
+ # take care to use owner_email instead of owner because users can change
+ # their username.
+ artifactory_username = data.coder_workspace.me.owner_email
+ artifactory_repository_keys = {
+ "npm" = "npm"
+ "python" = "python"
+ "go" = "go"
+ }
+ workspace_user = data.coder_workspace.me.owner
}
data "coder_provisioner" "me" {
@@ -28,9 +36,9 @@ provider "docker" {
data "coder_workspace" "me" {
}
-variable "jfrog_url" {
+variable "jfrog_host" {
type = string
- description = "The URL of the JFrog instance."
+ description = "JFrog instance hostname. For example, 'YYY.jfrog.io'."
}
variable "artifactory_access_token" {
@@ -38,17 +46,16 @@ variable "artifactory_access_token" {
description = "The admin-level access token to use for JFrog."
}
-
# Configure the Artifactory provider
provider "artifactory" {
- url = "${var.jfrog_url}/artifactory"
+ url = "https://${var.jfrog_host}/artifactory"
access_token = var.artifactory_access_token
}
-resource "artifactory_access_token" "me" {
- username = data.coder_workspace.me.owner_email
- # The token should live for the duration of the workspace.
- end_date_relative = "0s"
+resource "artifactory_scoped_token" "me" {
+ # This is hacky, but on terraform plan the data source gives empty strings,
+ # which fails validation.
+ username = length(local.artifactory_username) > 0 ? local.artifactory_username : "plan"
}
resource "coder_agent" "main" {
@@ -62,28 +69,53 @@ resource "coder_agent" "main" {
curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.11.0
/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &
+ # Install the JFrog VS Code extension.
+ # Find the latest version number at
+ # https://open-vsx.org/extension/JFrog/jfrog-vscode-extension.
+ JFROG_EXT_VERSION=2.4.1
+ curl -o /tmp/jfrog.vsix -L "https://open-vsx.org/api/JFrog/jfrog-vscode-extension/$JFROG_EXT_VERSION/file/JFrog.jfrog-vscode-extension-$JFROG_EXT_VERSION.vsix"
+ /tmp/code-server/bin/code-server --install-extension /tmp/jfrog.vsix
+
# The jf CLI checks $CI when determining whether to use interactive
# flows.
export CI=true
jf c rm 0 || true
- echo ${artifactory_access_token.me.access_token} | \
- jf c add --access-token-stdin --url ${var.jfrog_url} 0
+ echo ${artifactory_scoped_token.me.access_token} | \
+ jf c add --access-token-stdin --url https://${var.jfrog_host} 0
- # Configure the `npm` CLI to use the Artifactory "npm" registry.
+ # Configure the `npm` CLI to use the Artifactory "npm" repository.
cat << EOF > ~/.npmrc
email = ${data.coder_workspace.me.owner_email}
- registry=${var.jfrog_url}/artifactory/api/npm/npm/
+ registry = https://${var.jfrog_host}/artifactory/api/npm/${local.artifactory_repository_keys["npm"]}
EOF
jf rt curl /api/npm/auth >> .npmrc
+
+ # Configure the `pip` to use the Artifactory "python" repository.
+ mkdir -p ~/.pip
+ cat << EOF > ~/.pip/pip.conf
+ [global]
+ index-url = https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/pypi/${local.artifactory_repository_keys["python"]}/simple
+ EOF
+
EOT
+ # Set GOPROXY to use the Artifactory "go" repository.
+ env = {
+ GOPROXY : "https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/go/${local.artifactory_repository_keys["go"]}"
+ # Authenticate with JFrog extension.
+ JFROG_IDE_URL : "https://${var.jfrog_host}"
+ JFROG_IDE_USERNAME : "${local.artifactory_username}"
+ JFROG_IDE_PASSWORD : "${artifactory_scoped_token.me.access_token}"
+ JFROG_IDE_ACCESS_TOKEN : "${artifactory_scoped_token.me.access_token}"
+ JFROG_IDE_STORE_CONNECTION : "true"
+ }
}
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
- url = "http://localhost:13337/?folder=/home/${local.username}"
+ url = "http://localhost:13337/?folder=/home/${local.workspace_user}"
icon = "/icon/code.svg"
subdomain = false
share = "owner"
@@ -106,13 +138,13 @@ resource "docker_volume" "home_volume" {
resource "docker_image" "main" {
name = "coder-${data.coder_workspace.me.id}"
build {
- context = "./build"
+ context = "${path.module}/build"
build_args = {
- USER = local.username
+ USER = local.workspace_user
}
}
triggers = {
- dir_sha1 = sha1(join("", [for f in fileset(path.module, "build/*") : filesha1(f)]))
+ dir_sha1 = sha1(join("", [for f in fileset(path.module, "build/*") : filesha1("${path.module}/${f}")]))
}
}
@@ -130,7 +162,7 @@ resource "docker_container" "workspace" {
ip = "host-gateway"
}
volumes {
- container_path = "/home/${local.username}"
+ container_path = "/home/${local.workspace_user}"
volume_name = docker_volume.home_volume.name
read_only = false
}
diff --git a/examples/templates/jfrog/remote/main.tf b/examples/templates/jfrog/remote/main.tf
new file mode 100644
index 0000000000000..77fd75ed55b56
--- /dev/null
+++ b/examples/templates/jfrog/remote/main.tf
@@ -0,0 +1,16 @@
+module "docker" {
+ source = "cdr.jfrog.io/tf__main/docker/docker"
+ jfrog_host = var.jfrog_host
+ artifactory_access_token = var.artifactory_access_token
+}
+
+variable "jfrog_host" {
+ type = string
+ description = "JFrog instance hostname. For example, 'YYY.jfrog.io'."
+}
+
+variable "artifactory_access_token" {
+ type = string
+ description = "The admin-level access token to use for JFrog."
+}
+
diff --git a/examples/web-server/apache/README.md b/examples/web-server/apache/README.md
index e2b5ba4a1164c..787aa884b35ad 100644
--- a/examples/web-server/apache/README.md
+++ b/examples/web-server/apache/README.md
@@ -4,7 +4,7 @@
1. Start a Coder deployment and be sure to set the following [configuration values](https://coder.com/docs/v2/latest/admin/configure):
- ```console
+ ```env
CODER_HTTP_ADDRESS=127.0.0.1:3000
CODER_ACCESS_URL=https://coder.example.com
CODER_WILDCARD_ACCESS_URL=*coder.example.com
@@ -18,13 +18,13 @@
3. Install Apache (assuming you're on Debian/Ubuntu):
- ```console
+ ```shell
sudo apt install apache2
```
4. Enable the following Apache modules:
- ```console
+ ```shell
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod ssl
@@ -33,7 +33,7 @@
5. Stop Apache service and disable default site:
- ```console
+ ```shell
sudo a2dissite 000-default.conf
sudo systemctl stop apache2
```
@@ -56,7 +56,7 @@
dns_cloudflare_api_token = YOUR_API_TOKEN
```
- ```console
+ ```shell
mkdir -p ~/.secrets/certbot
touch ~/.secrets/certbot/cloudflare.ini
nano ~/.secrets/certbot/cloudflare.ini
@@ -64,7 +64,7 @@
3. Set the correct permissions:
- ```console
+ ```shell
sudo chmod 600 ~/.secrets/certbot/cloudflare.ini
```
@@ -72,7 +72,7 @@
1. Create the wildcard certificate:
- ```console
+ ```shell
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini -d coder.example.com -d *.coder.example.com
```
@@ -82,7 +82,7 @@
1. Create Apache configuration for Coder:
- ```console
+ ```shell
sudo nano /etc/apache2/sites-available/coder.conf
```
@@ -122,13 +122,13 @@
3. Enable the site:
- ```console
+ ```shell
sudo a2ensite coder.conf
```
4. Restart Apache:
- ```console
+ ```shell
sudo systemctl restart apache2
```
@@ -136,19 +136,19 @@
1. Create a new file in `/etc/cron.weekly`:
- ```console
+ ```shell
sudo touch /etc/cron.weekly/certbot
```
2. Make it executable:
- ```console
+ ```shell
sudo chmod +x /etc/cron.weekly/certbot
```
3. And add this code:
- ```sh
+ ```shell
#!/bin/sh
sudo certbot renew -q
```
diff --git a/examples/web-server/caddy/README.md b/examples/web-server/caddy/README.md
index 1b8eef65570ce..7e345fe08eb3b 100644
--- a/examples/web-server/caddy/README.md
+++ b/examples/web-server/caddy/README.md
@@ -10,7 +10,7 @@ This is an example configuration of how to use Coder with [caddy](https://caddys
1. Start with our example configuration
- ```console
+ ```shell
# Create a project folder
cd $HOME
mkdir coder-with-caddy
@@ -30,7 +30,7 @@ This is an example configuration of how to use Coder with [caddy](https://caddys
1. Start Coder. Set `CODER_ACCESS_URL` and `CODER_WILDCARD_ACCESS_URL` to the domain you're using in your Caddyfile.
- ```console
+ ```shell
export CODER_ACCESS_URL=https://coder.example.com
export CODER_WILDCARD_ACCESS_URL=*.coder.example.com
docker compose up -d # Run on startup
@@ -60,19 +60,19 @@ This is an example configuration of how to use Coder with [caddy](https://caddys
If you're [keeping Caddy running](https://caddyserver.com/docs/running) via a system service:
- ```console
+ ```shell
sudo systemctl restart caddy
```
Or run a standalone server:
- ```console
+ ```shell
caddy run
```
6. Optionally, use [ufw](https://wiki.ubuntu.com/UncomplicatedFirewall) or another firewall to disable external traffic outside of Caddy.
- ```console
+ ```shell
# Check status of UncomplicatedFirewall
sudo ufw status
diff --git a/examples/web-server/nginx/README.md b/examples/web-server/nginx/README.md
index c09edb5099db0..5c822856fdb1e 100644
--- a/examples/web-server/nginx/README.md
+++ b/examples/web-server/nginx/README.md
@@ -4,7 +4,7 @@
1. Start a Coder deployment and be sure to set the following [configuration values](https://coder.com/docs/v2/latest/admin/configure):
- ```console
+ ```env
CODER_HTTP_ADDRESS=127.0.0.1:3000
CODER_ACCESS_URL=https://coder.example.com
CODER_WILDCARD_ACCESS_URL=*coder.example.com
@@ -18,13 +18,13 @@
3. Install NGINX (assuming you're on Debian/Ubuntu):
- ```console
+ ```shell
sudo apt install nginx
```
4. Stop NGINX service:
- ```console
+ ```shell
sudo systemctl stop nginx
```
@@ -34,13 +34,13 @@
1. Create NGINX configuration for this app:
- ```console
+ ```shell
sudo touch /etc/nginx/sites-available/coder.example.com
```
2. Activate this file:
- ```console
+ ```shell
sudo ln -s /etc/nginx/sites-available/coder.example.com /etc/nginx/sites-enabled/coder.example.com
```
@@ -62,7 +62,7 @@
dns_cloudflare_api_token = YOUR_API_TOKEN
```
- ```console
+ ```shell
mkdir -p ~/.secrets/certbot
touch ~/.secrets/certbot/cloudflare.ini
nano ~/.secrets/certbot/cloudflare.ini
@@ -70,7 +70,7 @@
3. Set the correct permissions:
- ```console
+ ```shell
sudo chmod 600 ~/.secrets/certbot/cloudflare.ini
```
@@ -78,7 +78,7 @@
1. Create the wildcard certificate:
- ```console
+ ```shell
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini -d coder.example.com -d *.coder.example.com
```
@@ -86,7 +86,7 @@
1. Edit the file with:
- ```console
+ ```shell
sudo nano /etc/nginx/sites-available/coder.example.com
```
@@ -129,7 +129,7 @@
3. Test the configuration:
- ```console
+ ```shell
sudo nginx -t
```
@@ -137,26 +137,26 @@
1. Create a new file in `/etc/cron.weekly`:
- ```console
+ ```shell
sudo touch /etc/cron.weekly/certbot
```
2. Make it executable:
- ```console
+ ```shell
sudo chmod +x /etc/cron.weekly/certbot
```
3. And add this code:
- ```sh
+ ```shell
#!/bin/sh
sudo certbot renew -q
```
## Restart NGINX
-```console
+```shell
sudo systemctl restart nginx
```
diff --git a/flake.lock b/flake.lock
index 79d823fd527c6..9dc65b9c61e9f 100644
--- a/flake.lock
+++ b/flake.lock
@@ -70,11 +70,11 @@
},
"nixpkgs_2": {
"locked": {
- "lastModified": 1690179384,
- "narHash": "sha256-+arbgqFTAtoeKtepW9wCnA0njCOyoiDFyl0Q0SBSOtE=",
+ "lastModified": 1692447944,
+ "narHash": "sha256-fkJGNjEmTPvqBs215EQU4r9ivecV5Qge5cF/QDLVn3U=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "b12803b6d90e2e583429bb79b859ca53c348b39a",
+ "rev": "d680ded26da5cf104dd2735a51e88d2d8f487b4d",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index d2e7cd492dd7a..e8861a139fac0 100644
--- a/flake.nix
+++ b/flake.nix
@@ -11,49 +11,233 @@
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
- in
- {
formatter = pkgs.nixpkgs-fmt;
- devShells.default = pkgs.mkShell {
- buildInputs = with pkgs; [
- bash
- bat
- cairo
- drpc.defaultPackage.${system}
- exa
- getopt
- git
- go-migrate
- go_1_20
- golangci-lint
- gopls
- gotestsum
- jq
- kubernetes-helm
- mockgen
- nfpm
- nodePackages.pnpm
- nodePackages.typescript
- nodePackages.typescript-language-server
- nodejs
- openssh
- openssl
- pango
- pixman
- pkg-config
- postgresql
- protoc-gen-go
- ripgrep
- shellcheck
- shfmt
- sqlc
- terraform
- typos
- yq
- zip
- zstd
+ # Check in https://search.nixos.org/packages to find new packages.
+ # Use `nix --extra-experimental-features nix-command --extra-experimental-features flakes flake update`
+ # to update the lock file if packages are out-of-date.
+
+ # From https://nixos.wiki/wiki/Google_Cloud_SDK
+ gdk = pkgs.google-cloud-sdk.withExtraComponents ([pkgs.google-cloud-sdk.components.gke-gcloud-auth-plugin]);
+
+ devShellPackages = with pkgs; [
+ bat
+ cairo
+ curl
+ drpc.defaultPackage.${system}
+ gcc
+ gdk
+ getopt
+ git
+ gh
+ gnumake
+ gnused
+ go_1_20
+ go-migrate
+ golangci-lint
+ gopls
+ gotestsum
+ jq
+ kubectl
+ kubectx
+ kubernetes-helm
+ less
+ # Needed for many LD system libs!
+ libuuid
+ mockgen
+ nfpm
+ nodejs
+ nodePackages.pnpm
+ nodePackages.prettier
+ nodePackages.typescript
+ nodePackages.typescript-language-server
+ openssh
+ openssl
+ pango
+ pixman
+ pkg-config
+ postgresql_13
+ protobuf
+ protoc-gen-go
+ ripgrep
+ sapling
+ shellcheck
+ shfmt
+ sqlc
+ # strace is not available on OSX
+ (if system == "aarch64-darwin" then null else strace)
+ terraform
+ typos
+ vim
+ wget
+ yarn
+ yq-go
+ zip
+ zsh
+ zstd
+ ];
+ # We separate these to reduce the size of the dev shell for packages that we only
+ # want in the image.
+ devImagePackages = with pkgs; [
+ docker
+ exa
+ freetype
+ glib
+ harfbuzz
+ nix
+ nixpkgs-fmt
+ screen
+ ];
+
+ # This is the base image for our Docker container used for development.
+ # Use `nix-prefetch-docker ubuntu --arch amd64 --image-tag lunar` to get this.
+ baseDevEnvImage = pkgs.dockerTools.pullImage {
+ imageName = "ubuntu";
+ imageDigest = "sha256:7a520eeb6c18bc6d32a21bb7edcf673a7830813c169645d51c949cecb62387d0";
+ sha256 = "ajZzFSG/q7F5wAXfBOPpYBT+aVy8lqAXtBzkmAe2SeE=";
+ finalImageName = "ubuntu";
+ finalImageTag = "lunar";
+ };
+ # This is an intermediate stage that adds sudo with the setuid bit set.
+ # Nix doesn't allow setuid binaries in the store, so we have to do this
+ # in a separate stage.
+ intermediateDevEnvImage = pkgs.dockerTools.buildImage {
+ name = "intermediate";
+ fromImage = baseDevEnvImage;
+ runAsRoot = ''
+ #!${pkgs.runtimeShell}
+ ${pkgs.dockerTools.shadowSetup}
+ userdel ubuntu
+ groupadd docker
+ useradd coder \
+ --create-home \
+ --shell=/bin/bash \
+ --uid=1000 \
+ --user-group \
+ --groups docker
+ cp ${pkgs.sudo}/bin/sudo usr/bin/sudo
+ chmod 4755 usr/bin/sudo
+ mkdir -p /etc/init.d
+ '';
+ };
+ allPackages = devShellPackages ++ devImagePackages;
+ # Environment variables that live in `/etc/environment` in the container.
+ # These will also be applied to the container config.
+ devEnvVars = [
+ "PATH=${pkgs.lib.makeBinPath (allPackages)}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/coder/go/bin"
+ "LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath allPackages}"
+ # This setting prevents Go from using the public checksum database for
+ # our module path prefixes. It is required because these are in private
+ # repositories that require authentication.
+ #
+ # For details, see: https://golang.org/ref/mod#private-modules
+ "GOPRIVATE=coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder"
+ # Increase memory allocation to NodeJS
+ "NODE_OPTIONS=--max_old_space_size=8192"
+ "TERM=xterm-256color"
+ "LANG=en_US.UTF-8"
+ "LOCALE_ARCHIVE=/usr/lib/locale/locale-archive"
+ ];
+ # Builds our development environment image with all the tools included.
+ # Using Nix instead of Docker is **significantly** faster. This _build_
+ # doesn't really build anything, it just copies pre-built binaries into
+ # a container and adds them to the $PATH.
+ #
+ # To test changes and iterate on this, you can run:
+ # > nix build .#devEnvImage && ./result | docker load
+ # This will import the image into your local Docker daemon.
+ devEnvImage = pkgs.dockerTools.streamLayeredImage {
+ name = "codercom/oss-dogfood";
+ tag = "latest";
+ fromImage = intermediateDevEnvImage;
+ maxLayers = 64;
+ contents = [
+ # Required for `sudo` to persist the proper `PATH`.
+ (
+ pkgs.writeTextDir "etc/environment" (pkgs.lib.strings.concatLines devEnvVars)
+ )
+ # Allows `coder` to use `sudo` without a password.
+ (
+ pkgs.writeTextDir "etc/sudoers" ''
+ coder ALL=(ALL) NOPASSWD:ALL
+ ''
+ )
+ # Also allows `coder` to use `sudo` without a password.
+ (
+ pkgs.writeTextDir "etc/pam.d/other" ''
+ account sufficient pam_unix.so
+ auth sufficient pam_rootok.so
+ password requisite pam_unix.so nullok yescrypt
+ session required pam_unix.so
+ ''
+ )
+ # This allows users to chsh.
+ (
+ pkgs.writeTextDir "etc/pam.d/chsh" ''
+ auth sufficient pam_rootok.so
+ ''
+ )
+ # The default Nix config!
+ (
+ pkgs.writeTextDir "etc/nix/nix.conf" ''
+ experimental-features = nix-command flakes
+ ''
+ )
+ # Allow people to change shells!
+ (
+ pkgs.writeTextDir "etc/shells" ''
+ /bin/bash
+ ${pkgs.zsh}/bin/zsh
+ ''
+ )
+ # This is the debian script for managing Docker with `sudo service docker ...`.
+ (
+ pkgs.writeTextFile {
+ name = "docker";
+ destination = "/etc/init.d/docker";
+ executable = true;
+ text = (builtins.readFile (
+ pkgs.fetchFromGitHub
+ {
+ owner = "moby";
+ repo = "moby";
+ rev = "ae737656f9817fbd5afab96aa083754cfb81aab0";
+ sha256 = "sha256-oS3WplsxhKHCuHwL4/ytsCNJ1N/SZhlUZmzZTf81AoE=";
+ } + "/contrib/init/sysvinit-debian/docker"
+ ));
+ }
+ )
+ # The Docker script above looks here for the daemon binary location.
+ # Because we're injecting it with Nix, it's not in the default spot.
+ (
+ pkgs.writeTextDir "etc/default/docker" ''
+ DOCKERD=${pkgs.docker}/bin/dockerd
+ ''
+ )
+ # The same as `sudo apt install ca-certificates -y'.
+ (
+ pkgs.writeTextDir "etc/ssl/certs/ca-certificates.crt"
+ (builtins.readFile "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt")
+ )
];
+ # Required for the UTF-8 locale to exist!
+ extraCommands = ''
+ mkdir -p usr/lib/locale
+ cp -a ${pkgs.glibcLocales}/lib/locale/locale-archive usr/lib/locale/locale-archive
+ '';
+
+ config = {
+ Env = devEnvVars;
+ Entrypoint = [ "/bin/bash" ];
+ User = "coder";
+ };
+ };
+ in
+ {
+ packages = {
+ devEnvImage = devEnvImage;
};
+ defaultPackage = formatter; # or replace it with your desired default package.
+ devShell = pkgs.mkShell { buildInputs = devShellPackages; };
}
);
}
diff --git a/go.mod b/go.mod
index fa411a3263e27..67aac5fa2cbe9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/coder/coder
+module github.com/coder/coder/v2
go 1.20
@@ -36,7 +36,14 @@ replace github.com/dlclark/regexp2 => github.com/dlclark/regexp2 v1.7.0
// There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here:
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
-replace tailscale.com => github.com/coder/tailscale v0.0.0-20230731105344-d1b7f8087191
+replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20230824143504-4a17d5b8a684
+
+// This is replaced to include a fix that causes a deadlock when closing the
+// wireguard network.
+// The branch used is from https://github.com/coder/wireguard-go/tree/colin/tailscale
+// It is based on https://github.com/tailscale/wireguard-go/tree/tailscale, but
+// includes the upstream fix https://github.com/WireGuard/wireguard-go/commit/b7cd547315bed421a648d0a0f1ee5a0fc1b1151e
+replace github.com/tailscale/wireguard-go => github.com/coder/wireguard-go v0.0.0-20230807234434-d825b45ccbf5
// Use our tempfork of gvisor that includes a fix for TCP connection stalls:
// https://github.com/coder/coder/issues/7388
@@ -61,7 +68,7 @@ replace github.com/gliderlabs/ssh => github.com/coder/ssh v0.0.0-20230621095435-
replace github.com/imulab/go-scim/pkg/v2 => github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136
require (
- cdr.dev/slog v1.6.1
+ cdr.dev/slog v1.6.2-0.20230817204240-b386d5d10a80
cloud.google.com/go/compute/metadata v0.2.3
github.com/AlecAivazis/survey/v2 v2.3.5
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
@@ -70,8 +77,7 @@ require (
github.com/andybalholm/brotli v1.0.5
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
github.com/awalterschulze/gographviz v2.0.3+incompatible
- github.com/bep/debounce v1.2.1
- github.com/bgentry/speakeasy v0.1.0
+ github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816
github.com/bramvdbogaerde/go-scp v1.2.1-0.20221219230748-977ee74ac37b
github.com/briandowns/spinner v1.18.1
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5
@@ -80,7 +86,7 @@ require (
github.com/charmbracelet/glamour v0.6.0
// In later at least v0.7.1, lipgloss changes its terminal detection
// which breaks most of our CLI golden files tests.
- github.com/charmbracelet/lipgloss v0.7.1
+ github.com/charmbracelet/lipgloss v0.8.0
github.com/cli/safeexec v1.0.1
github.com/codeclysm/extract/v3 v3.1.1
github.com/coder/flog v1.1.0
@@ -100,7 +106,6 @@ require (
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a
github.com/gliderlabs/ssh v0.3.4
- github.com/go-chi/chi v1.5.4
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/cors v1.2.1
github.com/go-chi/httprate v0.7.1
@@ -108,15 +113,16 @@ require (
github.com/go-jose/go-jose/v3 v3.0.0
github.com/go-logr/logr v1.2.4
github.com/go-ping/ping v1.1.0
- github.com/go-playground/validator/v10 v10.14.0
+ github.com/go-playground/validator/v10 v10.15.0
github.com/gofrs/flock v0.8.1
- github.com/gohugoio/hugo v0.116.0
- github.com/golang-jwt/jwt v3.2.2+incompatible
+ github.com/gohugoio/hugo v0.117.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-migrate/migrate/v4 v4.16.0
github.com/golang/mock v1.6.0
+ github.com/google/go-cmp v0.5.9
github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405
github.com/google/uuid v1.3.0
+ github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.1
@@ -130,8 +136,8 @@ require (
github.com/jmoiron/sqlx v1.3.5
github.com/justinas/nosurf v1.1.1
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
- github.com/klauspost/compress v1.16.3
- github.com/lib/pq v1.10.6
+ github.com/klauspost/compress v1.16.5
+ github.com/lib/pq v1.10.9
github.com/mattn/go-isatty v0.0.19
github.com/mitchellh/go-wordwrap v1.0.1
github.com/mitchellh/mapstructure v1.5.0
@@ -144,7 +150,7 @@ require (
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
github.com/pkg/sftp v1.13.6-0.20221018182125-7da137aa03f0
github.com/prometheus/client_golang v1.16.0
- github.com/prometheus/client_model v0.3.0
+ github.com/prometheus/client_model v0.4.0
github.com/prometheus/common v0.42.0
github.com/quasilyte/go-ruleguard/dsl v0.3.21
github.com/robfig/cron/v3 v3.0.1
@@ -168,40 +174,42 @@ require (
go.opentelemetry.io/otel/trace v1.16.0
go.uber.org/atomic v1.11.0
go.uber.org/goleak v1.2.1
- go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf
- golang.org/x/crypto v0.11.0
- golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0
+ go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516
+ golang.org/x/crypto v0.12.0
+ golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
golang.org/x/mod v0.12.0
- golang.org/x/net v0.12.0
- golang.org/x/oauth2 v0.10.0
+ golang.org/x/net v0.14.0
+ golang.org/x/oauth2 v0.11.0
golang.org/x/sync v0.3.0
- golang.org/x/sys v0.10.0
- golang.org/x/term v0.10.0
- golang.org/x/tools v0.11.0
+ golang.org/x/sys v0.11.0
+ golang.org/x/term v0.11.0
+ golang.org/x/text v0.12.0
+ golang.org/x/time v0.3.0 // indirect
+ golang.org/x/tools v0.12.0
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
- google.golang.org/api v0.134.0
+ google.golang.org/api v0.138.0
google.golang.org/grpc v1.57.0
google.golang.org/protobuf v1.31.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
- gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0
+ gvisor.dev/gvisor v0.0.0-20230504175454-7b0a1988a28f
nhooyr.io/websocket v1.8.7
storj.io/drpc v0.0.33-0.20230420154621-9716137f6037
- tailscale.com v1.32.3
+ tailscale.com v1.46.1
)
require (
cloud.google.com/go/compute v1.23.0 // indirect
- cloud.google.com/go/logging v1.7.0 // indirect
+ cloud.google.com/go/logging v1.8.1 // indirect
cloud.google.com/go/longrunning v0.5.1 // indirect
- filippo.io/edwards25519 v1.0.0-rc.1 // indirect
+ filippo.io/edwards25519 v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
- github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
+ github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/akutz/memconn v0.1.0 // indirect
@@ -210,6 +218,19 @@ require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/armon/go-radix v1.0.0 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.20.0 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.18.32 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.13.31 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.7 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.37 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.31 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.3.38 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.31 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssm v1.37.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.13.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.21.1 // indirect
+ github.com/aws/smithy-go v1.14.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@@ -219,14 +240,14 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/charmbracelet/bubbles v0.15.0 // indirect
github.com/charmbracelet/bubbletea v0.23.2 // indirect
- github.com/clbanning/mxj/v2 v2.5.7 // indirect
+ github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/continuity v0.4.1 // indirect
github.com/coreos/go-iptables v0.6.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
- github.com/docker/cli v20.10.17+incompatible // indirect
- github.com/docker/docker v23.0.3+incompatible // indirect
+ github.com/docker/cli v23.0.5+incompatible // indirect
+ github.com/docker/docker v23.0.5+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/elastic/go-windows v1.0.0 // indirect
@@ -234,14 +255,17 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
+ github.com/go-chi/chi v1.5.4 // indirect
+ github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
- github.com/go-openapi/jsonpointer v0.19.5 // indirect
- github.com/go-openapi/jsonreference v0.20.0 // indirect
+ github.com/go-openapi/jsonpointer v0.19.6 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
- github.com/go-openapi/swag v0.19.15 // indirect
+ github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-test/deep v1.0.8 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/gobwas/glob v0.2.3 // indirect
@@ -253,19 +277,19 @@ require (
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/flatbuffers v23.1.21+incompatible // indirect
- github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-querystring v1.1.0 // indirect
- github.com/google/s2a-go v0.1.4 // indirect
+ github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c // indirect
+ github.com/google/s2a-go v0.1.5 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
github.com/gorilla/css v1.0.0 // indirect
+ github.com/gorilla/mux v1.8.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
github.com/hashicorp/go-hclog v1.2.1 // indirect
- github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/hcl/v2 v2.17.0 // indirect
@@ -273,14 +297,15 @@ require (
github.com/hashicorp/terraform-plugin-go v0.12.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.20.0 // indirect
- github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect
+ github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/illarion/gonotify v1.0.1 // indirect
- github.com/imdario/mergo v0.3.13 // indirect
- github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 // indirect
+ github.com/imdario/mergo v0.3.15 // indirect
+ github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect
- github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect
+ github.com/jsimonetti/rtnetlink v1.3.2 // indirect
github.com/juju/errors v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
@@ -292,13 +317,13 @@ require (
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
- github.com/mdlayher/genetlink v1.2.0 // indirect
- github.com/mdlayher/netlink v1.6.2 // indirect
+ github.com/mdlayher/genetlink v1.3.2 // indirect
+ github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
- github.com/mdlayher/socket v0.2.3 // indirect
+ github.com/mdlayher/socket v0.4.1 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/microcosm-cc/bluemonday v1.0.23 // indirect
- github.com/miekg/dns v1.1.45 // indirect
+ github.com/miekg/dns v1.1.55 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
@@ -313,7 +338,8 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
- github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+ github.com/pelletier/go-toml/v2 v2.0.9 // indirect
+ github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pion/transport v0.14.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -326,18 +352,18 @@ require (
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d // indirect
- github.com/tailscale/golang-x-crypto v0.0.0-20221102133106-bc99ab8c2d17 // indirect
+ github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect
- github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667 // indirect
+ github.com/tailscale/wireguard-go v0.0.0-20230710185534-bb2c8f22eccf // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
github.com/tcnksm/go-httpstat v0.2.0 // indirect
github.com/tdewolff/parse/v2 v2.6.6 // indirect
github.com/tdewolff/test v1.0.9 // indirect
- github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect
+ github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vishvananda/netlink v1.2.1-beta.2 // indirect
- github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
+ github.com/vishvananda/netns v0.0.4 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
github.com/vmihailenco/tagparser v0.1.1 // indirect
@@ -355,22 +381,15 @@ require (
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
- go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect
- golang.org/x/text v0.11.0
- golang.org/x/time v0.3.0 // indirect
+ go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20230720185612-659f7aaaa771 // indirect
+ google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
howett.net/plist v1.0.0 // indirect
inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect
)
-
-require (
- github.com/go-ini/ini v1.67.0 // indirect
- github.com/gorilla/mux v1.8.0 // indirect
-)
diff --git a/go.sum b/go.sum
index 7e721762b664b..87a117866bdf6 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-cdr.dev/slog v1.6.1 h1:IQjWZD0x6//sfv5n+qEhbu3wBkmtBQY5DILXNvMaIv4=
-cdr.dev/slog v1.6.1/go.mod h1:eHEYQLaZvxnIAXC+XdTSNLb/kgA/X2RVSF72v5wsxEI=
+cdr.dev/slog v1.6.2-0.20230817204240-b386d5d10a80 h1:CB4BlMetboYpi9FgPgvRpdRe5gkGukmhBVEcOhSvY8w=
+cdr.dev/slog v1.6.2-0.20230817204240-b386d5d10a80/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ=
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=
@@ -31,8 +31,8 @@ cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGB
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=
-cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
+cloud.google.com/go/logging v1.8.1 h1:26skQWPeYhvIasWKm48+Eq7oUqdcdbwsCVwz5Ys0FvU=
+cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI=
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@@ -46,15 +46,14 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
-filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
-filippo.io/mkcert v1.4.3 h1:axpnmtrZMM8u5Hf4N3UXxboGemMOV+Tn+e+pkHM6E3o=
+filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
+filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
+filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ=
github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
@@ -68,8 +67,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
-github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
-github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
+github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 h1:L6S7kR7SlhQKplIBpkra3s6yhcZV51lhRnXmYc4HohI=
+github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
@@ -109,6 +108,32 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E=
github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
+github.com/aws/aws-sdk-go-v2 v1.20.0 h1:INUDpYLt4oiPOJl0XwZDK2OVAVf0Rzo+MGVTv9f+gy8=
+github.com/aws/aws-sdk-go-v2 v1.20.0/go.mod h1:uWOr0m0jDsiWw8nnXiqZ+YG6LdvAlGYDLLf2NmHZoy4=
+github.com/aws/aws-sdk-go-v2/config v1.18.32 h1:tqEOvkbTxwEV7hToRcJ1xZRjcATqwDVsWbAscgRKyNI=
+github.com/aws/aws-sdk-go-v2/config v1.18.32/go.mod h1:U3ZF0fQRRA4gnbn9GGvOWLoT2EzzZfAWeKwnVrm1rDc=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.31 h1:vJyON3lG7R8VOErpJJBclBADiWTwzcwdkQpTKx8D2sk=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.31/go.mod h1:T4sESjBtY2lNxLgkIASmeP57b5j7hTQqCbqG0tWnxC4=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.7 h1:X3H6+SU21x+76LRglk21dFRgMTJMa5QcpW+SqUf5BBg=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.7/go.mod h1:3we0V09SwcJBzNlnyovrR2wWJhWmVdqAsmVs4uronv8=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.37 h1:zr/gxAZkMcvP71ZhQOcvdm8ReLjFgIXnIn0fw5AM7mo=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.37/go.mod h1:Pdn4j43v49Kk6+82spO3Tu5gSeQXRsxo56ePPQAvFiA=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.31 h1:0HCMIkAkVY9KMgueD8tf4bRTUanzEYvhw7KkPXIMpO0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.31/go.mod h1:fTJDMe8LOFYtqiFFFeHA+SVMAwqLhoq0kcInYoLa9Js=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.38 h1:+i1DOFrW3YZ3apE45tCal9+aDKK6kNEbW6Ib7e1nFxE=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.38/go.mod h1:1/jLp0OgOaWIetycOmycW+vYTYgTZFPttJQRgsI1PoU=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.31 h1:auGDJ0aLZahF5SPvkJ6WcUuX7iQ7kyl2MamV7Tm8QBk=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.31/go.mod h1:3+lloe3sZuBQw1aBc5MyndvodzQlyqCZ7x1QPDHaWP4=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.37.1 h1:8wSXZ0h+Oqwe44nBX8kW5A98pgoKaI3BpolnnpuBcOA=
+github.com/aws/aws-sdk-go-v2/service/ssm v1.37.1/go.mod h1:Z4GG8XYwKzRKKtexaeWeVmPVdwRDgh+LaR5ildi4mYQ=
+github.com/aws/aws-sdk-go-v2/service/sso v1.13.1 h1:DSNpSbfEgFXRV+IfEcKE5kTbqxm+MeF5WgyeRlsLnHY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.13.1/go.mod h1:TC9BubuFMVScIU+TLKamO6VZiYTkYoEHqlSQwAe2omw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.1 h1:hd0SKLMdOL/Sl6Z0np1PX9LeH2gqNtBe0MhTedA8MGI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.1/go.mod h1:XO/VcyoQ8nKyKfFW/3DMsRQXsfh/052tHTWmg3xBXRg=
+github.com/aws/aws-sdk-go-v2/service/sts v1.21.1 h1:pAOJj+80tC8sPVgSDHzMYD6KLWsaLQ1kZw31PTeORbs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.21.1/go.mod h1:G8SbvL0rFk4WOJroU8tKBczhsbhj2p/YY7qeJezJ3CI=
+github.com/aws/smithy-go v1.14.0 h1:+X90sB94fizKjDmwb4vyl2cTTPXTE5E2G/1mjByb0io=
+github.com/aws/smithy-go v1.14.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@@ -117,16 +142,14 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
-github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/godartsass v1.2.0 h1:E2VvQrxAHAFwbjyOIExAMmogTItSKodoKuijNrGm5yU=
github.com/bep/godartsass v1.2.0/go.mod h1:6LvK9RftsXMxGfsA0LDV12AGc4Jylnu6NgHL+Q5/pE8=
github.com/bep/godartsass/v2 v2.0.0 h1:Ruht+BpBWkpmW+yAM2dkp7RSSeN0VLaTobyW0CiSP3Y=
github.com/bep/godartsass/v2 v2.0.0/go.mod h1:AcP8QgC+OwOXEq6im0WgDRYK7scDsmZCEW62o1prQLo=
github.com/bep/golibsass v1.1.1 h1:xkaet75ygImMYjM+FnHIT3xJn7H0xBA9UxSOJjk8Khw=
github.com/bep/golibsass v1.1.1/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
-github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
-github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s=
+github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
github.com/bramvdbogaerde/go-scp v1.2.1-0.20221219230748-977ee74ac37b h1:UJeNthMS3NHVtMFKMhzZNxdaXpYqQlbLrDRtVXorT7w=
github.com/bramvdbogaerde/go-scp v1.2.1-0.20221219230748-977ee74ac37b/go.mod h1:s4ZldBoRAOgUg8IrRP2Urmq5qqd2yPXQTPshACY8vQ0=
@@ -154,17 +177,16 @@ github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM2
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
-github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
-github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
+github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU=
+github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
-github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk=
-github.com/cilium/ebpf v0.9.3 h1:5KtxXZU+scyERvkJMEm16TbScVvuuMrlhPly78ZMbSc=
-github.com/clbanning/mxj/v2 v2.5.7 h1:7q5lvUpaPF/WOkqgIDiwjBJaznaLCCBd78pi8ZyAnE0=
-github.com/clbanning/mxj/v2 v2.5.7/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
+github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ=
+github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
+github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
@@ -196,12 +218,14 @@ github.com/coder/retry v1.4.0 h1:g0fojHFxcdgM3sBULqgjFDxw1UIvaCqk4ngUDu0EWag=
github.com/coder/retry v1.4.0/go.mod h1:blHMk9vs6LkoRT9ZHyuZo360cufXEhrxqvEzeMtRGoY=
github.com/coder/ssh v0.0.0-20230621095435-9a7e23486f1c h1:TI7TzdFI0UvQmwgyQhtI1HeyYNRxAQpr8Tw/rjT8VSA=
github.com/coder/ssh v0.0.0-20230621095435-9a7e23486f1c/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ=
-github.com/coder/tailscale v0.0.0-20230731105344-d1b7f8087191 h1:FweUPlasdC67APP0jS8LTUdVlWcl49cX4NqTkr0ZL/4=
-github.com/coder/tailscale v0.0.0-20230731105344-d1b7f8087191/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA=
+github.com/coder/tailscale v1.1.1-0.20230824143504-4a17d5b8a684 h1:U1Nn5eL1gid6mOvu+L0u6t0gIB7uLV/7CFTOQNwsu6A=
+github.com/coder/tailscale v1.1.1-0.20230824143504-4a17d5b8a684/go.mod h1:L8tPrwSi31RAMEMV8rjb0vYTGs7rXt8rAHbqY/p41j4=
github.com/coder/terraform-provider-coder v0.11.1 h1:1sXcHfQrX8XhmLbtKxBED2lZ5jk3/ezBtaw6uVhpJZ4=
github.com/coder/terraform-provider-coder v0.11.1/go.mod h1:UIfU3bYNeSzJJvHyJ30tEKjD6Z9utloI+HUM/7n94CY=
github.com/coder/wgtunnel v0.1.5 h1:WP3sCj/3iJ34eKvpMQEp1oJHvm24RYh0NHbj1kfUKfs=
github.com/coder/wgtunnel v0.1.5/go.mod h1:bokoUrHnUFY4lu9KOeSYiIcHTI2MO1KwqumU4DPDyJI=
+github.com/coder/wireguard-go v0.0.0-20230807234434-d825b45ccbf5 h1:eDk/42Kj4xN4yfE504LsvcFEo3dWUiCOaBiWJ2uIH2A=
+github.com/coder/wireguard-go v0.0.0-20230807234434-d825b45ccbf5/go.mod h1:QRIcq2+DbdIC5sKh/gcAZhuqu6WT6L6G8/ALPN5wqYw=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/continuity v0.4.1 h1:wQnVrjIyQ8vhU2sgOiL5T07jo+ouqc2bnKsv5/EqGhU=
@@ -221,7 +245,7 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc=
github.com/dave/dst v0.27.2/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
-github.com/dave/jennifer v1.5.0 h1:HmgPN93bVDpkQyYbqhCHj5QlgvUkvEOzMyEvKLgCRrg=
+github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk=
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=
@@ -232,11 +256,11 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8
github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M=
-github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v23.0.5+incompatible h1:ufWmAOuD3Vmr7JP2G5K3cyuNC4YZWiAsuDEvFVVDafE=
+github.com/docker/cli v23.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
-github.com/docker/docker v23.0.3+incompatible h1:9GhVsShNWz1hO//9BNg/dpMnZW25KydO4wtVxWAIbho=
-github.com/docker/docker v23.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v23.0.5+incompatible h1:DaxtlTJjFSnLOXVNUBU1+6kXGz2lpDoEAH6QoxaSg8k=
+github.com/docker/docker v23.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
@@ -255,7 +279,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
@@ -269,9 +292,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8
github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
-github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
-github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
@@ -314,15 +336,18 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
-github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
-github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
+github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
-github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -334,10 +359,11 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
-github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
-github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
-github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-playground/validator/v10 v10.15.0 h1:nDU5XeOKtB3GEa+uB7GNYwhVKsgjAR7VgKoNB6ryXfw=
+github.com/go-playground/validator/v10 v10.15.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
@@ -363,10 +389,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/gohugoio/hugo v0.116.0 h1:hUxu+nTFBi+fkQef5HBxmpxunzqnB9e5e7g6kdj+3Oc=
-github.com/gohugoio/hugo v0.116.0/go.mod h1:Xg+aNZZj+wHiGiSVbaPAv3Y+FnpL5GXPvviQy/JrrXs=
-github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
-github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/gohugoio/hugo v0.117.0 h1:VP7MVke7R36zjUkWezg7mEtfnwHm2L5u0JwvMHvcsZQ=
+github.com/gohugoio/hugo v0.117.0/go.mod h1:b6mjGjvIqRD36x+ePNDhn1ZCXdWBkvu95+dZNTCuoDM=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-migrate/migrate/v4 v4.16.0 h1:FU2GR7EdAO0LmhNLcKthfDzuYCtMcWNR7rUbZjsgH3o=
@@ -423,6 +447,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 h1:DdHws/YnnPrSywrjNYu2lEHqYHWp/LnEx56w59esd54=
@@ -433,6 +458,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c h1:06RMfw+TMMHtRuUOroMeatRCCgSMWXCJQeABvHU69YQ=
+github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c/go.mod h1:BVIYo3cdnT4qSylnYqcd5YtmXhr51cJPGtnLBe/uLBU=
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/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -444,8 +471,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
-github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
+github.com/google/s2a-go v0.1.5 h1:8IYp3w9nysqv3JH+NJgXJzGbDHzLOTj43BmSkp+O7qg=
+github.com/google/s2a-go v0.1.5/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -458,7 +485,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
@@ -516,45 +542,42 @@ github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b57
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
-github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 h1:aSVUgRRRtOrZOC1fYmY9gV0e9z/Iu+xNVSASWjsuyGU=
-github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod h1:5PC6ZNPde8bBqU/ewGZig35+UIZtw9Ytxez8/q5ZyFE=
+github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU=
+github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
-github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE=
-github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
-github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
-github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg=
-github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE=
+github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
+github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E=
+github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI=
github.com/jedib0t/go-pretty/v6 v6.4.0 h1:YlI/2zYDrweA4MThiYMKtGRfT+2qZOO65ulej8GTcVI=
github.com/jedib0t/go-pretty/v6 v6.4.0/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
-github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
-github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
-github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
-github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
-github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b h1:Yws7RV6kZr2O7PPdT+RkbSmmOponA8i/1DuGHe8BRsM=
-github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b/go.mod h1:TzDCVOZKUa79z6iXbbXqhtAflVgUKaFkZ21M5tK5tzY=
+github.com/jsimonetti/rtnetlink v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI=
+github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
@@ -566,8 +589,8 @@ github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDS
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
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.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
-github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
+github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
@@ -598,8 +621,8 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
-github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -632,30 +655,21 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
-github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
-github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU=
-github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ=
-github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
-github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
-github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
-github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
-github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
-github.com/mdlayher/netlink v1.6.2 h1:D2zGSkvYsJ6NreeED3JiVTu1lj2sIYATqSaZlhPzUgQ=
-github.com/mdlayher/netlink v1.6.2/go.mod h1:O1HXX2sIWSMJ3Qn1BYZk1yZM+7iMki/uYGGiwGyq/iU=
-github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
-github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
+github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
+github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
+github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
+github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
-github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
-github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM=
-github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY=
+github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
+github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
-github.com/miekg/dns v1.1.45 h1:g5fRIhm9nx7g8osrAvgb16QJfmyMsyOCb+J7LSv+Qzk=
-github.com/miekg/dns v1.1.45/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
+github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
+github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
@@ -715,8 +729,11 @@ github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.m
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
-github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
-github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
+github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
+github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
@@ -738,8 +755,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
-github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
+github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
@@ -769,8 +786,6 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
@@ -796,8 +811,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/swaggest/assertjson v1.8.1 h1:Be2EHY9S2qwKWV+xWZB747Cd7Y79YK6JLdeyrgFvyMo=
@@ -812,14 +827,12 @@ github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG0
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d h1:K3j02b5j2Iw1xoggN9B2DIEkhWGheqFOeDkdJdBrJI8=
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
-github.com/tailscale/golang-x-crypto v0.0.0-20221102133106-bc99ab8c2d17 h1:cSm67hIDABvL13S0n9TNoVhzYwjb24M46znbABLll18=
-github.com/tailscale/golang-x-crypto v0.0.0-20221102133106-bc99ab8c2d17/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
+github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ=
+github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e/go.mod h1:DjoeCULdP6vTJ/xY+nzzR9LaUHprkbZEpNidX0aqEEk=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
-github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667 h1:etWp6uUwKu8NEj37K2OuMBnZ7EnVMKA7gJg5AqPFy/o=
-github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667/go.mod h1:iiClgxBTruKI+nmzlQxbFw6c3nB/wb4Td/WCyX2berY=
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/tdewolff/parse/v2 v2.6.6 h1:Yld+0CrKUJaCV78DL1G2nk3C9lKrxyRTux5aaK/AkDo=
@@ -831,8 +844,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0=
github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8=
github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY=
-github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww=
-github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E=
+github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=
+github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
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/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
@@ -850,8 +863,8 @@ github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0m
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
-github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
-github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
+github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
@@ -925,12 +938,10 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
-go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
-go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofeKGjuoUqNp1QC4=
-go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
-go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf h1:IdwJUzqoIo5lkr2EOyKoe5qipUaEjbOKKY5+fzPBZ3A=
-go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf/go.mod h1:+QXzaoURFd0rGDIjDNpyIkv+F9R7EmeKorvlKRnhqgA=
-go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
+go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
+go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
+go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 h1:X66ZEoMN2SuaoI/dfZVYobB6E5zjZyyHUMWlCA7MgGE=
+go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -947,8 +958,9 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
-golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
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=
@@ -959,9 +971,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
-golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
-golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE=
+golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI=
+golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
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=
@@ -987,6 +998,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -996,14 +1008,11 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
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-20190419010253-1f3472d942ba/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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191007182048-72f939374954/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-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -1018,7 +1027,6 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -1027,16 +1035,14 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
-golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
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=
@@ -1047,8 +1053,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
-golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
+golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
+golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
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=
@@ -1061,7 +1067,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
@@ -1069,20 +1074,15 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
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-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1105,8 +1105,6 @@ golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1131,7 +1129,6 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1145,15 +1142,18 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
-golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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=
@@ -1165,8 +1165,10 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1178,7 +1180,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
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=
@@ -1224,10 +1225,10 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
-golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
+golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1261,8 +1262,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.134.0 h1:ktL4Goua+UBgoP1eL1/60LwZJqa1sIzkLmvoR3hR6Gw=
-google.golang.org/api v0.134.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk=
+google.golang.org/api v0.138.0 h1:K/tVp05MxNVbHShRw9m7e9VJGdagNeTdMzqPH7AUqr0=
+google.golang.org/api v0.138.0/go.mod h1:4xyob8CxC+0GChNBvEUAk8VBKNvYOTWM9T3v3UfRxuY=
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=
@@ -1308,12 +1309,12 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
-google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
-google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
-google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230720185612-659f7aaaa771 h1:Z8qdAF9GFsmcUuWQ5KVYIpP3PCKydn/YKORnghIalu4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230720185612-659f7aaaa771/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
+google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g=
+google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8=
+google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 h1:nIgk/EEq3/YlnmVVXVnm14rC2oxgs1o0ong4sD/rd44=
+google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 h1:wukfNtZmZUurLN/atp2hiIeTKn7QJWIQdHzqmsOnAOk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
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=
@@ -1356,6 +1357,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
@@ -1367,7 +1369,6 @@ 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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
@@ -1378,7 +1379,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.4.2 h1:6qXr+R5w+ktL5UkwEbPp+fEvfyoMPche6GkOpGHZcLc=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
diff --git a/helm/Makefile b/helm/Makefile
index a3f689b1637af..4010cf42d64fb 100644
--- a/helm/Makefile
+++ b/helm/Makefile
@@ -13,6 +13,13 @@ all: lint
lint: lint/helm
.PHONY: lint
-lint/helm:
- helm lint --strict --set coder.image.tag=v0.0.1 .
+lint/helm: lint/helm/coder lint/helm/provisioner
.PHONY: lint/helm
+
+lint/helm/coder:
+ helm lint --strict --set coder.image.tag=v0.0.1 coder/
+.PHONY: lint/helm/coder
+
+lint/helm/provisioner:
+ helm lint --strict --set coder.image.tag=v0.0.1 provisioner/
+.PHONY: lint/helm/provisioner
diff --git a/helm/.helmignore b/helm/coder/.helmignore
similarity index 100%
rename from helm/.helmignore
rename to helm/coder/.helmignore
diff --git a/helm/coder/Chart.lock b/helm/coder/Chart.lock
new file mode 100644
index 0000000000000..9692722e192f1
--- /dev/null
+++ b/helm/coder/Chart.lock
@@ -0,0 +1,6 @@
+dependencies:
+- name: libcoder
+ repository: file://../libcoder
+ version: 0.1.0
+digest: sha256:5c9a99109258073b590a9f98268490ef387fde24c0c7c7ade9c1a8c7ef5e6e10
+generated: "2023-08-08T07:27:19.677972411Z"
diff --git a/helm/Chart.yaml b/helm/coder/Chart.yaml
similarity index 85%
rename from helm/Chart.yaml
rename to helm/coder/Chart.yaml
index a68aa330d8d49..99f6b710474c3 100644
--- a/helm/Chart.yaml
+++ b/helm/coder/Chart.yaml
@@ -21,9 +21,14 @@ keywords:
- coder
- terraform
sources:
- - https://github.com/coder/coder/tree/main/helm
+ - https://github.com/coder/coder/tree/main/helm/coder
icon: https://helm.coder.com/coder_logo_black.png
maintainers:
- name: Coder Technologies, Inc.
email: support@coder.com
url: https://coder.com/contact
+
+dependencies:
+ - name: libcoder
+ version: 0.1.0
+ repository: file://../libcoder
diff --git a/helm/README.md b/helm/coder/README.md
similarity index 100%
rename from helm/README.md
rename to helm/coder/README.md
diff --git a/helm/coder/charts/libcoder-0.1.0.tgz b/helm/coder/charts/libcoder-0.1.0.tgz
new file mode 100644
index 0000000000000..baae560bb8310
Binary files /dev/null and b/helm/coder/charts/libcoder-0.1.0.tgz differ
diff --git a/helm/templates/NOTES.txt b/helm/coder/templates/NOTES.txt
similarity index 100%
rename from helm/templates/NOTES.txt
rename to helm/coder/templates/NOTES.txt
diff --git a/helm/coder/templates/_coder.tpl b/helm/coder/templates/_coder.tpl
new file mode 100644
index 0000000000000..98a89ff5d419a
--- /dev/null
+++ b/helm/coder/templates/_coder.tpl
@@ -0,0 +1,102 @@
+{{/*
+Service account to merge into the libcoder template
+*/}}
+{{- define "coder.serviceaccount" -}}
+{{- end -}}
+
+{{/*
+Deployment to merge into the libcoder template
+*/}}
+{{- define "coder.deployment" -}}
+spec:
+ template:
+ spec:
+ containers:
+ -
+{{ include "libcoder.containerspec" (list . "coder.containerspec") | indent 8}}
+
+{{- end -}}
+
+{{/*
+ContainerSpec for the Coder container of the Coder deployment
+*/}}
+{{- define "coder.containerspec" -}}
+args:
+{{- if .Values.coder.commandArgs }}
+ {{- toYaml .Values.coder.commandArgs | nindent 12 }}
+{{- else }}
+ {{- if .Values.coder.workspaceProxy }}
+- wsproxy
+ {{- end }}
+- server
+{{- end }}
+env:
+- name: CODER_HTTP_ADDRESS
+ value: "0.0.0.0:8080"
+- name: CODER_PROMETHEUS_ADDRESS
+ value: "0.0.0.0:2112"
+{{- if .Values.provisionerDaemon.pskSecretName }}
+- name: CODER_PROVISIONER_DAEMON_PSK
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.provisionerDaemon.pskSecretName | quote }}
+ key: psk
+{{- end }}
+ # Set the default access URL so a `helm apply` works by default.
+ # See: https://github.com/coder/coder/issues/5024
+{{- $hasAccessURL := false }}
+{{- range .Values.coder.env }}
+{{- if eq .name "CODER_ACCESS_URL" }}
+{{- $hasAccessURL = true }}
+{{- end }}
+{{- end }}
+{{- if not $hasAccessURL }}
+- name: CODER_ACCESS_URL
+ value: {{ include "coder.defaultAccessURL" . | quote }}
+{{- end }}
+# Used for inter-pod communication with high-availability.
+- name: KUBE_POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+- name: CODER_DERP_SERVER_RELAY_URL
+ value: "http://$(KUBE_POD_IP):8080"
+{{- include "coder.tlsEnv" . }}
+{{- with .Values.coder.env }}
+{{ toYaml . }}
+{{- end }}
+ports:
+- name: "http"
+ containerPort: 8080
+ protocol: TCP
+ {{- if eq (include "coder.tlsEnabled" .) "true" }}
+- name: "https"
+ containerPort: 8443
+ protocol: TCP
+ {{- end }}
+ {{- range .Values.coder.env }}
+ {{- if eq .name "CODER_PROMETHEUS_ENABLE" }}
+ {{/*
+ This sadly has to be nested to avoid evaluating the second part
+ of the condition too early and potentially getting type errors if
+ the value is not a string (like a `valueFrom`). We do not support
+ `valueFrom` for this env var specifically.
+ */}}
+ {{- if eq .value "true" }}
+- name: "prometheus-http"
+ containerPort: 2112
+ protocol: TCP
+ {{- end }}
+ {{- end }}
+ {{- end }}
+readinessProbe:
+ httpGet:
+ path: /healthz
+ port: "http"
+ scheme: "HTTP"
+livenessProbe:
+ httpGet:
+ path: /healthz
+ port: "http"
+ scheme: "HTTP"
+{{- end }}
diff --git a/helm/coder/templates/coder.yaml b/helm/coder/templates/coder.yaml
new file mode 100644
index 0000000000000..65eaac00ac001
--- /dev/null
+++ b/helm/coder/templates/coder.yaml
@@ -0,0 +1,5 @@
+---
+{{ include "libcoder.serviceaccount" (list . "coder.serviceaccount") }}
+
+---
+{{ include "libcoder.deployment" (list . "coder.deployment") }}
diff --git a/helm/templates/extra-templates.yaml b/helm/coder/templates/extra-templates.yaml
similarity index 100%
rename from helm/templates/extra-templates.yaml
rename to helm/coder/templates/extra-templates.yaml
diff --git a/helm/templates/ingress.yaml b/helm/coder/templates/ingress.yaml
similarity index 100%
rename from helm/templates/ingress.yaml
rename to helm/coder/templates/ingress.yaml
diff --git a/helm/coder/templates/rbac.yaml b/helm/coder/templates/rbac.yaml
new file mode 100644
index 0000000000000..07fb36d876824
--- /dev/null
+++ b/helm/coder/templates/rbac.yaml
@@ -0,0 +1 @@
+{{ include "libcoder.rbac.tpl" . }}
diff --git a/helm/templates/service.yaml b/helm/coder/templates/service.yaml
similarity index 78%
rename from helm/templates/service.yaml
rename to helm/coder/templates/service.yaml
index 60dd5fe931dfa..1881f992a695e 100644
--- a/helm/templates/service.yaml
+++ b/helm/coder/templates/service.yaml
@@ -16,11 +16,17 @@ spec:
port: 80
targetPort: "http"
protocol: TCP
+ {{ if eq .Values.coder.service.type "NodePort" }}
+ nodePort: {{ .Values.coder.service.httpNodePort }}
+ {{ end }}
{{- if eq (include "coder.tlsEnabled" .) "true" }}
- name: "https"
port: 443
targetPort: "https"
protocol: TCP
+ {{ if eq .Values.coder.service.type "NodePort" }}
+ nodePort: {{ .Values.coder.service.httpsNodePort }}
+ {{ end }}
{{- end }}
{{- if eq "LoadBalancer" .Values.coder.service.type }}
{{- with .Values.coder.service.loadBalancerIP }}
diff --git a/helm/tests/chart_test.go b/helm/coder/tests/chart_test.go
similarity index 90%
rename from helm/tests/chart_test.go
rename to helm/coder/tests/chart_test.go
index 7442be08fc2e3..8fe4dac61508e 100644
--- a/helm/tests/chart_test.go
+++ b/helm/coder/tests/chart_test.go
@@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/testutil"
)
// These tests run `helm template` with the values file specified in each test
@@ -20,10 +20,10 @@ import (
// All values and golden files are located in the `testdata` directory.
// To update golden files, run `go test . -update`.
-// UpdateGoldenFiles is a flag that can be set to update golden files.
-var UpdateGoldenFiles = flag.Bool("update", false, "Update golden files")
+// updateGoldenFiles is a flag that can be set to update golden files.
+var updateGoldenFiles = flag.Bool("update", false, "Update golden files")
-var TestCases = []TestCase{
+var testCases = []testCase{
{
name: "default_values",
expectedError: "",
@@ -56,24 +56,28 @@ var TestCases = []TestCase{
name: "command_args",
expectedError: "",
},
+ {
+ name: "provisionerd_psk",
+ expectedError: "",
+ },
}
-type TestCase struct {
+type testCase struct {
name string // Name of the test case. This is used to control which values and golden file are used.
expectedError string // Expected error from running `helm template`.
}
-func (tc TestCase) valuesFilePath() string {
+func (tc testCase) valuesFilePath() string {
return filepath.Join("./testdata", tc.name+".yaml")
}
-func (tc TestCase) goldenFilePath() string {
+func (tc testCase) goldenFilePath() string {
return filepath.Join("./testdata", tc.name+".golden")
}
func TestRenderChart(t *testing.T) {
t.Parallel()
- if *UpdateGoldenFiles {
+ if *updateGoldenFiles {
t.Skip("Golden files are being updated. Skipping test.")
}
if testutil.InCI() {
@@ -85,7 +89,7 @@ func TestRenderChart(t *testing.T) {
// Ensure that Helm is available in $PATH
helmPath := lookupHelm(t)
- for _, tc := range TestCases {
+ for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -121,12 +125,12 @@ func TestRenderChart(t *testing.T) {
func TestUpdateGoldenFiles(t *testing.T) {
t.Parallel()
- if !*UpdateGoldenFiles {
+ if !*updateGoldenFiles {
t.Skip("Run with -update to update golden files")
}
helmPath := lookupHelm(t)
- for _, tc := range TestCases {
+ for _, tc := range testCases {
if tc.expectedError != "" {
t.Logf("skipping test case %q with render error", tc.name)
continue
diff --git a/helm/tests/testdata/command.golden b/helm/coder/tests/testdata/command.golden
similarity index 61%
rename from helm/tests/testdata/command.golden
rename to helm/coder/tests/testdata/command.golden
index 616971e98d458..4e88c36d4641d 100644
--- a/helm/tests/testdata/command.golden
+++ b/helm/coder/tests/testdata/command.golden
@@ -3,16 +3,15 @@
apiVersion: v1
kind: ServiceAccount
metadata:
- name: "coder"
- annotations:
- {}
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
---
# Source: coder/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -91,6 +90,7 @@ spec:
port: 80
targetPort: "http"
protocol: TCP
+
externalTrafficPolicy: "Cluster"
selector:
app.kubernetes.io/name: coder
@@ -100,37 +100,32 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
- name: coder
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
- annotations:
- {}
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
spec:
replicas: 1
selector:
matchLabels:
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder
template:
metadata:
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
- annotations:
- {}
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
spec:
- serviceAccountName: "coder"
- restartPolicy: Always
- terminationGracePeriodSeconds: 60
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
@@ -144,55 +139,52 @@ spec:
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- - name: coder
- image: "ghcr.io/coder/coder:latest"
- imagePullPolicy: IfNotPresent
- command:
- - /opt/colin
- args:
- - server
- resources:
- {}
- lifecycle:
- {}
- env:
- - name: CODER_HTTP_ADDRESS
- value: "0.0.0.0:8080"
- - name: CODER_PROMETHEUS_ADDRESS
- value: "0.0.0.0:2112"
- # Set the default access URL so a `helm apply` works by default.
- # See: https://github.com/coder/coder/issues/5024
- - name: CODER_ACCESS_URL
- value: "http://coder.default.svc.cluster.local"
- # Used for inter-pod communication with high-availability.
- - name: KUBE_POD_IP
- valueFrom:
- fieldRef:
- fieldPath: status.podIP
- - name: CODER_DERP_SERVER_RELAY_URL
- value: "http://$(KUBE_POD_IP):8080"
-
- ports:
- - name: "http"
- containerPort: 8080
- protocol: TCP
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: null
- runAsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- seccompProfile:
- type: RuntimeDefault
- readinessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- livenessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- volumeMounts: []
+ - args:
+ - server
+ command:
+ - /opt/colin
+ env:
+ - name: CODER_HTTP_ADDRESS
+ value: 0.0.0.0:8080
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_ACCESS_URL
+ value: http://coder.default.svc.cluster.local
+ - name: KUBE_POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: CODER_DERP_SERVER_RELAY_URL
+ value: http://$(KUBE_POD_IP):8080
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ name: coder
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder
+ terminationGracePeriodSeconds: 60
volumes: []
diff --git a/helm/tests/testdata/command.yaml b/helm/coder/tests/testdata/command.yaml
similarity index 100%
rename from helm/tests/testdata/command.yaml
rename to helm/coder/tests/testdata/command.yaml
diff --git a/helm/tests/testdata/command_args.golden b/helm/coder/tests/testdata/command_args.golden
similarity index 61%
rename from helm/tests/testdata/command_args.golden
rename to helm/coder/tests/testdata/command_args.golden
index 92e87fd58097c..9e7a9a01ee27a 100644
--- a/helm/tests/testdata/command_args.golden
+++ b/helm/coder/tests/testdata/command_args.golden
@@ -3,16 +3,15 @@
apiVersion: v1
kind: ServiceAccount
metadata:
- name: "coder"
- annotations:
- {}
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
---
# Source: coder/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -91,6 +90,7 @@ spec:
port: 80
targetPort: "http"
protocol: TCP
+
externalTrafficPolicy: "Cluster"
selector:
app.kubernetes.io/name: coder
@@ -100,37 +100,32 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
- name: coder
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
- annotations:
- {}
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
spec:
replicas: 1
selector:
matchLabels:
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder
template:
metadata:
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
- annotations:
- {}
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
spec:
- serviceAccountName: "coder"
- restartPolicy: Always
- terminationGracePeriodSeconds: 60
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
@@ -144,56 +139,53 @@ spec:
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- - name: coder
- image: "ghcr.io/coder/coder:latest"
- imagePullPolicy: IfNotPresent
- command:
- - /opt/coder
- args:
- - arg1
- - arg2
- resources:
- {}
- lifecycle:
- {}
- env:
- - name: CODER_HTTP_ADDRESS
- value: "0.0.0.0:8080"
- - name: CODER_PROMETHEUS_ADDRESS
- value: "0.0.0.0:2112"
- # Set the default access URL so a `helm apply` works by default.
- # See: https://github.com/coder/coder/issues/5024
- - name: CODER_ACCESS_URL
- value: "http://coder.default.svc.cluster.local"
- # Used for inter-pod communication with high-availability.
- - name: KUBE_POD_IP
- valueFrom:
- fieldRef:
- fieldPath: status.podIP
- - name: CODER_DERP_SERVER_RELAY_URL
- value: "http://$(KUBE_POD_IP):8080"
-
- ports:
- - name: "http"
- containerPort: 8080
- protocol: TCP
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: null
- runAsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- seccompProfile:
- type: RuntimeDefault
- readinessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- livenessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- volumeMounts: []
+ - args:
+ - arg1
+ - arg2
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_HTTP_ADDRESS
+ value: 0.0.0.0:8080
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_ACCESS_URL
+ value: http://coder.default.svc.cluster.local
+ - name: KUBE_POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: CODER_DERP_SERVER_RELAY_URL
+ value: http://$(KUBE_POD_IP):8080
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ name: coder
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder
+ terminationGracePeriodSeconds: 60
volumes: []
diff --git a/helm/tests/testdata/command_args.yaml b/helm/coder/tests/testdata/command_args.yaml
similarity index 100%
rename from helm/tests/testdata/command_args.yaml
rename to helm/coder/tests/testdata/command_args.yaml
diff --git a/helm/tests/testdata/default_values.golden b/helm/coder/tests/testdata/default_values.golden
similarity index 61%
rename from helm/tests/testdata/default_values.golden
rename to helm/coder/tests/testdata/default_values.golden
index cb1988e1ab3e9..ed02773c6f7bb 100644
--- a/helm/tests/testdata/default_values.golden
+++ b/helm/coder/tests/testdata/default_values.golden
@@ -3,16 +3,15 @@
apiVersion: v1
kind: ServiceAccount
metadata:
- name: "coder"
- annotations:
- {}
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
---
# Source: coder/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -91,6 +90,7 @@ spec:
port: 80
targetPort: "http"
protocol: TCP
+
externalTrafficPolicy: "Cluster"
selector:
app.kubernetes.io/name: coder
@@ -100,37 +100,32 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
- name: coder
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
- annotations:
- {}
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
spec:
replicas: 1
selector:
matchLabels:
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder
template:
metadata:
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
- annotations:
- {}
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
spec:
- serviceAccountName: "coder"
- restartPolicy: Always
- terminationGracePeriodSeconds: 60
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
@@ -144,55 +139,52 @@ spec:
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- - name: coder
- image: "ghcr.io/coder/coder:latest"
- imagePullPolicy: IfNotPresent
- command:
- - /opt/coder
- args:
- - server
- resources:
- {}
- lifecycle:
- {}
- env:
- - name: CODER_HTTP_ADDRESS
- value: "0.0.0.0:8080"
- - name: CODER_PROMETHEUS_ADDRESS
- value: "0.0.0.0:2112"
- # Set the default access URL so a `helm apply` works by default.
- # See: https://github.com/coder/coder/issues/5024
- - name: CODER_ACCESS_URL
- value: "http://coder.default.svc.cluster.local"
- # Used for inter-pod communication with high-availability.
- - name: KUBE_POD_IP
- valueFrom:
- fieldRef:
- fieldPath: status.podIP
- - name: CODER_DERP_SERVER_RELAY_URL
- value: "http://$(KUBE_POD_IP):8080"
-
- ports:
- - name: "http"
- containerPort: 8080
- protocol: TCP
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: null
- runAsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- seccompProfile:
- type: RuntimeDefault
- readinessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- livenessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- volumeMounts: []
+ - args:
+ - server
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_HTTP_ADDRESS
+ value: 0.0.0.0:8080
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_ACCESS_URL
+ value: http://coder.default.svc.cluster.local
+ - name: KUBE_POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: CODER_DERP_SERVER_RELAY_URL
+ value: http://$(KUBE_POD_IP):8080
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ name: coder
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder
+ terminationGracePeriodSeconds: 60
volumes: []
diff --git a/helm/tests/testdata/default_values.yaml b/helm/coder/tests/testdata/default_values.yaml
similarity index 100%
rename from helm/tests/testdata/default_values.yaml
rename to helm/coder/tests/testdata/default_values.yaml
diff --git a/helm/tests/testdata/labels_annotations.golden b/helm/coder/tests/testdata/labels_annotations.golden
similarity index 64%
rename from helm/tests/testdata/labels_annotations.golden
rename to helm/coder/tests/testdata/labels_annotations.golden
index e6f85d0dfa476..38812ffeab832 100644
--- a/helm/tests/testdata/labels_annotations.golden
+++ b/helm/coder/tests/testdata/labels_annotations.golden
@@ -3,16 +3,15 @@
apiVersion: v1
kind: ServiceAccount
metadata:
- name: "coder"
- annotations:
- {}
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
---
# Source: coder/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -91,6 +90,7 @@ spec:
port: 80
targetPort: "http"
protocol: TCP
+
externalTrafficPolicy: "Cluster"
selector:
app.kubernetes.io/name: coder
@@ -100,43 +100,40 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
- name: coder
+ annotations:
+ com.coder/annotation/baz: qux
+ com.coder/annotation/foo: bar
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
com.coder/label/baz: qux
com.coder/label/foo: bar
- annotations:
- com.coder/annotation/baz: qux
- com.coder/annotation/foo: bar
+ helm.sh/chart: coder-0.1.0
+ name: coder
spec:
replicas: 1
selector:
matchLabels:
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder
template:
metadata:
+ annotations:
+ com.coder/podAnnotation/baz: qux
+ com.coder/podAnnotation/foo: bar
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
com.coder/podLabel/baz: qux
com.coder/podLabel/foo: bar
- annotations:
- com.coder/podAnnotation/baz: qux
- com.coder/podAnnotation/foo: bar
+ helm.sh/chart: coder-0.1.0
spec:
- serviceAccountName: "coder"
- restartPolicy: Always
- terminationGracePeriodSeconds: 60
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
@@ -150,55 +147,52 @@ spec:
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- - name: coder
- image: "ghcr.io/coder/coder:latest"
- imagePullPolicy: IfNotPresent
- command:
- - /opt/coder
- args:
- - server
- resources:
- {}
- lifecycle:
- {}
- env:
- - name: CODER_HTTP_ADDRESS
- value: "0.0.0.0:8080"
- - name: CODER_PROMETHEUS_ADDRESS
- value: "0.0.0.0:2112"
- # Set the default access URL so a `helm apply` works by default.
- # See: https://github.com/coder/coder/issues/5024
- - name: CODER_ACCESS_URL
- value: "http://coder.default.svc.cluster.local"
- # Used for inter-pod communication with high-availability.
- - name: KUBE_POD_IP
- valueFrom:
- fieldRef:
- fieldPath: status.podIP
- - name: CODER_DERP_SERVER_RELAY_URL
- value: "http://$(KUBE_POD_IP):8080"
-
- ports:
- - name: "http"
- containerPort: 8080
- protocol: TCP
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: null
- runAsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- seccompProfile:
- type: RuntimeDefault
- readinessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- livenessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- volumeMounts: []
+ - args:
+ - server
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_HTTP_ADDRESS
+ value: 0.0.0.0:8080
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_ACCESS_URL
+ value: http://coder.default.svc.cluster.local
+ - name: KUBE_POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: CODER_DERP_SERVER_RELAY_URL
+ value: http://$(KUBE_POD_IP):8080
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ name: coder
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder
+ terminationGracePeriodSeconds: 60
volumes: []
diff --git a/helm/tests/testdata/labels_annotations.yaml b/helm/coder/tests/testdata/labels_annotations.yaml
similarity index 100%
rename from helm/tests/testdata/labels_annotations.yaml
rename to helm/coder/tests/testdata/labels_annotations.yaml
diff --git a/helm/tests/testdata/missing_values.yaml b/helm/coder/tests/testdata/missing_values.yaml
similarity index 100%
rename from helm/tests/testdata/missing_values.yaml
rename to helm/coder/tests/testdata/missing_values.yaml
diff --git a/helm/coder/tests/testdata/provisionerd_psk.golden b/helm/coder/tests/testdata/provisionerd_psk.golden
new file mode 100644
index 0000000000000..4dcde1eabe0fc
--- /dev/null
+++ b/helm/coder/tests/testdata/provisionerd_psk.golden
@@ -0,0 +1,195 @@
+---
+# Source: coder/templates/coder.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
+---
+# Source: coder/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-workspace-perms
+rules:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+---
+# Source: coder/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "coder"
+subjects:
+ - kind: ServiceAccount
+ name: "coder"
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-workspace-perms
+---
+# Source: coder/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: coder
+ labels:
+ helm.sh/chart: coder-0.1.0
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: "0.1.0"
+ app.kubernetes.io/managed-by: Helm
+ annotations:
+ {}
+spec:
+ type: LoadBalancer
+ sessionAffinity: ClientIP
+ ports:
+ - name: "http"
+ port: 80
+ targetPort: "http"
+ protocol: TCP
+
+ externalTrafficPolicy: "Cluster"
+ selector:
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/instance: release-name
+---
+# Source: coder/templates/coder.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder
+ template:
+ metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ spec:
+ affinity:
+ podAntiAffinity:
+ preferredDuringSchedulingIgnoredDuringExecution:
+ - podAffinityTerm:
+ labelSelector:
+ matchExpressions:
+ - key: app.kubernetes.io/instance
+ operator: In
+ values:
+ - coder
+ topologyKey: kubernetes.io/hostname
+ weight: 1
+ containers:
+ - args:
+ - server
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_HTTP_ADDRESS
+ value: 0.0.0.0:8080
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_PROVISIONER_DAEMON_PSK
+ valueFrom:
+ secretKeyRef:
+ key: psk
+ name: coder-provisionerd-psk
+ - name: CODER_ACCESS_URL
+ value: http://coder.default.svc.cluster.local
+ - name: KUBE_POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: CODER_DERP_SERVER_RELAY_URL
+ value: http://$(KUBE_POD_IP):8080
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ name: coder
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder
+ terminationGracePeriodSeconds: 60
+ volumes: []
diff --git a/helm/coder/tests/testdata/provisionerd_psk.yaml b/helm/coder/tests/testdata/provisionerd_psk.yaml
new file mode 100644
index 0000000000000..915b7aeb66f0f
--- /dev/null
+++ b/helm/coder/tests/testdata/provisionerd_psk.yaml
@@ -0,0 +1,5 @@
+coder:
+ image:
+ tag: latest
+provisionerDaemon:
+ pskSecretName: "coder-provisionerd-psk"
diff --git a/helm/tests/testdata/sa.golden b/helm/coder/tests/testdata/sa.golden
similarity index 60%
rename from helm/tests/testdata/sa.golden
rename to helm/coder/tests/testdata/sa.golden
index 5e94a67818c62..cf3b2df693835 100644
--- a/helm/tests/testdata/sa.golden
+++ b/helm/coder/tests/testdata/sa.golden
@@ -3,22 +3,22 @@
apiVersion: v1
kind: ServiceAccount
metadata:
- name: "coder-service-account"
- annotations:
+ annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/coder-service-account
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder-service-account
---
# Source: coder/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
- name: coder-workspace-perms
+ name: coder-service-account-workspace-perms
rules:
- apiGroups: [""]
resources: ["pods"]
@@ -67,7 +67,7 @@ subjects:
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
- name: coder-workspace-perms
+ name: coder-service-account-workspace-perms
---
# Source: coder/templates/service.yaml
apiVersion: v1
@@ -91,6 +91,7 @@ spec:
port: 80
targetPort: "http"
protocol: TCP
+
externalTrafficPolicy: "Cluster"
selector:
app.kubernetes.io/name: coder
@@ -100,37 +101,32 @@ spec:
apiVersion: apps/v1
kind: Deployment
metadata:
- name: coder
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
- annotations:
- {}
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
spec:
replicas: 1
selector:
matchLabels:
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder
template:
metadata:
+ annotations: {}
labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
- annotations:
- {}
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
spec:
- serviceAccountName: "coder-service-account"
- restartPolicy: Always
- terminationGracePeriodSeconds: 60
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
@@ -144,55 +140,52 @@ spec:
topologyKey: kubernetes.io/hostname
weight: 1
containers:
- - name: coder
- image: "ghcr.io/coder/coder:latest"
- imagePullPolicy: IfNotPresent
- command:
- - /opt/coder
- args:
- - server
- resources:
- {}
- lifecycle:
- {}
- env:
- - name: CODER_HTTP_ADDRESS
- value: "0.0.0.0:8080"
- - name: CODER_PROMETHEUS_ADDRESS
- value: "0.0.0.0:2112"
- # Set the default access URL so a `helm apply` works by default.
- # See: https://github.com/coder/coder/issues/5024
- - name: CODER_ACCESS_URL
- value: "http://coder.default.svc.cluster.local"
- # Used for inter-pod communication with high-availability.
- - name: KUBE_POD_IP
- valueFrom:
- fieldRef:
- fieldPath: status.podIP
- - name: CODER_DERP_SERVER_RELAY_URL
- value: "http://$(KUBE_POD_IP):8080"
-
- ports:
- - name: "http"
- containerPort: 8080
- protocol: TCP
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: null
- runAsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- seccompProfile:
- type: RuntimeDefault
- readinessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- livenessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- volumeMounts: []
+ - args:
+ - server
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_HTTP_ADDRESS
+ value: 0.0.0.0:8080
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_ACCESS_URL
+ value: http://coder.default.svc.cluster.local
+ - name: KUBE_POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: CODER_DERP_SERVER_RELAY_URL
+ value: http://$(KUBE_POD_IP):8080
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ name: coder
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder-service-account
+ terminationGracePeriodSeconds: 60
volumes: []
diff --git a/helm/tests/testdata/sa.yaml b/helm/coder/tests/testdata/sa.yaml
similarity index 100%
rename from helm/tests/testdata/sa.yaml
rename to helm/coder/tests/testdata/sa.yaml
diff --git a/helm/coder/tests/testdata/tls.golden b/helm/coder/tests/testdata/tls.golden
new file mode 100644
index 0000000000000..fccbbec0a2aa2
--- /dev/null
+++ b/helm/coder/tests/testdata/tls.golden
@@ -0,0 +1,212 @@
+---
+# Source: coder/templates/coder.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
+---
+# Source: coder/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-workspace-perms
+rules:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+---
+# Source: coder/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "coder"
+subjects:
+ - kind: ServiceAccount
+ name: "coder"
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-workspace-perms
+---
+# Source: coder/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: coder
+ labels:
+ helm.sh/chart: coder-0.1.0
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: "0.1.0"
+ app.kubernetes.io/managed-by: Helm
+ annotations:
+ {}
+spec:
+ type: LoadBalancer
+ sessionAffinity: ClientIP
+ ports:
+ - name: "http"
+ port: 80
+ targetPort: "http"
+ protocol: TCP
+
+ - name: "https"
+ port: 443
+ targetPort: "https"
+ protocol: TCP
+
+ externalTrafficPolicy: "Cluster"
+ selector:
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/instance: release-name
+---
+# Source: coder/templates/coder.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder
+ template:
+ metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ spec:
+ affinity:
+ podAntiAffinity:
+ preferredDuringSchedulingIgnoredDuringExecution:
+ - podAffinityTerm:
+ labelSelector:
+ matchExpressions:
+ - key: app.kubernetes.io/instance
+ operator: In
+ values:
+ - coder
+ topologyKey: kubernetes.io/hostname
+ weight: 1
+ containers:
+ - args:
+ - server
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_HTTP_ADDRESS
+ value: 0.0.0.0:8080
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_ACCESS_URL
+ value: https://coder.default.svc.cluster.local
+ - name: KUBE_POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: CODER_DERP_SERVER_RELAY_URL
+ value: http://$(KUBE_POD_IP):8080
+ - name: CODER_TLS_ENABLE
+ value: "true"
+ - name: CODER_TLS_ADDRESS
+ value: 0.0.0.0:8443
+ - name: CODER_TLS_CERT_FILE
+ value: /etc/ssl/certs/coder/coder-tls/tls.crt
+ - name: CODER_TLS_KEY_FILE
+ value: /etc/ssl/certs/coder/coder-tls/tls.key
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ name: coder
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ - containerPort: 8443
+ name: https
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts:
+ - mountPath: /etc/ssl/certs/coder/coder-tls
+ name: tls-coder-tls
+ readOnly: true
+ restartPolicy: Always
+ serviceAccountName: coder
+ terminationGracePeriodSeconds: 60
+ volumes:
+ - name: tls-coder-tls
+ secret:
+ secretName: coder-tls
diff --git a/helm/tests/testdata/tls.yaml b/helm/coder/tests/testdata/tls.yaml
similarity index 100%
rename from helm/tests/testdata/tls.yaml
rename to helm/coder/tests/testdata/tls.yaml
diff --git a/helm/coder/tests/testdata/workspace_proxy.golden b/helm/coder/tests/testdata/workspace_proxy.golden
new file mode 100644
index 0000000000000..096b40978aac0
--- /dev/null
+++ b/helm/coder/tests/testdata/workspace_proxy.golden
@@ -0,0 +1,198 @@
+---
+# Source: coder/templates/coder.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
+---
+# Source: coder/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-workspace-perms
+rules:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+---
+# Source: coder/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "coder"
+subjects:
+ - kind: ServiceAccount
+ name: "coder"
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-workspace-perms
+---
+# Source: coder/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: coder
+ labels:
+ helm.sh/chart: coder-0.1.0
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: "0.1.0"
+ app.kubernetes.io/managed-by: Helm
+ annotations:
+ {}
+spec:
+ type: LoadBalancer
+ sessionAffinity: ClientIP
+ ports:
+ - name: "http"
+ port: 80
+ targetPort: "http"
+ protocol: TCP
+
+ externalTrafficPolicy: "Cluster"
+ selector:
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/instance: release-name
+---
+# Source: coder/templates/coder.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ name: coder
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder
+ template:
+ metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder
+ app.kubernetes.io/part-of: coder
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-0.1.0
+ spec:
+ affinity:
+ podAntiAffinity:
+ preferredDuringSchedulingIgnoredDuringExecution:
+ - podAffinityTerm:
+ labelSelector:
+ matchExpressions:
+ - key: app.kubernetes.io/instance
+ operator: In
+ values:
+ - coder
+ topologyKey: kubernetes.io/hostname
+ weight: 1
+ containers:
+ - args:
+ - wsproxy
+ - server
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_HTTP_ADDRESS
+ value: 0.0.0.0:8080
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_ACCESS_URL
+ value: http://coder.default.svc.cluster.local
+ - name: KUBE_POD_IP
+ valueFrom:
+ fieldRef:
+ fieldPath: status.podIP
+ - name: CODER_DERP_SERVER_RELAY_URL
+ value: http://$(KUBE_POD_IP):8080
+ - name: CODER_PRIMARY_ACCESS_URL
+ value: https://dev.coder.com
+ - name: CODER_PROXY_SESSION_TOKEN
+ valueFrom:
+ secretKeyRef:
+ key: token
+ name: coder-workspace-proxy-session-token
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ name: coder
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ scheme: HTTP
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder
+ terminationGracePeriodSeconds: 60
+ volumes: []
diff --git a/helm/tests/testdata/workspace_proxy.yaml b/helm/coder/tests/testdata/workspace_proxy.yaml
similarity index 100%
rename from helm/tests/testdata/workspace_proxy.yaml
rename to helm/coder/tests/testdata/workspace_proxy.yaml
diff --git a/helm/values.yaml b/helm/coder/values.yaml
similarity index 92%
rename from helm/values.yaml
rename to helm/coder/values.yaml
index e3a1298628d5b..2b85b54e67127 100644
--- a/helm/values.yaml
+++ b/helm/coder/values.yaml
@@ -10,7 +10,7 @@ coder:
# - CODER_TLS_ENABLE: set if tls.secretName is not empty.
# - CODER_TLS_CERT_FILE: set if tls.secretName is not empty.
# - CODER_TLS_KEY_FILE: set if tls.secretName is not empty.
- # - CODER_PROMETHEUS_ADDRESS: set to 0.0.0.0:6060 and cannot be changed.
+ # - CODER_PROMETHEUS_ADDRESS: set to 0.0.0.0:2112 and cannot be changed.
# Prometheus must still be enabled by setting CODER_PROMETHEUS_ENABLE.
# - KUBE_POD_IP
# - CODER_DERP_SERVER_RELAY_URL
@@ -241,6 +241,12 @@ coder:
# coder.service.annotations -- The service annotations. See:
# https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer
annotations: {}
+ # coder.service.httpNodePort -- Enabled if coder.service.type is set to NodePort.
+ # If not set, Kubernetes will allocate a port from the default range, 30000-32767.
+ httpNodePort: ""
+ # coder.service.httpsNodePort -- Enabled if coder.service.type is set to NodePort.
+ # If not set, Kubernetes will allocate a port from the default range, 30000-32767.
+ httpsNodePort: ""
# coder.ingress -- The Ingress object to expose for Coder.
ingress:
@@ -280,6 +286,16 @@ coder:
# coder.commandArgs -- Set arguments for the entrypoint command of the Coder pod.
commandArgs: []
+# provisionerDaemon -- Configuration for external provisioner daemons.
+#
+# This is an Enterprise feature. Contact sales@coder.com.
+provisionerDaemon:
+ # provisionerDaemon.pskSecretName -- The name of the Kubernetes secret that contains the
+ # Pre-Shared Key (PSK) to use to authenticate external provisioner daemons with Coder. The
+ # secret must be in the same namespace as the Helm deployment, and contain an item called "psk"
+ # which contains the pre-shared key.
+ pskSecretName: ""
+
# extraTemplates -- Array of extra objects to deploy with the release. Strings
# are evaluated as a template and can use template expansions and functions. All
# other objects are used as yaml.
diff --git a/helm/libcoder/Chart.yaml b/helm/libcoder/Chart.yaml
new file mode 100644
index 0000000000000..90c881af5d62d
--- /dev/null
+++ b/helm/libcoder/Chart.yaml
@@ -0,0 +1,13 @@
+apiVersion: v2
+name: libcoder
+description: Coder library chart
+home: https://github.com/coder/coder
+
+type: library
+version: "0.1.0"
+appVersion: "0.1.0"
+
+maintainers:
+ - name: Coder Technologies, Inc.
+ email: support@coder.com
+ url: https://coder.com/contact
diff --git a/helm/libcoder/templates/_coder.yaml b/helm/libcoder/templates/_coder.yaml
new file mode 100644
index 0000000000000..77cdbb2a3dfe5
--- /dev/null
+++ b/helm/libcoder/templates/_coder.yaml
@@ -0,0 +1,85 @@
+{{- define "libcoder.deployment.tpl" -}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "coder.name" .}}
+ labels:
+ {{- include "coder.labels" . | nindent 4 }}
+ {{- with .Values.coder.labels }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ annotations: {{ toYaml .Values.coder.annotations | nindent 4}}
+spec:
+ replicas: {{ .Values.coder.replicaCount }}
+ selector:
+ matchLabels:
+ {{- include "coder.selectorLabels" . | nindent 6 }}
+ template:
+ metadata:
+ labels:
+ {{- include "coder.labels" . | nindent 8 }}
+ {{- with .Values.coder.podLabels }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ annotations:
+ {{- toYaml .Values.coder.podAnnotations | nindent 8 }}
+ spec:
+ serviceAccountName: {{ .Values.coder.serviceAccount.name | quote }}
+ restartPolicy: Always
+ {{- with .Values.coder.image.pullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ terminationGracePeriodSeconds: 60
+ {{- with .Values.coder.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.coder.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.coder.nodeSelector }}
+ nodeSelector:
+ {{ toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.coder.initContainers }}
+ initContainers:
+ {{ toYaml . | nindent 8 }}
+ {{- end }}
+ containers: []
+ {{- include "coder.volumes" . | nindent 6 }}
+{{- end -}}
+{{- define "libcoder.deployment" -}}
+{{- include "libcoder.util.merge" (append . "libcoder.deployment.tpl") -}}
+{{- end -}}
+
+{{- define "libcoder.containerspec.tpl" -}}
+name: coder
+image: {{ include "coder.image" . | quote }}
+imagePullPolicy: {{ .Values.coder.image.pullPolicy }}
+command:
+ {{- toYaml .Values.coder.command | nindent 2 }}
+resources:
+ {{- toYaml .Values.coder.resources | nindent 2 }}
+lifecycle:
+ {{- toYaml .Values.coder.lifecycle | nindent 2 }}
+securityContext: {{ toYaml .Values.coder.securityContext | nindent 2 }}
+{{ include "coder.volumeMounts" . }}
+{{- end -}}
+{{- define "libcoder.containerspec" -}}
+{{- include "libcoder.util.merge" (append . "libcoder.containerspec.tpl") -}}
+{{- end -}}
+
+{{- define "libcoder.serviceaccount.tpl" -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ .Values.coder.serviceAccount.name | quote }}
+ annotations: {{ toYaml .Values.coder.serviceAccount.annotations | nindent 4 }}
+ labels:
+ {{- include "coder.labels" . | nindent 4 }}
+{{- end -}}
+{{- define "libcoder.serviceaccount" -}}
+{{- include "libcoder.util.merge" (append . "libcoder.serviceaccount.tpl") -}}
+{{- end -}}
diff --git a/helm/templates/_helpers.tpl b/helm/libcoder/templates/_helpers.tpl
similarity index 93%
rename from helm/templates/_helpers.tpl
rename to helm/libcoder/templates/_helpers.tpl
index d884b28402000..9a6c5dfcfb82d 100644
--- a/helm/templates/_helpers.tpl
+++ b/helm/libcoder/templates/_helpers.tpl
@@ -49,11 +49,15 @@ Coder Docker image URI
Coder TLS enabled.
*/}}
{{- define "coder.tlsEnabled" -}}
-{{- if .Values.coder.tls.secretNames -}}
-true
-{{- else -}}
-false
-{{- end -}}
+ {{- if hasKey .Values.coder "tls" -}}
+ {{- if .Values.coder.tls.secretNames -}}
+ true
+ {{- else -}}
+ false
+ {{- end -}}
+ {{- else -}}
+ false
+ {{- end -}}
{{- end }}
{{/*
@@ -88,11 +92,13 @@ http
Coder volume definitions.
*/}}
{{- define "coder.volumeList" }}
-{{ range $secretName := .Values.coder.tls.secretNames -}}
+{{- if hasKey .Values.coder "tls" -}}
+{{- range $secretName := .Values.coder.tls.secretNames }}
- name: "tls-{{ $secretName }}"
secret:
secretName: {{ $secretName | quote }}
{{ end -}}
+{{- end }}
{{ range $secret := .Values.coder.certs.secrets -}}
- name: "ca-cert-{{ $secret.name }}"
secret:
@@ -119,11 +125,13 @@ volumes: []
Coder volume mounts.
*/}}
{{- define "coder.volumeMountList" }}
+{{- if hasKey .Values.coder "tls" }}
{{ range $secretName := .Values.coder.tls.secretNames -}}
- name: "tls-{{ $secretName }}"
mountPath: "/etc/ssl/certs/coder/{{ $secretName }}"
readOnly: true
{{ end -}}
+{{- end }}
{{ range $secret := .Values.coder.certs.secrets -}}
- name: "ca-cert-{{ $secret.name }}"
mountPath: "/etc/ssl/certs/{{ $secret.name }}.crt"
diff --git a/helm/templates/rbac.yaml b/helm/libcoder/templates/_rbac.yaml
similarity index 85%
rename from helm/templates/rbac.yaml
rename to helm/libcoder/templates/_rbac.yaml
index 3105e1a604b63..c60357ad2a796 100644
--- a/helm/templates/rbac.yaml
+++ b/helm/libcoder/templates/_rbac.yaml
@@ -1,9 +1,10 @@
+{{- define "libcoder.rbac.tpl" -}}
{{- if .Values.coder.serviceAccount.workspacePerms }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
- name: coder-workspace-perms
+ name: {{ .Values.coder.serviceAccount.name }}-workspace-perms
rules:
- apiGroups: [""]
resources: ["pods"]
@@ -53,5 +54,6 @@ subjects:
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
- name: coder-workspace-perms
+ name: {{ .Values.coder.serviceAccount.name }}-workspace-perms
{{- end }}
+{{- end -}}
diff --git a/helm/libcoder/templates/_util.yaml b/helm/libcoder/templates/_util.yaml
new file mode 100644
index 0000000000000..ebdc13e3631ec
--- /dev/null
+++ b/helm/libcoder/templates/_util.yaml
@@ -0,0 +1,13 @@
+{{- /*
+ libcoder.util.merge will merge two YAML templates and output the result.
+ This takes an array of three values:
+ - the top context
+ - the template name of the overrides (destination)
+ - the template name of the base (source)
+*/}}
+{{- define "libcoder.util.merge" -}}
+{{- $top := first . -}}
+{{- $overrides := fromYaml (include (index . 1) $top) | default (dict ) -}}
+{{- $tpl := fromYaml (include (index . 2) $top) | default (dict ) -}}
+{{- toYaml (merge $overrides $tpl) -}}
+{{- end -}}
diff --git a/helm/provisioner/Chart.lock b/helm/provisioner/Chart.lock
new file mode 100644
index 0000000000000..b51a533086d42
--- /dev/null
+++ b/helm/provisioner/Chart.lock
@@ -0,0 +1,6 @@
+dependencies:
+- name: libcoder
+ repository: file://../libcoder
+ version: 0.1.0
+digest: sha256:5c9a99109258073b590a9f98268490ef387fde24c0c7c7ade9c1a8c7ef5e6e10
+generated: "2023-08-07T12:43:45.49343898Z"
diff --git a/helm/provisioner/Chart.yaml b/helm/provisioner/Chart.yaml
new file mode 100644
index 0000000000000..de0102e1b5d41
--- /dev/null
+++ b/helm/provisioner/Chart.yaml
@@ -0,0 +1,36 @@
+apiVersion: v2
+name: coder-provisioner
+description:
+ "External provisioner daemon for Coder. This is an Enterprise feature; contact
+ sales@coder.com."
+home: https://github.com/coder/coder
+
+# version and appVersion are injected at release and will always be shown as
+# 0.1.0 in the repository.
+#
+# If you're installing the Helm chart directly from git it will have this
+# version, which means the auto-generated image URI will be invalid. You can set
+# "coder.image.tag" to the desired tag manually.
+type: application
+version: "0.1.0"
+appVersion: "0.1.0"
+
+# Coder has a hard requirement on Kubernetes 1.19, as this version introduced
+# the networking.k8s.io/v1 API.
+kubeVersion: ">= 1.19.0-0"
+
+keywords:
+ - coder
+ - terraform
+sources:
+ - https://github.com/coder/coder/tree/main/helm/provisioner
+icon: https://helm.coder.com/coder_logo_black.png
+maintainers:
+ - name: Coder Technologies, Inc.
+ email: support@coder.com
+ url: https://coder.com/contact
+
+dependencies:
+ - name: libcoder
+ version: 0.1.0
+ repository: file://../libcoder
diff --git a/helm/provisioner/README.md b/helm/provisioner/README.md
new file mode 100644
index 0000000000000..d1f8b6727fa11
--- /dev/null
+++ b/helm/provisioner/README.md
@@ -0,0 +1,36 @@
+# Coder Helm Chart
+
+This directory contains the Helm chart used to deploy Coder provisioner daemons onto a Kubernetes
+cluster.
+
+External provisioner daemons are an Enterprise feature. Contact sales@coder.com.
+
+## Getting Started
+
+> **Warning**: The main branch in this repository does not represent the
+> latest release of Coder. Please reference our installation docs for
+> instructions on a tagged release.
+
+View
+[our docs](https://coder.com/docs/v2/latest/admin/provisioners)
+for detailed installation instructions.
+
+## Values
+
+Please refer to [values.yaml](values.yaml) for available Helm values and their
+defaults.
+
+A good starting point for your values file is:
+
+```yaml
+coder:
+ env:
+ - name: CODER_URL
+ value: "https://coder.example.com"
+ # This env enables the Prometheus metrics endpoint.
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: "0.0.0.0:2112"
+ replicaCount: 10
+provisionerDaemon:
+ pskSecretName: "coder-provisioner-psk"
+```
diff --git a/helm/provisioner/charts/libcoder-0.1.0.tgz b/helm/provisioner/charts/libcoder-0.1.0.tgz
new file mode 100644
index 0000000000000..094e3f64207ad
Binary files /dev/null and b/helm/provisioner/charts/libcoder-0.1.0.tgz differ
diff --git a/helm/provisioner/templates/_coder.tpl b/helm/provisioner/templates/_coder.tpl
new file mode 100644
index 0000000000000..b84b7d8c4e48c
--- /dev/null
+++ b/helm/provisioner/templates/_coder.tpl
@@ -0,0 +1,86 @@
+{{/*
+Service account to merge into the libcoder template
+*/}}
+{{- define "coder.serviceaccount" -}}
+{{- end }}
+
+{{/*
+Deployment to merge into the libcoder template
+*/}}
+{{- define "coder.deployment" -}}
+spec:
+ template:
+ spec:
+ terminationGracePeriodSeconds: {{ .Values.provisionerDaemon.terminationGracePeriodSeconds }}
+ containers:
+ -
+{{ include "libcoder.containerspec" (list . "coder.containerspec") | indent 8}}
+
+{{- end }}
+
+{{/*
+ContainerSpec for the Coder container of the Coder deployment
+*/}}
+{{- define "coder.containerspec" -}}
+args:
+{{- if .Values.coder.commandArgs }}
+ {{- toYaml .Values.coder.commandArgs | nindent 12 }}
+{{- else }}
+- provisionerd
+- start
+{{- end }}
+env:
+- name: CODER_PROMETHEUS_ADDRESS
+ value: "0.0.0.0:2112"
+- name: CODER_PROVISIONER_DAEMON_PSK
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.provisionerDaemon.pskSecretName | quote }}
+ key: psk
+{{- if include "provisioner.tags" . }}
+- name: CODER_PROVISIONERD_TAGS
+ value: {{ include "provisioner.tags" . }}
+{{- end }}
+ # Set the default access URL so a `helm apply` works by default.
+ # See: https://github.com/coder/coder/issues/5024
+{{- $hasAccessURL := false }}
+{{- range .Values.coder.env }}
+{{- if eq .name "CODER_URL" }}
+{{- $hasAccessURL = true }}
+{{- end }}
+{{- end }}
+{{- if not $hasAccessURL }}
+- name: CODER_URL
+ value: {{ include "coder.defaultAccessURL" . | quote }}
+{{- end }}
+{{- with .Values.coder.env }}
+{{ toYaml . }}
+{{- end }}
+ports:
+ {{- range .Values.coder.env }}
+ {{- if eq .name "CODER_PROMETHEUS_ENABLE" }}
+ {{/*
+ This sadly has to be nested to avoid evaluating the second part
+ of the condition too early and potentially getting type errors if
+ the value is not a string (like a `valueFrom`). We do not support
+ `valueFrom` for this env var specifically.
+ */}}
+ {{- if eq .value "true" }}
+- name: "prometheus-http"
+ containerPort: 2112
+ protocol: TCP
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+
+{{/*
+Convert provisioner tags to the environment variable format
+*/}}
+{{- define "provisioner.tags" -}}
+ {{- $keys := keys .Values.provisionerDaemon.tags | sortAlpha -}}
+ {{- range $i, $key := $keys -}}
+ {{- $val := get $.Values.provisionerDaemon.tags $key -}}
+ {{- if ne $i 0 -}},{{- end -}}{{ $key }}={{ $val }}
+ {{- end -}}
+{{- end -}}
diff --git a/helm/provisioner/templates/coder.yaml b/helm/provisioner/templates/coder.yaml
new file mode 100644
index 0000000000000..65eaac00ac001
--- /dev/null
+++ b/helm/provisioner/templates/coder.yaml
@@ -0,0 +1,5 @@
+---
+{{ include "libcoder.serviceaccount" (list . "coder.serviceaccount") }}
+
+---
+{{ include "libcoder.deployment" (list . "coder.deployment") }}
diff --git a/helm/provisioner/templates/rbac.yaml b/helm/provisioner/templates/rbac.yaml
new file mode 100644
index 0000000000000..07fb36d876824
--- /dev/null
+++ b/helm/provisioner/templates/rbac.yaml
@@ -0,0 +1 @@
+{{ include "libcoder.rbac.tpl" . }}
diff --git a/helm/provisioner/tests/chart_test.go b/helm/provisioner/tests/chart_test.go
new file mode 100644
index 0000000000000..6e683a3601424
--- /dev/null
+++ b/helm/provisioner/tests/chart_test.go
@@ -0,0 +1,172 @@
+package tests // nolint: testpackage
+
+import (
+ "bytes"
+ "flag"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "golang.org/x/xerrors"
+
+ "github.com/coder/coder/v2/testutil"
+)
+
+// These tests run `helm template` with the values file specified in each test
+// and compare the output to the contents of the corresponding golden file.
+// All values and golden files are located in the `testdata` directory.
+// To update golden files, run `go test . -update`.
+
+// updateGoldenFiles is a flag that can be set to update golden files.
+var updateGoldenFiles = flag.Bool("update", false, "Update golden files")
+
+var testCases = []testCase{
+ {
+ name: "default_values",
+ expectedError: "",
+ },
+ {
+ name: "missing_values",
+ expectedError: `You must specify the coder.image.tag value if you're installing the Helm chart directly from Git.`,
+ },
+ {
+ name: "sa",
+ expectedError: "",
+ },
+ {
+ name: "labels_annotations",
+ expectedError: "",
+ },
+ {
+ name: "command",
+ expectedError: "",
+ },
+ {
+ name: "command_args",
+ expectedError: "",
+ },
+ {
+ name: "provisionerd_psk",
+ expectedError: "",
+ },
+}
+
+type testCase struct {
+ name string // Name of the test case. This is used to control which values and golden file are used.
+ expectedError string // Expected error from running `helm template`.
+}
+
+func (tc testCase) valuesFilePath() string {
+ return filepath.Join("./testdata", tc.name+".yaml")
+}
+
+func (tc testCase) goldenFilePath() string {
+ return filepath.Join("./testdata", tc.name+".golden")
+}
+
+func TestRenderChart(t *testing.T) {
+ t.Parallel()
+ if *updateGoldenFiles {
+ t.Skip("Golden files are being updated. Skipping test.")
+ }
+ if testutil.InCI() {
+ switch runtime.GOOS {
+ case "windows", "darwin":
+ t.Skip("Skipping tests on Windows and macOS in CI")
+ }
+ }
+
+ // Ensure that Helm is available in $PATH
+ helmPath := lookupHelm(t)
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ // Ensure that the values file exists.
+ valuesFilePath := tc.valuesFilePath()
+ if _, err := os.Stat(valuesFilePath); os.IsNotExist(err) {
+ t.Fatalf("values file %q does not exist", valuesFilePath)
+ }
+
+ // Run helm template with the values file.
+ templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesFilePath)
+ if tc.expectedError != "" {
+ require.Error(t, err, "helm template should have failed")
+ require.Contains(t, templateOutput, tc.expectedError, "helm template output should contain expected error")
+ } else {
+ require.NoError(t, err, "helm template should not have failed")
+ require.NotEmpty(t, templateOutput, "helm template output should not be empty")
+ goldenFilePath := tc.goldenFilePath()
+ goldenBytes, err := os.ReadFile(goldenFilePath)
+ require.NoError(t, err, "failed to read golden file %q", goldenFilePath)
+
+ // Remove carriage returns to make tests pass on Windows.
+ goldenBytes = bytes.Replace(goldenBytes, []byte("\r"), []byte(""), -1)
+ expected := string(goldenBytes)
+
+ require.NoError(t, err, "failed to load golden file %q")
+ require.Equal(t, expected, templateOutput)
+ }
+ })
+ }
+}
+
+func TestUpdateGoldenFiles(t *testing.T) {
+ t.Parallel()
+ if !*updateGoldenFiles {
+ t.Skip("Run with -update to update golden files")
+ }
+
+ helmPath := lookupHelm(t)
+ for _, tc := range testCases {
+ if tc.expectedError != "" {
+ t.Logf("skipping test case %q with render error", tc.name)
+ continue
+ }
+
+ valuesPath := tc.valuesFilePath()
+ templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesPath)
+
+ require.NoError(t, err, "failed to run `helm template -f %q`", valuesPath)
+
+ goldenFilePath := tc.goldenFilePath()
+ err = os.WriteFile(goldenFilePath, []byte(templateOutput), 0o644) // nolint:gosec
+ require.NoError(t, err, "failed to write golden file %q", goldenFilePath)
+ }
+ t.Log("Golden files updated. Please review the changes and commit them.")
+}
+
+// runHelmTemplate runs helm template on the given chart with the given values and
+// returns the raw output.
+func runHelmTemplate(t testing.TB, helmPath, chartDir, valuesFilePath string) (string, error) {
+ // Ensure that valuesFilePath exists
+ if _, err := os.Stat(valuesFilePath); err != nil {
+ return "", xerrors.Errorf("values file %q does not exist: %w", valuesFilePath, err)
+ }
+
+ cmd := exec.Command(helmPath, "template", chartDir, "-f", valuesFilePath, "--namespace", "default")
+ t.Logf("exec command: %v", cmd.Args)
+ out, err := cmd.CombinedOutput()
+ return string(out), err
+}
+
+// lookupHelm ensures that Helm is available in $PATH and returns the path to the
+// Helm executable.
+func lookupHelm(t testing.TB) string {
+ helmPath, err := exec.LookPath("helm")
+ if err != nil {
+ t.Fatalf("helm not found in $PATH: %v", err)
+ return ""
+ }
+ t.Logf("Using helm at %q", helmPath)
+ return helmPath
+}
+
+func TestMain(m *testing.M) {
+ flag.Parse()
+ os.Exit(m.Run())
+}
diff --git a/helm/provisioner/tests/testdata/command.golden b/helm/provisioner/tests/testdata/command.golden
new file mode 100644
index 0000000000000..39760332be082
--- /dev/null
+++ b/helm/provisioner/tests/testdata/command.golden
@@ -0,0 +1,135 @@
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-provisioner-workspace-perms
+rules:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "coder-provisioner"
+subjects:
+ - kind: ServiceAccount
+ name: "coder-provisioner"
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-provisioner-workspace-perms
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder-provisioner
+ template:
+ metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ spec:
+ containers:
+ - args:
+ - provisionerd
+ - start
+ command:
+ - /opt/colin
+ env:
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_PROVISIONER_DAEMON_PSK
+ valueFrom:
+ secretKeyRef:
+ key: psk
+ name: coder-provisioner-psk
+ - name: CODER_URL
+ value: http://coder.default.svc.cluster.local
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ name: coder
+ ports: null
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder-provisioner
+ terminationGracePeriodSeconds: 600
+ volumes: []
diff --git a/helm/provisioner/tests/testdata/command.yaml b/helm/provisioner/tests/testdata/command.yaml
new file mode 100644
index 0000000000000..ef4c8de967f1a
--- /dev/null
+++ b/helm/provisioner/tests/testdata/command.yaml
@@ -0,0 +1,5 @@
+coder:
+ image:
+ tag: latest
+ command:
+ - /opt/colin
diff --git a/helm/provisioner/tests/testdata/command_args.golden b/helm/provisioner/tests/testdata/command_args.golden
new file mode 100644
index 0000000000000..48162991f61eb
--- /dev/null
+++ b/helm/provisioner/tests/testdata/command_args.golden
@@ -0,0 +1,135 @@
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-provisioner-workspace-perms
+rules:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "coder-provisioner"
+subjects:
+ - kind: ServiceAccount
+ name: "coder-provisioner"
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-provisioner-workspace-perms
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder-provisioner
+ template:
+ metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ spec:
+ containers:
+ - args:
+ - arg1
+ - arg2
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_PROVISIONER_DAEMON_PSK
+ valueFrom:
+ secretKeyRef:
+ key: psk
+ name: coder-provisioner-psk
+ - name: CODER_URL
+ value: http://coder.default.svc.cluster.local
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ name: coder
+ ports: null
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder-provisioner
+ terminationGracePeriodSeconds: 600
+ volumes: []
diff --git a/helm/provisioner/tests/testdata/command_args.yaml b/helm/provisioner/tests/testdata/command_args.yaml
new file mode 100644
index 0000000000000..59d012aabbefb
--- /dev/null
+++ b/helm/provisioner/tests/testdata/command_args.yaml
@@ -0,0 +1,6 @@
+coder:
+ image:
+ tag: latest
+ commandArgs:
+ - arg1
+ - arg2
diff --git a/helm/provisioner/tests/testdata/default_values.golden b/helm/provisioner/tests/testdata/default_values.golden
new file mode 100644
index 0000000000000..04197fca37468
--- /dev/null
+++ b/helm/provisioner/tests/testdata/default_values.golden
@@ -0,0 +1,135 @@
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-provisioner-workspace-perms
+rules:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "coder-provisioner"
+subjects:
+ - kind: ServiceAccount
+ name: "coder-provisioner"
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-provisioner-workspace-perms
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder-provisioner
+ template:
+ metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ spec:
+ containers:
+ - args:
+ - provisionerd
+ - start
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_PROVISIONER_DAEMON_PSK
+ valueFrom:
+ secretKeyRef:
+ key: psk
+ name: coder-provisioner-psk
+ - name: CODER_URL
+ value: http://coder.default.svc.cluster.local
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ name: coder
+ ports: null
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder-provisioner
+ terminationGracePeriodSeconds: 600
+ volumes: []
diff --git a/helm/provisioner/tests/testdata/default_values.yaml b/helm/provisioner/tests/testdata/default_values.yaml
new file mode 100644
index 0000000000000..70cdc8b472215
--- /dev/null
+++ b/helm/provisioner/tests/testdata/default_values.yaml
@@ -0,0 +1,3 @@
+coder:
+ image:
+ tag: latest
diff --git a/helm/provisioner/tests/testdata/labels_annotations.golden b/helm/provisioner/tests/testdata/labels_annotations.golden
new file mode 100644
index 0000000000000..1c2d49d8c424c
--- /dev/null
+++ b/helm/provisioner/tests/testdata/labels_annotations.golden
@@ -0,0 +1,143 @@
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-provisioner-workspace-perms
+rules:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "coder-provisioner"
+subjects:
+ - kind: ServiceAccount
+ name: "coder-provisioner"
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-provisioner-workspace-perms
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations:
+ com.coder/annotation/baz: qux
+ com.coder/annotation/foo: bar
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ com.coder/label/baz: qux
+ com.coder/label/foo: bar
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder-provisioner
+ template:
+ metadata:
+ annotations:
+ com.coder/podAnnotation/baz: qux
+ com.coder/podAnnotation/foo: bar
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ com.coder/podLabel/baz: qux
+ com.coder/podLabel/foo: bar
+ helm.sh/chart: coder-provisioner-0.1.0
+ spec:
+ containers:
+ - args:
+ - provisionerd
+ - start
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_PROVISIONER_DAEMON_PSK
+ valueFrom:
+ secretKeyRef:
+ key: psk
+ name: coder-provisioner-psk
+ - name: CODER_URL
+ value: http://coder.default.svc.cluster.local
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ name: coder
+ ports: null
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder-provisioner
+ terminationGracePeriodSeconds: 600
+ volumes: []
diff --git a/helm/provisioner/tests/testdata/labels_annotations.yaml b/helm/provisioner/tests/testdata/labels_annotations.yaml
new file mode 100644
index 0000000000000..a7ddda708be79
--- /dev/null
+++ b/helm/provisioner/tests/testdata/labels_annotations.yaml
@@ -0,0 +1,15 @@
+coder:
+ image:
+ tag: latest
+ annotations:
+ com.coder/annotation/foo: bar
+ com.coder/annotation/baz: qux
+ labels:
+ com.coder/label/foo: bar
+ com.coder/label/baz: qux
+ podAnnotations:
+ com.coder/podAnnotation/foo: bar
+ com.coder/podAnnotation/baz: qux
+ podLabels:
+ com.coder/podLabel/foo: bar
+ com.coder/podLabel/baz: qux
diff --git a/helm/provisioner/tests/testdata/missing_values.yaml b/helm/provisioner/tests/testdata/missing_values.yaml
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/helm/provisioner/tests/testdata/provisionerd_psk.golden b/helm/provisioner/tests/testdata/provisionerd_psk.golden
new file mode 100644
index 0000000000000..b641ee0db37cb
--- /dev/null
+++ b/helm/provisioner/tests/testdata/provisionerd_psk.golden
@@ -0,0 +1,137 @@
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-provisioner-workspace-perms
+rules:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "coder-provisioner"
+subjects:
+ - kind: ServiceAccount
+ name: "coder-provisioner"
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-provisioner-workspace-perms
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder-provisioner
+ template:
+ metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ spec:
+ containers:
+ - args:
+ - provisionerd
+ - start
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_PROVISIONER_DAEMON_PSK
+ valueFrom:
+ secretKeyRef:
+ key: psk
+ name: coder-provisionerd-psk
+ - name: CODER_PROVISIONERD_TAGS
+ value: clusterType=k8s,location=auh
+ - name: CODER_URL
+ value: http://coder.default.svc.cluster.local
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ name: coder
+ ports: null
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder-provisioner
+ terminationGracePeriodSeconds: 600
+ volumes: []
diff --git a/helm/provisioner/tests/testdata/provisionerd_psk.yaml b/helm/provisioner/tests/testdata/provisionerd_psk.yaml
new file mode 100644
index 0000000000000..f891b007db539
--- /dev/null
+++ b/helm/provisioner/tests/testdata/provisionerd_psk.yaml
@@ -0,0 +1,8 @@
+coder:
+ image:
+ tag: latest
+provisionerDaemon:
+ pskSecretName: "coder-provisionerd-psk"
+ tags:
+ location: auh
+ clusterType: k8s
diff --git a/helm/provisioner/tests/testdata/sa.golden b/helm/provisioner/tests/testdata/sa.golden
new file mode 100644
index 0000000000000..e8f6ee3bd45dd
--- /dev/null
+++ b/helm/provisioner/tests/testdata/sa.golden
@@ -0,0 +1,136 @@
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ annotations:
+ eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/coder-service-account
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-service-account
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: coder-service-account-workspace-perms
+rules:
+ - apiGroups: [""]
+ resources: ["pods"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - create
+ - delete
+ - deletecollection
+ - get
+ - list
+ - patch
+ - update
+ - watch
+---
+# Source: coder-provisioner/templates/rbac.yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: "coder-service-account"
+subjects:
+ - kind: ServiceAccount
+ name: "coder-service-account"
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: coder-service-account-workspace-perms
+---
+# Source: coder-provisioner/templates/coder.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ name: coder-provisioner
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/name: coder-provisioner
+ template:
+ metadata:
+ annotations: {}
+ labels:
+ app.kubernetes.io/instance: release-name
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: coder-provisioner
+ app.kubernetes.io/part-of: coder-provisioner
+ app.kubernetes.io/version: 0.1.0
+ helm.sh/chart: coder-provisioner-0.1.0
+ spec:
+ containers:
+ - args:
+ - provisionerd
+ - start
+ command:
+ - /opt/coder
+ env:
+ - name: CODER_PROMETHEUS_ADDRESS
+ value: 0.0.0.0:2112
+ - name: CODER_PROVISIONER_DAEMON_PSK
+ valueFrom:
+ secretKeyRef:
+ key: psk
+ name: coder-provisioner-psk
+ - name: CODER_URL
+ value: http://coder.default.svc.cluster.local
+ image: ghcr.io/coder/coder:latest
+ imagePullPolicy: IfNotPresent
+ lifecycle: {}
+ name: coder
+ ports: null
+ resources: {}
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: null
+ runAsGroup: 1000
+ runAsNonRoot: true
+ runAsUser: 1000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts: []
+ restartPolicy: Always
+ serviceAccountName: coder-service-account
+ terminationGracePeriodSeconds: 600
+ volumes: []
diff --git a/helm/provisioner/tests/testdata/sa.yaml b/helm/provisioner/tests/testdata/sa.yaml
new file mode 100644
index 0000000000000..4e0c98c223ae1
--- /dev/null
+++ b/helm/provisioner/tests/testdata/sa.yaml
@@ -0,0 +1,8 @@
+coder:
+ image:
+ tag: latest
+ serviceAccount:
+ name: coder-service-account
+ annotations:
+ eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/coder-service-account
+ workspacePerms: true
diff --git a/helm/provisioner/values.yaml b/helm/provisioner/values.yaml
new file mode 100644
index 0000000000000..ff628dd883929
--- /dev/null
+++ b/helm/provisioner/values.yaml
@@ -0,0 +1,209 @@
+# coder -- Common configuration options.
+coder:
+ # coder.env -- The environment variables to set. These can be used to
+ # configure all aspects of Coder provisioner daemon. Please see
+ # `coder provisionerd start --help for information about what environment
+ # variables can be set.
+ # Note: The following environment variables are set by default and cannot be
+ # overridden:
+ # - CODER_PROMETHEUS_ADDRESS: set to 0.0.0.0:2112 and cannot be changed.
+ # Prometheus must still be enabled by setting CODER_PROMETHEUS_ENABLE.
+ #
+ # We will additionally set CODER_URL, if unset, to the cluster service
+ # URL.
+ env: []
+ # - name: "CODER_URL"
+ # value: "https://coder.example.com"
+
+ # coder.image -- The image to use for Coder provisioner daemon.
+ image:
+ # coder.image.repo -- The repository of the image.
+ repo: "ghcr.io/coder/coder"
+ # coder.image.tag -- The tag of the image, defaults to {{.Chart.AppVersion}}
+ # if not set. If you're using the chart directly from git, the default
+ # app version will not work and you'll need to set this value. The helm
+ # chart helpfully fails quickly in this case.
+ tag: ""
+ # coder.image.pullPolicy -- The pull policy to use for the image. See:
+ # https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy
+ pullPolicy: IfNotPresent
+ # coder.image.pullSecrets -- The secrets used for pulling the Coder image from
+ # a private registry.
+ pullSecrets: []
+ # - name: "pull-secret"
+
+ # coder.initContainers -- Init containers for the deployment. See:
+ # https://kubernetes.io/docs/concepts/workloads/pods/init-containers/
+ initContainers:
+ []
+ # - name: init-container
+ # image: busybox:1.28
+ # command: ['sh', '-c', "sleep 2"]
+
+ # coder.annotations -- The Deployment annotations. See:
+ # https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
+ annotations: {}
+
+ # coder.labels -- The Deployment labels. See:
+ # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
+ labels: {}
+
+ # coder.podAnnotations -- The Coder pod annotations. See:
+ # https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
+ podAnnotations: {}
+
+ # coder.podLabels -- The Coder pod labels. See:
+ # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
+ podLabels: {}
+
+ # coder.serviceAccount -- Configuration for the automatically created service
+ # account. Creation of the service account cannot be disabled.
+ serviceAccount:
+ # coder.serviceAccount.workspacePerms -- Whether or not to grant the
+ # service account permissions to manage workspaces. This includes
+ # permission to manage pods and persistent volume claims in the deployment
+ # namespace.
+ #
+ # It is recommended to keep this on if you are using Kubernetes templates
+ # within Coder.
+ workspacePerms: true
+ # coder.serviceAccount.enableDeployments -- Provides the service account permission
+ # to manage Kubernetes deployments.
+ enableDeployments: true
+ # coder.serviceAccount.annotations -- The Coder service account annotations.
+ annotations: {}
+ # coder.serviceAccount.name -- The service account name
+ name: coder-provisioner
+
+ # coder.securityContext -- Fields related to the container's security
+ # context (as opposed to the pod). Some fields are also present in the pod
+ # security context, in which case these values will take precedence.
+ securityContext:
+ # coder.securityContext.runAsNonRoot -- Requires that the coder container
+ # runs as an unprivileged user. If setting runAsUser to 0 (root), this
+ # will need to be set to false.
+ runAsNonRoot: true
+ # coder.securityContext.runAsUser -- Sets the user id of the container.
+ # For security reasons, we recommend using a non-root user.
+ runAsUser: 1000
+ # coder.securityContext.runAsGroup -- Sets the group id of the container.
+ # For security reasons, we recommend using a non-root group.
+ runAsGroup: 1000
+ # coder.securityContext.readOnlyRootFilesystem -- Mounts the container's
+ # root filesystem as read-only.
+ readOnlyRootFilesystem: null
+ # coder.securityContext.seccompProfile -- Sets the seccomp profile for
+ # the coder container.
+ seccompProfile:
+ type: RuntimeDefault
+ # coder.securityContext.allowPrivilegeEscalation -- Controls whether
+ # the container can gain additional privileges, such as escalating to
+ # root. It is recommended to leave this setting disabled in production.
+ allowPrivilegeEscalation: false
+
+ # coder.volumes -- A list of extra volumes to add to the Coder provisioner daemon pod.
+ volumes: []
+ # - name: "my-volume"
+ # emptyDir: {}
+
+ # coder.volumeMounts -- A list of extra volume mounts to add to the Coder provisioner daemon pod.
+ volumeMounts: []
+ # - name: "my-volume"
+ # mountPath: "/mnt/my-volume"
+
+ # coder.replicaCount -- The number of Kubernetes deployment replicas. This
+ # should only be increased if High Availability is enabled.
+ #
+ # This is an Enterprise feature. Contact sales@coder.com.
+ replicaCount: 1
+
+ # coder.lifecycle -- container lifecycle handlers for the Coder container, allowing
+ # for lifecycle events such as postStart and preStop events
+ # See: https://kubernetes.io/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/
+ lifecycle:
+ {}
+ # postStart:
+ # exec:
+ # command: ["/bin/sh", "-c", "echo postStart"]
+ # preStop:
+ # exec:
+ # command: ["/bin/sh","-c","echo preStart"]
+
+ # coder.resources -- The resources to request for Coder. These are optional
+ # and are not set by default.
+ resources:
+ {}
+ # limits:
+ # cpu: 2000m
+ # memory: 4096Mi
+ # requests:
+ # cpu: 2000m
+ # memory: 4096Mi
+
+ # coder.certs -- CA bundles to mount inside the Coder pod.
+ certs:
+ # coder.certs.secrets -- A list of CA bundle secrets to mount into the
+ # pod. The secrets should exist in the same namespace as the Helm
+ # deployment.
+ #
+ # The given key in each secret is mounted at
+ # `/etc/ssl/certs/{secret_name}.crt`.
+ secrets:
+ []
+ # - name: "my-ca-bundle"
+ # key: "ca-bundle.crt"
+
+ # coder.affinity -- Allows specifying an affinity rule for the deployment.
+ affinity:
+ {}
+ # podAntiAffinity:
+ # preferredDuringSchedulingIgnoredDuringExecution:
+ # - podAffinityTerm:
+ # labelSelector:
+ # matchExpressions:
+ # - key: app.kubernetes.io/instance
+ # operator: In
+ # values:
+ # - "coder"
+ # topologyKey: kubernetes.io/hostname
+ # weight: 1
+
+ # coder.tolerations -- Tolerations for tainted nodes.
+ # See: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
+ tolerations:
+ {}
+ # - key: "key"
+ # operator: "Equal"
+ # value: "value"
+ # effect: "NoSchedule"
+
+ # coder.nodeSelector -- Node labels for constraining coder pods to nodes.
+ # See: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
+ nodeSelector: {}
+ # kubernetes.io/os: linux
+
+ # coder.command -- The command to use when running the container. Used
+ # for customizing the location of the `coder` binary in your image.
+ command:
+ - /opt/coder
+
+ # coder.commandArgs -- Set arguments for the entrypoint command of the Coder pod.
+ commandArgs: []
+
+# provisionerDaemon -- Provisioner Daemon configuration options
+provisionerDaemon:
+ # provisionerDaemon.pskSecretName -- The name of the Kubernetes secret that contains the
+ # Pre-Shared Key (PSK) to use to authenticate with Coder. The secret must be in the same namespace
+ # as the Helm deployment, and contain an item called "psk" which contains the pre-shared key.
+ pskSecretName: "coder-provisioner-psk"
+
+ # provisionerDaemon.tags -- Tags to filter provisioner jobs by
+ tags:
+ {}
+ # location: usa
+ # provider: kubernetes
+
+ # provisionerDaemon.terminationGracePeriodSeconds -- Time in seconds that Kubernetes should wait before forcibly
+ # terminating the provisioner daemon. You should set this to be longer than your longest expected build time so that
+ # redeployments do not interrupt builds in progress.
+ terminationGracePeriodSeconds: 600
diff --git a/helm/templates/coder.yaml b/helm/templates/coder.yaml
deleted file mode 100644
index 09b284e676bc8..0000000000000
--- a/helm/templates/coder.yaml
+++ /dev/null
@@ -1,143 +0,0 @@
-{{- include "coder.verifyDeprecated" . -}}
----
-apiVersion: v1
-kind: ServiceAccount
-metadata:
- name: {{ .Values.coder.serviceAccount.name | quote }}
- annotations: {{ toYaml .Values.coder.serviceAccount.annotations | nindent 4 }}
- labels:
- {{- include "coder.labels" . | nindent 4 }}
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: coder
- labels:
- {{- include "coder.labels" . | nindent 4 }}
- {{- with .Values.coder.labels }}
- {{- toYaml . | nindent 4 }}
- {{- end }}
- annotations: {{ toYaml .Values.coder.annotations | nindent 4}}
-spec:
- replicas: {{ .Values.coder.replicaCount }}
- selector:
- matchLabels:
- {{- include "coder.selectorLabels" . | nindent 6 }}
- template:
- metadata:
- labels:
- {{- include "coder.labels" . | nindent 8 }}
- {{- with .Values.coder.podLabels }}
- {{- toYaml . | nindent 8 }}
- {{- end }}
- annotations:
- {{- toYaml .Values.coder.podAnnotations | nindent 8 }}
- spec:
- serviceAccountName: {{ .Values.coder.serviceAccount.name | quote }}
- restartPolicy: Always
- {{- with .Values.coder.image.pullSecrets }}
- imagePullSecrets:
- {{- toYaml . | nindent 8 }}
- {{- end }}
- terminationGracePeriodSeconds: 60
- {{- with .Values.coder.affinity }}
- affinity:
- {{- toYaml . | nindent 8 }}
- {{- end }}
- {{- with .Values.coder.tolerations }}
- tolerations:
- {{- toYaml . | nindent 8 }}
- {{- end }}
- {{- with .Values.coder.nodeSelector }}
- nodeSelector:
- {{ toYaml . | nindent 8 }}
- {{- end }}
- {{- with .Values.coder.initContainers }}
- initContainers:
- {{ toYaml . | nindent 8 }}
- {{- end }}
- containers:
- - name: coder
- image: {{ include "coder.image" . | quote }}
- imagePullPolicy: {{ .Values.coder.image.pullPolicy }}
- command:
- {{- toYaml .Values.coder.command | nindent 12 }}
- args:
- {{- if .Values.coder.commandArgs }}
- {{- toYaml .Values.coder.commandArgs | nindent 12 }}
- {{- else }}
- {{- if .Values.coder.workspaceProxy }}
- - wsproxy
- {{- end }}
- - server
- {{- end }}
- resources:
- {{- toYaml .Values.coder.resources | nindent 12 }}
- lifecycle:
- {{- toYaml .Values.coder.lifecycle | nindent 12 }}
- env:
- - name: CODER_HTTP_ADDRESS
- value: "0.0.0.0:8080"
- - name: CODER_PROMETHEUS_ADDRESS
- value: "0.0.0.0:2112"
- # Set the default access URL so a `helm apply` works by default.
- # See: https://github.com/coder/coder/issues/5024
- {{- $hasAccessURL := false }}
- {{- range .Values.coder.env }}
- {{- if eq .name "CODER_ACCESS_URL" }}
- {{- $hasAccessURL = true }}
- {{- end }}
- {{- end }}
- {{- if not $hasAccessURL }}
- - name: CODER_ACCESS_URL
- value: {{ include "coder.defaultAccessURL" . | quote }}
- {{- end }}
- # Used for inter-pod communication with high-availability.
- - name: KUBE_POD_IP
- valueFrom:
- fieldRef:
- fieldPath: status.podIP
- - name: CODER_DERP_SERVER_RELAY_URL
- value: "http://$(KUBE_POD_IP):8080"
- {{- include "coder.tlsEnv" . | nindent 12 }}
- {{- with .Values.coder.env -}}
- {{ toYaml . | nindent 12 }}
- {{- end }}
- ports:
- - name: "http"
- containerPort: 8080
- protocol: TCP
- {{- if eq (include "coder.tlsEnabled" .) "true" }}
- - name: "https"
- containerPort: 8443
- protocol: TCP
- {{- end }}
- {{- range .Values.coder.env }}
- {{- if eq .name "CODER_PROMETHEUS_ENABLE" }}
- {{/*
- This sadly has to be nested to avoid evaluating the second part
- of the condition too early and potentially getting type errors if
- the value is not a string (like a `valueFrom`). We do not support
- `valueFrom` for this env var specifically.
- */}}
- {{- if eq .value "true" }}
- - name: "prometheus-http"
- containerPort: 2112
- protocol: TCP
- {{- end }}
- {{- end }}
- {{- end }}
- securityContext: {{ toYaml .Values.coder.securityContext | nindent 12 }}
- readinessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- livenessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- {{- include "coder.volumeMounts" . | nindent 10 }}
-
- {{- include "coder.volumes" . | nindent 6 }}
diff --git a/helm/tests/testdata/tls.golden b/helm/tests/testdata/tls.golden
deleted file mode 100644
index 8ef85d138f722..0000000000000
--- a/helm/tests/testdata/tls.golden
+++ /dev/null
@@ -1,220 +0,0 @@
----
-# Source: coder/templates/coder.yaml
-apiVersion: v1
-kind: ServiceAccount
-metadata:
- name: "coder"
- annotations:
- {}
- labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
- app.kubernetes.io/managed-by: Helm
----
-# Source: coder/templates/rbac.yaml
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
- name: coder-workspace-perms
-rules:
- - apiGroups: [""]
- resources: ["pods"]
- verbs:
- - create
- - delete
- - deletecollection
- - get
- - list
- - patch
- - update
- - watch
- - apiGroups: [""]
- resources: ["persistentvolumeclaims"]
- verbs:
- - create
- - delete
- - deletecollection
- - get
- - list
- - patch
- - update
- - watch
- - apiGroups:
- - apps
- resources:
- - deployments
- verbs:
- - create
- - delete
- - deletecollection
- - get
- - list
- - patch
- - update
- - watch
----
-# Source: coder/templates/rbac.yaml
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
- name: "coder"
-subjects:
- - kind: ServiceAccount
- name: "coder"
-roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: Role
- name: coder-workspace-perms
----
-# Source: coder/templates/service.yaml
-apiVersion: v1
-kind: Service
-metadata:
- name: coder
- labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
- app.kubernetes.io/managed-by: Helm
- annotations:
- {}
-spec:
- type: LoadBalancer
- sessionAffinity: ClientIP
- ports:
- - name: "http"
- port: 80
- targetPort: "http"
- protocol: TCP
- - name: "https"
- port: 443
- targetPort: "https"
- protocol: TCP
- externalTrafficPolicy: "Cluster"
- selector:
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
----
-# Source: coder/templates/coder.yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: coder
- labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
- app.kubernetes.io/managed-by: Helm
- annotations:
- {}
-spec:
- replicas: 1
- selector:
- matchLabels:
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- template:
- metadata:
- labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
- app.kubernetes.io/managed-by: Helm
- annotations:
- {}
- spec:
- serviceAccountName: "coder"
- restartPolicy: Always
- terminationGracePeriodSeconds: 60
- affinity:
- podAntiAffinity:
- preferredDuringSchedulingIgnoredDuringExecution:
- - podAffinityTerm:
- labelSelector:
- matchExpressions:
- - key: app.kubernetes.io/instance
- operator: In
- values:
- - coder
- topologyKey: kubernetes.io/hostname
- weight: 1
- containers:
- - name: coder
- image: "ghcr.io/coder/coder:latest"
- imagePullPolicy: IfNotPresent
- command:
- - /opt/coder
- args:
- - server
- resources:
- {}
- lifecycle:
- {}
- env:
- - name: CODER_HTTP_ADDRESS
- value: "0.0.0.0:8080"
- - name: CODER_PROMETHEUS_ADDRESS
- value: "0.0.0.0:2112"
- # Set the default access URL so a `helm apply` works by default.
- # See: https://github.com/coder/coder/issues/5024
- - name: CODER_ACCESS_URL
- value: "https://coder.default.svc.cluster.local"
- # Used for inter-pod communication with high-availability.
- - name: KUBE_POD_IP
- valueFrom:
- fieldRef:
- fieldPath: status.podIP
- - name: CODER_DERP_SERVER_RELAY_URL
- value: "http://$(KUBE_POD_IP):8080"
-
- - name: CODER_TLS_ENABLE
- value: "true"
- - name: CODER_TLS_ADDRESS
- value: "0.0.0.0:8443"
- - name: CODER_TLS_CERT_FILE
- value: "/etc/ssl/certs/coder/coder-tls/tls.crt"
- - name: CODER_TLS_KEY_FILE
- value: "/etc/ssl/certs/coder/coder-tls/tls.key"
- ports:
- - name: "http"
- containerPort: 8080
- protocol: TCP
- - name: "https"
- containerPort: 8443
- protocol: TCP
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: null
- runAsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- seccompProfile:
- type: RuntimeDefault
- readinessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- livenessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- volumeMounts:
- - name: "tls-coder-tls"
- mountPath: "/etc/ssl/certs/coder/coder-tls"
- readOnly: true
-
- volumes:
- - name: "tls-coder-tls"
- secret:
- secretName: "coder-tls"
diff --git a/helm/tests/testdata/workspace_proxy.golden b/helm/tests/testdata/workspace_proxy.golden
deleted file mode 100644
index 88e0213be559d..0000000000000
--- a/helm/tests/testdata/workspace_proxy.golden
+++ /dev/null
@@ -1,206 +0,0 @@
----
-# Source: coder/templates/coder.yaml
-apiVersion: v1
-kind: ServiceAccount
-metadata:
- name: "coder"
- annotations:
- {}
- labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
- app.kubernetes.io/managed-by: Helm
----
-# Source: coder/templates/rbac.yaml
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
- name: coder-workspace-perms
-rules:
- - apiGroups: [""]
- resources: ["pods"]
- verbs:
- - create
- - delete
- - deletecollection
- - get
- - list
- - patch
- - update
- - watch
- - apiGroups: [""]
- resources: ["persistentvolumeclaims"]
- verbs:
- - create
- - delete
- - deletecollection
- - get
- - list
- - patch
- - update
- - watch
- - apiGroups:
- - apps
- resources:
- - deployments
- verbs:
- - create
- - delete
- - deletecollection
- - get
- - list
- - patch
- - update
- - watch
----
-# Source: coder/templates/rbac.yaml
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
- name: "coder"
-subjects:
- - kind: ServiceAccount
- name: "coder"
-roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: Role
- name: coder-workspace-perms
----
-# Source: coder/templates/service.yaml
-apiVersion: v1
-kind: Service
-metadata:
- name: coder
- labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
- app.kubernetes.io/managed-by: Helm
- annotations:
- {}
-spec:
- type: LoadBalancer
- sessionAffinity: ClientIP
- ports:
- - name: "http"
- port: 80
- targetPort: "http"
- protocol: TCP
- externalTrafficPolicy: "Cluster"
- selector:
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
----
-# Source: coder/templates/coder.yaml
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: coder
- labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
- app.kubernetes.io/managed-by: Helm
- annotations:
- {}
-spec:
- replicas: 1
- selector:
- matchLabels:
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- template:
- metadata:
- labels:
- helm.sh/chart: coder-0.1.0
- app.kubernetes.io/name: coder
- app.kubernetes.io/instance: release-name
- app.kubernetes.io/part-of: coder
- app.kubernetes.io/version: "0.1.0"
- app.kubernetes.io/managed-by: Helm
- annotations:
- {}
- spec:
- serviceAccountName: "coder"
- restartPolicy: Always
- terminationGracePeriodSeconds: 60
- affinity:
- podAntiAffinity:
- preferredDuringSchedulingIgnoredDuringExecution:
- - podAffinityTerm:
- labelSelector:
- matchExpressions:
- - key: app.kubernetes.io/instance
- operator: In
- values:
- - coder
- topologyKey: kubernetes.io/hostname
- weight: 1
- containers:
- - name: coder
- image: "ghcr.io/coder/coder:latest"
- imagePullPolicy: IfNotPresent
- command:
- - /opt/coder
- args:
- - wsproxy
- - server
- resources:
- {}
- lifecycle:
- {}
- env:
- - name: CODER_HTTP_ADDRESS
- value: "0.0.0.0:8080"
- - name: CODER_PROMETHEUS_ADDRESS
- value: "0.0.0.0:2112"
- # Set the default access URL so a `helm apply` works by default.
- # See: https://github.com/coder/coder/issues/5024
- - name: CODER_ACCESS_URL
- value: "http://coder.default.svc.cluster.local"
- # Used for inter-pod communication with high-availability.
- - name: KUBE_POD_IP
- valueFrom:
- fieldRef:
- fieldPath: status.podIP
- - name: CODER_DERP_SERVER_RELAY_URL
- value: "http://$(KUBE_POD_IP):8080"
-
- - name: CODER_PRIMARY_ACCESS_URL
- value: https://dev.coder.com
- - name: CODER_PROXY_SESSION_TOKEN
- valueFrom:
- secretKeyRef:
- key: token
- name: coder-workspace-proxy-session-token
- ports:
- - name: "http"
- containerPort: 8080
- protocol: TCP
- securityContext:
- allowPrivilegeEscalation: false
- readOnlyRootFilesystem: null
- runAsGroup: 1000
- runAsNonRoot: true
- runAsUser: 1000
- seccompProfile:
- type: RuntimeDefault
- readinessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- livenessProbe:
- httpGet:
- path: /healthz
- port: "http"
- scheme: "HTTP"
- volumeMounts: []
- volumes: []
diff --git a/package.json b/package.json
index 356cfea31f613..6e6e1f420b57a 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,14 @@
{
+ "_comment": "This version doesn't matter, it's just to allow importing from other repos.",
+ "name": "coder",
+ "version": "0.0.0",
"scripts": {
"format:write:only": "pnpm exec prettier --write"
},
"devDependencies": {
"prettier": "3.0.0"
+ },
+ "dependencies": {
+ "exec": "^0.2.1"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 34c73146883c8..e5e4d2584e40f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,11 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+dependencies:
+ exec:
+ specifier: ^0.2.1
+ version: 0.2.1
+
devDependencies:
prettier:
specifier: 3.0.0
@@ -11,6 +16,12 @@ devDependencies:
packages:
+ /exec@0.2.1:
+ resolution: {integrity: sha512-lE5ZlJgRYh+rmwidatL2AqRA/U9IBoCpKlLriBmnfUIrV/Rj4oLjb63qZ57iBCHWi5j9IjLt5wOWkFYPiTfYAg==}
+ engines: {node: '>= v0.9.1'}
+ deprecated: deprecated in favor of builtin child_process.execFile
+ dev: false
+
/prettier@3.0.0:
resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==}
engines: {node: '>=14'}
diff --git a/provisioner/appslug_test.go b/provisioner/appslug_test.go
index 2fbd3f08ea1cd..f13f220e9c63c 100644
--- a/provisioner/appslug_test.go
+++ b/provisioner/appslug_test.go
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/provisioner"
+ "github.com/coder/coder/v2/provisioner"
)
func TestValidAppSlugRegex(t *testing.T) {
diff --git a/provisioner/echo/serve.go b/provisioner/echo/serve.go
index 6df498a9af495..9e22157bc381b 100644
--- a/provisioner/echo/serve.go
+++ b/provisioner/echo/serve.go
@@ -5,25 +5,24 @@ import (
"bytes"
"context"
"fmt"
+ "os"
"path/filepath"
"strings"
+ "github.com/google/uuid"
"golang.org/x/xerrors"
protobuf "google.golang.org/protobuf/proto"
- "github.com/google/uuid"
- "github.com/spf13/afero"
-
- "github.com/coder/coder/provisionersdk"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
// ProvisionApplyWithAgent returns provision responses that will mock a fake
// "aws_instance" resource with an agent that has the given auth token.
-func ProvisionApplyWithAgent(authToken string) []*proto.Provision_Response {
- return []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+func ProvisionApplyWithAgent(authToken string) []*proto.Response {
+ return []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -42,23 +41,36 @@ func ProvisionApplyWithAgent(authToken string) []*proto.Provision_Response {
var (
// ParseComplete is a helper to indicate an empty parse completion.
- ParseComplete = []*proto.Parse_Response{{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{},
+ ParseComplete = []*proto.Response{{
+ Type: &proto.Response_Parse{
+ Parse: &proto.ParseComplete{},
},
}}
- // ProvisionComplete is a helper to indicate an empty provision completion.
- ProvisionComplete = []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
+ // PlanComplete is a helper to indicate an empty provision completion.
+ PlanComplete = []*proto.Response{{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{},
+ },
+ }}
+ // ApplyComplete is a helper to indicate an empty provision completion.
+ ApplyComplete = []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{},
},
}}
- // ProvisionFailed is a helper to convey a failed provision
- // operation.
- ProvisionFailed = []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ // PlanFailed is a helper to convey a failed plan operation
+ PlanFailed = []*proto.Response{{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Error: "failed!",
+ },
+ },
+ }}
+ // ApplyFailed is a helper to convey a failed apply operation
+ ApplyFailed = []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Error: "failed!",
},
},
@@ -66,179 +78,219 @@ var (
)
// Serve starts the echo provisioner.
-func Serve(ctx context.Context, filesystem afero.Fs, options *provisionersdk.ServeOptions) error {
- return provisionersdk.Serve(ctx, &echo{
- filesystem: filesystem,
- }, options)
+func Serve(ctx context.Context, options *provisionersdk.ServeOptions) error {
+ return provisionersdk.Serve(ctx, &echo{}, options)
}
// The echo provisioner serves as a dummy provisioner primarily
// used for testing. It echos responses from JSON files in the
// format %d.protobuf. It's used for testing.
-type echo struct {
- filesystem afero.Fs
-}
+type echo struct{}
-// Parse reads requests from the provided directory to stream responses.
-func (e *echo) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_ParseStream) error {
- for index := 0; ; index++ {
- path := filepath.Join(request.Directory, fmt.Sprintf("%d.parse.protobuf", index))
- _, err := e.filesystem.Stat(path)
- if err != nil {
- if index == 0 {
- // Error if nothing is around to enable failed states.
- return xerrors.Errorf("no state: %w", err)
+func readResponses(sess *provisionersdk.Session, trans string, suffix string) ([]*proto.Response, error) {
+ var responses []*proto.Response
+ for i := 0; ; i++ {
+ paths := []string{
+ // Try more specific path first, then fallback to generic.
+ filepath.Join(sess.WorkDirectory, fmt.Sprintf("%d.%s.%s", i, trans, suffix)),
+ filepath.Join(sess.WorkDirectory, fmt.Sprintf("%d.%s", i, suffix)),
+ }
+ for pathIndex, path := range paths {
+ _, err := os.Stat(path)
+ if err != nil && pathIndex == (len(paths)-1) {
+ // If there are zero messages, something is wrong
+ if i == 0 {
+ // Error if nothing is around to enable failed states.
+ return nil, xerrors.Errorf("no state: %w", err)
+ }
+ // Otherwise, we've read all responses
+ return responses, nil
+ }
+ if err != nil {
+ // try next path
+ continue
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, xerrors.Errorf("read file %q: %w", path, err)
}
+ response := new(proto.Response)
+ err = protobuf.Unmarshal(data, response)
+ if err != nil {
+ return nil, xerrors.Errorf("unmarshal: %w", err)
+ }
+ responses = append(responses, response)
break
}
- data, err := afero.ReadFile(e.filesystem, path)
- if err != nil {
- return xerrors.Errorf("read file %q: %w", path, err)
- }
- var response proto.Parse_Response
- err = protobuf.Unmarshal(data, &response)
- if err != nil {
- return xerrors.Errorf("unmarshal: %w", err)
- }
- err = stream.Send(&response)
- if err != nil {
- return err
- }
}
- <-stream.Context().Done()
- return stream.Context().Err()
}
-// Provision reads requests from the provided directory to stream responses.
-func (e *echo) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
- msg, err := stream.Recv()
+// Parse reads requests from the provided directory to stream responses.
+func (*echo) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete {
+ responses, err := readResponses(sess, "unspecified", "parse.protobuf")
if err != nil {
- return err
+ return &proto.ParseComplete{Error: err.Error()}
}
-
- var config *proto.Provision_Config
- switch {
- case msg.GetPlan() != nil:
- config = msg.GetPlan().GetConfig()
- case msg.GetApply() != nil:
- config = msg.GetApply().GetConfig()
- default:
- // Probably a cancel
- return nil
- }
-
- for index := 0; ; index++ {
- var extension string
- if msg.GetPlan() != nil {
- extension = ".plan.protobuf"
- } else {
- extension = ".apply.protobuf"
- }
- path := filepath.Join(config.Directory, fmt.Sprintf("%d.provision"+extension, index))
- _, err := e.filesystem.Stat(path)
- if err != nil {
- if index == 0 {
- // Error if nothing is around to enable failed states.
- return xerrors.New("no state")
- }
- break
+ for _, response := range responses {
+ if log := response.GetLog(); log != nil {
+ sess.ProvisionLog(log.Level, log.Output)
}
- data, err := afero.ReadFile(e.filesystem, path)
- if err != nil {
- return xerrors.Errorf("read file %q: %w", path, err)
+ if complete := response.GetParse(); complete != nil {
+ return complete
}
- var response proto.Provision_Response
- err = protobuf.Unmarshal(data, &response)
- if err != nil {
- return xerrors.Errorf("unmarshal: %w", err)
+ }
+
+ // if we didn't get a complete from the filesystem, that's an error
+ return provisionersdk.ParseErrorf("complete response missing")
+}
+
+// Plan reads requests from the provided directory to stream responses.
+func (*echo) Plan(sess *provisionersdk.Session, req *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete {
+ responses, err := readResponses(
+ sess,
+ strings.ToLower(req.GetMetadata().GetWorkspaceTransition().String()),
+ "plan.protobuf")
+ if err != nil {
+ return &proto.PlanComplete{Error: err.Error()}
+ }
+ for _, response := range responses {
+ if log := response.GetLog(); log != nil {
+ sess.ProvisionLog(log.Level, log.Output)
}
- r, ok := filterLogResponses(config, &response)
- if !ok {
- continue
+ if complete := response.GetPlan(); complete != nil {
+ return complete
}
+ }
- err = stream.Send(r)
- if err != nil {
- return err
+ // some tests use Echo without a complete response to test cancel
+ <-canceledOrComplete
+ return provisionersdk.PlanErrorf("canceled")
+}
+
+// Apply reads requests from the provided directory to stream responses.
+func (*echo) Apply(sess *provisionersdk.Session, req *proto.ApplyRequest, canceledOrComplete <-chan struct{}) *proto.ApplyComplete {
+ responses, err := readResponses(
+ sess,
+ strings.ToLower(req.GetMetadata().GetWorkspaceTransition().String()),
+ "apply.protobuf")
+ if err != nil {
+ return &proto.ApplyComplete{Error: err.Error()}
+ }
+ for _, response := range responses {
+ if log := response.GetLog(); log != nil {
+ sess.ProvisionLog(log.Level, log.Output)
+ }
+ if complete := response.GetApply(); complete != nil {
+ return complete
}
}
- <-stream.Context().Done()
- return stream.Context().Err()
+
+ // some tests use Echo without a complete response to test cancel
+ <-canceledOrComplete
+ return provisionersdk.ApplyErrorf("canceled")
}
func (*echo) Shutdown(_ context.Context, _ *proto.Empty) (*proto.Empty, error) {
return &proto.Empty{}, nil
}
+// Responses is a collection of mocked responses to Provision operations.
type Responses struct {
- Parse []*proto.Parse_Response
- ProvisionApply []*proto.Provision_Response
- ProvisionPlan []*proto.Provision_Response
+ Parse []*proto.Response
+
+ // ProvisionApply and ProvisionPlan are used to mock ALL responses of
+ // Apply and Plan, regardless of transition.
+ ProvisionApply []*proto.Response
+ ProvisionPlan []*proto.Response
+
+ // ProvisionApplyMap and ProvisionPlanMap are used to mock specific
+ // transition responses. They are prioritized over the generic responses.
+ ProvisionApplyMap map[proto.WorkspaceTransition][]*proto.Response
+ ProvisionPlanMap map[proto.WorkspaceTransition][]*proto.Response
}
// Tar returns a tar archive of responses to provisioner operations.
func Tar(responses *Responses) ([]byte, error) {
if responses == nil {
- responses = &Responses{ParseComplete, ProvisionComplete, ProvisionComplete}
+ responses = &Responses{
+ ParseComplete, ApplyComplete, PlanComplete,
+ nil, nil,
+ }
}
if responses.ProvisionPlan == nil {
- responses.ProvisionPlan = responses.ProvisionApply
+ for _, resp := range responses.ProvisionApply {
+ if resp.GetLog() != nil {
+ responses.ProvisionPlan = append(responses.ProvisionPlan, resp)
+ continue
+ }
+ responses.ProvisionPlan = append(responses.ProvisionPlan, &proto.Response{
+ Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
+ Error: resp.GetApply().GetError(),
+ Resources: resp.GetApply().GetResources(),
+ Parameters: resp.GetApply().GetParameters(),
+ GitAuthProviders: resp.GetApply().GetGitAuthProviders(),
+ }},
+ })
+ }
}
var buffer bytes.Buffer
writer := tar.NewWriter(&buffer)
- for index, response := range responses.Parse {
- data, err := protobuf.Marshal(response)
+
+ writeProto := func(name string, message protobuf.Message) error {
+ data, err := protobuf.Marshal(message)
if err != nil {
- return nil, err
+ return err
}
+
err = writer.WriteHeader(&tar.Header{
- Name: fmt.Sprintf("%d.parse.protobuf", index),
+ Name: name,
Size: int64(len(data)),
Mode: 0o644,
})
if err != nil {
- return nil, err
+ return err
}
+
_, err = writer.Write(data)
if err != nil {
- return nil, err
+ return err
}
+
+ return nil
}
- for index, response := range responses.ProvisionApply {
- data, err := protobuf.Marshal(response)
- if err != nil {
- return nil, err
- }
- err = writer.WriteHeader(&tar.Header{
- Name: fmt.Sprintf("%d.provision.apply.protobuf", index),
- Size: int64(len(data)),
- Mode: 0o644,
- })
+ for index, response := range responses.Parse {
+ err := writeProto(fmt.Sprintf("%d.parse.protobuf", index), response)
if err != nil {
return nil, err
}
- _, err = writer.Write(data)
+ }
+ for index, response := range responses.ProvisionApply {
+ err := writeProto(fmt.Sprintf("%d.apply.protobuf", index), response)
if err != nil {
return nil, err
}
}
for index, response := range responses.ProvisionPlan {
- data, err := protobuf.Marshal(response)
+ err := writeProto(fmt.Sprintf("%d.plan.protobuf", index), response)
if err != nil {
return nil, err
}
- err = writer.WriteHeader(&tar.Header{
- Name: fmt.Sprintf("%d.provision.plan.protobuf", index),
- Size: int64(len(data)),
- Mode: 0o644,
- })
- if err != nil {
- return nil, err
+ }
+ for trans, m := range responses.ProvisionApplyMap {
+ for i, rs := range m {
+ err := writeProto(fmt.Sprintf("%d.%s.apply.protobuf", i, strings.ToLower(trans.String())), rs)
+ if err != nil {
+ return nil, err
+ }
}
- _, err = writer.Write(data)
- if err != nil {
- return nil, err
+ }
+ for trans, m := range responses.ProvisionPlanMap {
+ for i, rs := range m {
+ err := writeProto(fmt.Sprintf("%d.%s.plan.protobuf", i, strings.ToLower(trans.String())), rs)
+ if err != nil {
+ return nil, err
+ }
}
}
err := writer.Flush()
@@ -248,22 +300,14 @@ func Tar(responses *Responses) ([]byte, error) {
return buffer.Bytes(), nil
}
-func filterLogResponses(config *proto.Provision_Config, response *proto.Provision_Response) (*proto.Provision_Response, bool) {
- responseLog, ok := response.Type.(*proto.Provision_Response_Log)
- if !ok {
- // Pass all non-log responses
- return response, true
- }
-
- if config.ProvisionerLogLevel == "" {
- // Don't change the default behavior of "echo"
- return response, true
- }
-
- provisionerLogLevel := proto.LogLevel_value[strings.ToUpper(config.ProvisionerLogLevel)]
- if int32(responseLog.Log.Level) < provisionerLogLevel {
- // Log level is not enabled
- return nil, false
+func WithResources(resources []*proto.Resource) *Responses {
+ return &Responses{
+ Parse: ParseComplete,
+ ProvisionApply: []*proto.Response{{Type: &proto.Response_Apply{Apply: &proto.ApplyComplete{
+ Resources: resources,
+ }}}},
+ ProvisionPlan: []*proto.Response{{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
+ Resources: resources,
+ }}}},
}
- return response, true
}
diff --git a/provisioner/echo/serve_test.go b/provisioner/echo/serve_test.go
index d240b08e7a36e..6590f2ecafc54 100644
--- a/provisioner/echo/serve_test.go
+++ b/provisioner/echo/serve_test.go
@@ -1,27 +1,23 @@
package echo_test
import (
- "archive/tar"
- "bytes"
"context"
- "io"
- "os"
- "path/filepath"
"testing"
- "github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func TestEcho(t *testing.T) {
t.Parallel()
- fs := afero.NewMemMapFs()
+ workdir := t.TempDir()
+
// Create an in-memory provisioner to communicate with.
client, server := provisionersdk.MemTransportPipe()
ctx, cancelFunc := context.WithCancel(context.Background())
@@ -31,8 +27,9 @@ func TestEcho(t *testing.T) {
cancelFunc()
})
go func() {
- err := echo.Serve(ctx, fs, &provisionersdk.ServeOptions{
- Listener: server,
+ err := echo.Serve(ctx, &provisionersdk.ServeOptions{
+ Listener: server,
+ WorkDirectory: workdir,
})
assert.NoError(t, err)
}()
@@ -40,25 +37,39 @@ func TestEcho(t *testing.T) {
t.Run("Parse", func(t *testing.T) {
t.Parallel()
+ ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort)
+ defer cancel()
- responses := []*proto.Parse_Response{{
- Type: &proto.Parse_Response_Log{
- Log: &proto.Log{
- Output: "log-output",
+ responses := []*proto.Response{
+ {
+ Type: &proto.Response_Log{
+ Log: &proto.Log{
+ Output: "log-output",
+ },
},
},
- }, {
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{},
+ {
+ Type: &proto.Response_Parse{
+ Parse: &proto.ParseComplete{},
+ },
},
- }}
+ }
data, err := echo.Tar(&echo.Responses{
Parse: responses,
})
require.NoError(t, err)
- client, err := api.Parse(ctx, &proto.Parse_Request{
- Directory: unpackTar(t, fs, data),
- })
+ client, err := api.Session(ctx)
+ require.NoError(t, err)
+ defer func() {
+ err := client.Close()
+ require.NoError(t, err)
+ }()
+ err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{
+ TemplateSourceArchive: data,
+ }}})
+ require.NoError(t, err)
+
+ err = client.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
require.NoError(t, err)
log, err := client.Recv()
require.NoError(t, err)
@@ -70,68 +81,172 @@ func TestEcho(t *testing.T) {
t.Run("Provision", func(t *testing.T) {
t.Parallel()
+ ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort)
+ defer cancel()
- responses := []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
- Log: &proto.Log{
- Level: proto.LogLevel_INFO,
- Output: "log-output",
+ planResponses := []*proto.Response{
+ {
+ Type: &proto.Response_Log{
+ Log: &proto.Log{
+ Level: proto.LogLevel_INFO,
+ Output: "log-output",
+ },
},
},
- }, {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Resources: []*proto.Resource{{
- Name: "resource",
- }},
+ {
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Resources: []*proto.Resource{{
+ Name: "resource",
+ }},
+ },
},
},
- }}
+ }
+ applyResponses := []*proto.Response{
+ {
+ Type: &proto.Response_Log{
+ Log: &proto.Log{
+ Level: proto.LogLevel_INFO,
+ Output: "log-output",
+ },
+ },
+ },
+ {
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
+ Resources: []*proto.Resource{{
+ Name: "resource",
+ }},
+ },
+ },
+ },
+ }
data, err := echo.Tar(&echo.Responses{
- ProvisionApply: responses,
+ ProvisionPlan: planResponses,
+ ProvisionApply: applyResponses,
})
require.NoError(t, err)
- client, err := api.Provision(ctx)
+ client, err := api.Session(ctx)
+ require.NoError(t, err)
+ defer func() {
+ err := client.Close()
+ require.NoError(t, err)
+ }()
+ err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{
+ TemplateSourceArchive: data,
+ }}})
+ require.NoError(t, err)
+
+ err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}})
require.NoError(t, err)
- err = client.Send(&proto.Provision_Request{
- Type: &proto.Provision_Request_Plan{
- Plan: &proto.Provision_Plan{
- Config: &proto.Provision_Config{
- Directory: unpackTar(t, fs, data),
+ log, err := client.Recv()
+ require.NoError(t, err)
+ require.Equal(t, planResponses[0].GetLog().Output, log.GetLog().Output)
+ complete, err := client.Recv()
+ require.NoError(t, err)
+ require.Equal(t, planResponses[1].GetPlan().Resources[0].Name,
+ complete.GetPlan().Resources[0].Name)
+
+ err = client.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}})
+ require.NoError(t, err)
+ log, err = client.Recv()
+ require.NoError(t, err)
+ require.Equal(t, applyResponses[0].GetLog().Output, log.GetLog().Output)
+ complete, err = client.Recv()
+ require.NoError(t, err)
+ require.Equal(t, applyResponses[1].GetApply().Resources[0].Name,
+ complete.GetApply().Resources[0].Name)
+ })
+
+ t.Run("ProvisionStop", func(t *testing.T) {
+ t.Parallel()
+
+ // Stop responses should be returned when the workspace is being stopped.
+ data, err := echo.Tar(&echo.Responses{
+ ProvisionApply: applyCompleteResource("DEFAULT"),
+ ProvisionPlan: planCompleteResource("DEFAULT"),
+ ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Response{
+ proto.WorkspaceTransition_STOP: planCompleteResource("STOP"),
+ },
+ ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{
+ proto.WorkspaceTransition_STOP: applyCompleteResource("STOP"),
+ },
+ })
+ require.NoError(t, err)
+
+ client, err := api.Session(ctx)
+ require.NoError(t, err)
+ defer func() {
+ err := client.Close()
+ require.NoError(t, err)
+ }()
+ err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{
+ TemplateSourceArchive: data,
+ }}})
+ require.NoError(t, err)
+
+ // Do stop.
+ err = client.Send(&proto.Request{
+ Type: &proto.Request_Plan{
+ Plan: &proto.PlanRequest{
+ Metadata: &proto.Metadata{
+ WorkspaceTransition: proto.WorkspaceTransition_STOP,
},
},
},
})
require.NoError(t, err)
- log, err := client.Recv()
- require.NoError(t, err)
- require.Equal(t, responses[0].GetLog().Output, log.GetLog().Output)
+
complete, err := client.Recv()
require.NoError(t, err)
- require.Equal(t, responses[1].GetComplete().Resources[0].Name,
- complete.GetComplete().Resources[0].Name)
+ require.Equal(t,
+ "STOP",
+ complete.GetPlan().Resources[0].Name,
+ )
+
+ // Do start.
+ err = client.Send(&proto.Request{
+ Type: &proto.Request_Plan{
+ Plan: &proto.PlanRequest{
+ Metadata: &proto.Metadata{
+ WorkspaceTransition: proto.WorkspaceTransition_START,
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ complete, err = client.Recv()
+ require.NoError(t, err)
+ require.Equal(t,
+ "DEFAULT",
+ complete.GetPlan().Resources[0].Name,
+ )
})
t.Run("ProvisionWithLogLevel", func(t *testing.T) {
t.Parallel()
+ ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort)
+ defer cancel()
- responses := []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Log{
+ responses := []*proto.Response{{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_TRACE,
Output: "log-output-trace",
},
},
}, {
- Type: &proto.Provision_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "log-output-info",
},
},
}, {
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "resource",
}},
@@ -139,49 +254,62 @@ func TestEcho(t *testing.T) {
},
}}
data, err := echo.Tar(&echo.Responses{
+ ProvisionPlan: echo.PlanComplete,
ProvisionApply: responses,
})
require.NoError(t, err)
- client, err := api.Provision(ctx)
+ client, err := api.Session(ctx)
require.NoError(t, err)
- err = client.Send(&proto.Provision_Request{
- Type: &proto.Provision_Request_Plan{
- Plan: &proto.Provision_Plan{
- Config: &proto.Provision_Config{
- Directory: unpackTar(t, fs, data),
- ProvisionerLogLevel: "debug",
- },
- },
- },
- })
+ defer func() {
+ err := client.Close()
+ require.NoError(t, err)
+ }()
+ err = client.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{
+ TemplateSourceArchive: data,
+ ProvisionerLogLevel: "debug",
+ }}})
+ require.NoError(t, err)
+
+ // Plan is required before apply
+ err = client.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}})
+ require.NoError(t, err)
+ complete, err := client.Recv()
+ require.NoError(t, err)
+ require.NotNil(t, complete.GetPlan())
+
+ err = client.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}})
require.NoError(t, err)
log, err := client.Recv()
require.NoError(t, err)
// Skip responses[0] as it's trace level
require.Equal(t, responses[1].GetLog().Output, log.GetLog().Output)
- complete, err := client.Recv()
+ complete, err = client.Recv()
require.NoError(t, err)
- require.Equal(t, responses[2].GetComplete().Resources[0].Name,
- complete.GetComplete().Resources[0].Name)
+ require.Equal(t, responses[2].GetApply().Resources[0].Name,
+ complete.GetApply().Resources[0].Name)
})
}
-func unpackTar(t *testing.T, fs afero.Fs, data []byte) string {
- directory := t.TempDir()
- reader := tar.NewReader(bytes.NewReader(data))
- for {
- header, err := reader.Next()
- if err != nil {
- break
- }
- // #nosec
- path := filepath.Join(directory, header.Name)
- file, err := fs.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
- require.NoError(t, err)
- _, err = io.CopyN(file, reader, 1<<20)
- require.ErrorIs(t, err, io.EOF)
- err = file.Close()
- require.NoError(t, err)
- }
- return directory
+func planCompleteResource(name string) []*proto.Response {
+ return []*proto.Response{{
+ Type: &proto.Response_Plan{
+ Plan: &proto.PlanComplete{
+ Resources: []*proto.Resource{{
+ Name: name,
+ }},
+ },
+ },
+ }}
+}
+
+func applyCompleteResource(name string) []*proto.Response {
+ return []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
+ Resources: []*proto.Resource{{
+ Name: name,
+ }},
+ },
+ },
+ }}
}
diff --git a/provisioner/terraform/diagnostic_test.go b/provisioner/terraform/diagnostic_test.go
index 836ae85e4915c..54b5b6c5c35d3 100644
--- a/provisioner/terraform/diagnostic_test.go
+++ b/provisioner/terraform/diagnostic_test.go
@@ -8,7 +8,7 @@ import (
tfjson "github.com/hashicorp/terraform-json"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/provisioner/terraform"
+ "github.com/coder/coder/v2/provisioner/terraform"
)
type hasDiagnostic struct {
diff --git a/provisioner/terraform/executor.go b/provisioner/terraform/executor.go
index c595d39f8043e..d523eeca198e5 100644
--- a/provisioner/terraform/executor.go
+++ b/provisioner/terraform/executor.go
@@ -20,11 +20,12 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
type executor struct {
+ logger slog.Logger
server *server
mut *sync.Mutex
binaryPath string
@@ -50,8 +51,10 @@ func (e *executor) execWriteOutput(ctx, killCtx context.Context, args, env []str
ctx, span := e.server.startTrace(ctx, fmt.Sprintf("exec - terraform %s", args[0]))
defer span.End()
span.SetAttributes(attribute.StringSlice("args", args))
+ e.logger.Debug(ctx, "starting command", slog.F("args", args))
defer func() {
+ e.logger.Debug(ctx, "closing writers", slog.Error(err))
closeErr := stdOutWriter.Close()
if err == nil && closeErr != nil {
err = closeErr
@@ -62,6 +65,7 @@ func (e *executor) execWriteOutput(ctx, killCtx context.Context, args, env []str
}
}()
if ctx.Err() != nil {
+ e.logger.Debug(ctx, "context canceled before command started", slog.F("args", args))
return ctx.Err()
}
@@ -84,13 +88,20 @@ func (e *executor) execWriteOutput(ctx, killCtx context.Context, args, env []str
cmd.Stdout = syncWriter{mut, stdOutWriter}
cmd.Stderr = syncWriter{mut, stdErrWriter}
+ e.server.logger.Debug(ctx, "executing terraform command",
+ slog.F("binary_path", e.binaryPath),
+ slog.F("args", args),
+ )
err = cmd.Start()
if err != nil {
+ e.logger.Debug(ctx, "failed to start command", slog.F("args", args))
return err
}
- interruptCommandOnCancel(ctx, killCtx, cmd)
+ interruptCommandOnCancel(ctx, killCtx, e.logger, cmd)
- return cmd.Wait()
+ err = cmd.Wait()
+ e.logger.Debug(ctx, "command done", slog.F("args", args), slog.Error(err))
+ return err
}
// execParseJSON must only be called while the lock is held.
@@ -116,7 +127,7 @@ func (e *executor) execParseJSON(ctx, killCtx context.Context, args, env []strin
if err != nil {
return err
}
- interruptCommandOnCancel(ctx, killCtx, cmd)
+ interruptCommandOnCancel(ctx, killCtx, e.logger, cmd)
err = cmd.Wait()
if err != nil {
@@ -203,15 +214,23 @@ func (e *executor) init(ctx, killCtx context.Context, logr logSink) error {
return e.execWriteOutput(ctx, killCtx, args, e.basicEnv(), outWriter, errWriter)
}
+func getPlanFilePath(workdir string) string {
+ return filepath.Join(workdir, "terraform.tfplan")
+}
+
+func getStateFilePath(workdir string) string {
+ return filepath.Join(workdir, "terraform.tfstate")
+}
+
// revive:disable-next-line:flag-parameter
-func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr logSink, destroy bool) (*proto.Provision_Response, error) {
+func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr logSink, destroy bool) (*proto.PlanComplete, error) {
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
defer span.End()
e.mut.Lock()
defer e.mut.Unlock()
- planfilePath := filepath.Join(e.workdir, "terraform.tfplan")
+ planfilePath := getPlanFilePath(e.workdir)
args := []string{
"plan",
"-no-color",
@@ -244,22 +263,30 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
if err != nil {
return nil, err
}
- planFileByt, err := os.ReadFile(planfilePath)
- if err != nil {
- return nil, err
- }
- return &proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: state.Parameters,
- Resources: state.Resources,
- GitAuthProviders: state.GitAuthProviders,
- Plan: planFileByt,
- },
- },
+ return &proto.PlanComplete{
+ Parameters: state.Parameters,
+ Resources: state.Resources,
+ GitAuthProviders: state.GitAuthProviders,
}, nil
}
+func onlyDataResources(sm tfjson.StateModule) tfjson.StateModule {
+ filtered := sm
+ filtered.Resources = []*tfjson.StateResource{}
+ for _, r := range sm.Resources {
+ if r.Mode == "data" {
+ filtered.Resources = append(filtered.Resources, r)
+ }
+ }
+
+ filtered.ChildModules = []*tfjson.StateModule{}
+ for _, c := range sm.ChildModules {
+ filteredChild := onlyDataResources(*c)
+ filtered.ChildModules = append(filtered.ChildModules, &filteredChild)
+ }
+ return filtered
+}
+
// planResources must only be called while the lock is held.
func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) (*State, error) {
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
@@ -276,7 +303,15 @@ func (e *executor) planResources(ctx, killCtx context.Context, planfilePath stri
}
modules := []*tfjson.StateModule{}
if plan.PriorState != nil {
- modules = append(modules, plan.PriorState.Values.RootModule)
+ // We need the data resources for rich parameters. For some reason, they
+ // only show up in the PriorState.
+ //
+ // We don't want all prior resources, because Quotas (and
+ // future features) would never know which resources are getting
+ // deleted by a stop.
+
+ filtered := onlyDataResources(*plan.PriorState.Values.RootModule)
+ modules = append(modules, &filtered)
}
modules = append(modules, plan.PlannedValues.RootModule)
@@ -317,7 +352,7 @@ func (e *executor) graph(ctx, killCtx context.Context) (string, error) {
if err != nil {
return "", err
}
- interruptCommandOnCancel(ctx, killCtx, cmd)
+ interruptCommandOnCancel(ctx, killCtx, e.logger, cmd)
err = cmd.Wait()
if err != nil {
@@ -328,33 +363,22 @@ func (e *executor) graph(ctx, killCtx context.Context) (string, error) {
func (e *executor) apply(
ctx, killCtx context.Context,
- plan []byte,
env []string,
logr logSink,
-) (*proto.Provision_Response, error) {
+) (*proto.ApplyComplete, error) {
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
defer span.End()
e.mut.Lock()
defer e.mut.Unlock()
- planFile, err := os.CreateTemp("", "coder-terrafrom-plan")
- if err != nil {
- return nil, xerrors.Errorf("create plan file: %w", err)
- }
- _, err = planFile.Write(plan)
- if err != nil {
- return nil, xerrors.Errorf("write plan file: %w", err)
- }
- defer os.Remove(planFile.Name())
-
args := []string{
"apply",
"-no-color",
"-auto-approve",
"-input=false",
"-json",
- planFile.Name(),
+ getPlanFilePath(e.workdir),
}
outWriter, doneOut := provisionLogWriter(logr)
@@ -366,7 +390,7 @@ func (e *executor) apply(
<-doneErr
}()
- err = e.execWriteOutput(ctx, killCtx, args, env, outWriter, errWriter)
+ err := e.execWriteOutput(ctx, killCtx, args, env, outWriter, errWriter)
if err != nil {
return nil, xerrors.Errorf("terraform apply: %w", err)
}
@@ -379,15 +403,11 @@ func (e *executor) apply(
if err != nil {
return nil, xerrors.Errorf("read statefile %q: %w", statefilePath, err)
}
- return &proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: state.Parameters,
- Resources: state.Resources,
- GitAuthProviders: state.GitAuthProviders,
- State: stateContent,
- },
- },
+ return &proto.ApplyComplete{
+ Parameters: state.Parameters,
+ Resources: state.Resources,
+ GitAuthProviders: state.GitAuthProviders,
+ State: stateContent,
}, nil
}
@@ -432,48 +452,28 @@ func (e *executor) state(ctx, killCtx context.Context) (*tfjson.State, error) {
return state, nil
}
-func interruptCommandOnCancel(ctx, killCtx context.Context, cmd *exec.Cmd) {
+func interruptCommandOnCancel(ctx, killCtx context.Context, logger slog.Logger, cmd *exec.Cmd) {
go func() {
select {
case <-ctx.Done():
+ var err error
switch runtime.GOOS {
case "windows":
// Interrupts aren't supported by Windows.
- _ = cmd.Process.Kill()
+ err = cmd.Process.Kill()
default:
- _ = cmd.Process.Signal(os.Interrupt)
+ err = cmd.Process.Signal(os.Interrupt)
}
+ logger.Debug(ctx, "interrupted command", slog.F("args", cmd.Args), slog.Error(err))
case <-killCtx.Done():
+ logger.Debug(ctx, "kill context ended", slog.F("args", cmd.Args))
}
}()
}
type logSink interface {
- Log(*proto.Log)
-}
-
-type streamLogSink struct {
- // Any errors writing to the stream will be logged to logger.
- logger slog.Logger
- stream proto.DRPCProvisioner_ProvisionStream
-}
-
-var _ logSink = streamLogSink{}
-
-func (s streamLogSink) Log(l *proto.Log) {
- err := s.stream.Send(&proto.Provision_Response{
- Type: &proto.Provision_Response_Log{
- Log: l,
- },
- })
- if err != nil {
- s.logger.Warn(context.Background(), "write log to stream",
- slog.F("level", l.Level.String()),
- slog.F("message", l.Output),
- slog.Error(err),
- )
- }
+ ProvisionLog(l proto.LogLevel, o string)
}
// logWriter creates a WriteCloser that will log each line of text at the given level. The WriteCloser must be closed
@@ -497,7 +497,7 @@ func readAndLog(sink logSink, r io.Reader, done chan<- any, level proto.LogLevel
continue
}
- sink.Log(&proto.Log{Level: level, Output: scanner.Text()})
+ sink.ProvisionLog(level, scanner.Text())
continue
}
@@ -514,7 +514,7 @@ func readAndLog(sink logSink, r io.Reader, done chan<- any, level proto.LogLevel
if logLevel == proto.LogLevel_INFO {
logLevel = proto.LogLevel_DEBUG
}
- sink.Log(&proto.Log{Level: logLevel, Output: log.Message})
+ sink.ProvisionLog(logLevel, log.Message)
}
}
@@ -559,7 +559,7 @@ func provisionReadAndLog(sink logSink, r io.Reader, done chan<- any) {
}
logLevel := convertTerraformLogLevel(log.Level, sink)
- sink.Log(&proto.Log{Level: logLevel, Output: log.Message})
+ sink.ProvisionLog(logLevel, log.Message)
// If the diagnostic is provided, let's provide a bit more info!
if log.Diagnostic == nil {
@@ -567,7 +567,7 @@ func provisionReadAndLog(sink logSink, r io.Reader, done chan<- any) {
}
logLevel = convertTerraformLogLevel(string(log.Diagnostic.Severity), sink)
for _, diagLine := range strings.Split(FormatDiagnostic(log.Diagnostic), "\n") {
- sink.Log(&proto.Log{Level: logLevel, Output: diagLine})
+ sink.ProvisionLog(logLevel, diagLine)
}
}
}
@@ -585,10 +585,7 @@ func convertTerraformLogLevel(logLevel string, sink logSink) proto.LogLevel {
case "error":
return proto.LogLevel_ERROR
default:
- sink.Log(&proto.Log{
- Level: proto.LogLevel_WARN,
- Output: fmt.Sprintf("unable to convert log level %s", logLevel),
- })
+ sink.ProvisionLog(proto.LogLevel_WARN, fmt.Sprintf("unable to convert log level %s", logLevel))
return proto.LogLevel_INFO
}
}
diff --git a/provisioner/terraform/executor_internal_test.go b/provisioner/terraform/executor_internal_test.go
index 7c9d6d6f082e5..97cb5285372f2 100644
--- a/provisioner/terraform/executor_internal_test.go
+++ b/provisioner/terraform/executor_internal_test.go
@@ -1,11 +1,13 @@
package terraform
import (
+ "encoding/json"
"testing"
+ tfjson "github.com/hashicorp/terraform-json"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
type mockLogger struct {
@@ -14,8 +16,8 @@ type mockLogger struct {
var _ logSink = &mockLogger{}
-func (m *mockLogger) Log(l *proto.Log) {
- m.logs = append(m.logs, l)
+func (m *mockLogger) ProvisionLog(l proto.LogLevel, o string) {
+ m.logs = append(m.logs, &proto.Log{Level: l, Output: o})
}
func TestLogWriter_Mainline(t *testing.T) {
@@ -41,3 +43,133 @@ From standing in the English rain`))
}
require.Equal(t, expected, logr.logs)
}
+
+func TestOnlyDataResources(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ stateMod *tfjson.StateModule
+ expected *tfjson.StateModule
+ }{
+ {
+ name: "empty state module",
+ stateMod: &tfjson.StateModule{},
+ expected: &tfjson.StateModule{},
+ },
+ {
+ name: "only data resources",
+ stateMod: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{
+ {Name: "cat", Type: "coder_parameter", Mode: "data", Address: "cat-address"},
+ {Name: "cow", Type: "foobaz", Mode: "data", Address: "cow-address"},
+ },
+ ChildModules: []*tfjson.StateModule{
+ {
+ Resources: []*tfjson.StateResource{
+ {Name: "child-cat", Type: "coder_parameter", Mode: "data", Address: "child-cat-address"},
+ {Name: "child-dog", Type: "foobar", Mode: "data", Address: "child-dog-address"},
+ },
+ Address: "child-module-1",
+ },
+ },
+ Address: "fake-module",
+ },
+ expected: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{
+ {Name: "cat", Type: "coder_parameter", Mode: "data", Address: "cat-address"},
+ {Name: "cow", Type: "foobaz", Mode: "data", Address: "cow-address"},
+ },
+ ChildModules: []*tfjson.StateModule{
+ {
+ Resources: []*tfjson.StateResource{
+ {Name: "child-cat", Type: "coder_parameter", Mode: "data", Address: "child-cat-address"},
+ {Name: "child-dog", Type: "foobar", Mode: "data", Address: "child-dog-address"},
+ },
+ Address: "child-module-1",
+ },
+ },
+ Address: "fake-module",
+ },
+ },
+ {
+ name: "only non-data resources",
+ stateMod: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{
+ {Name: "cat", Type: "coder_parameter", Mode: "foobar", Address: "cat-address"},
+ {Name: "cow", Type: "foobaz", Mode: "foo", Address: "cow-address"},
+ },
+ ChildModules: []*tfjson.StateModule{
+ {
+ Resources: []*tfjson.StateResource{
+ {Name: "child-cat", Type: "coder_parameter", Mode: "foobar", Address: "child-cat-address"},
+ {Name: "child-dog", Type: "foobar", Mode: "foobaz", Address: "child-dog-address"},
+ },
+ Address: "child-module-1",
+ },
+ },
+ Address: "fake-module",
+ },
+ expected: &tfjson.StateModule{
+ Address: "fake-module",
+ ChildModules: []*tfjson.StateModule{
+ {Address: "child-module-1"},
+ },
+ },
+ },
+ {
+ name: "mixed resources",
+ stateMod: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{
+ {Name: "cat", Type: "coder_parameter", Mode: "data", Address: "cat-address"},
+ {Name: "dog", Type: "foobar", Mode: "magic", Address: "dog-address"},
+ {Name: "cow", Type: "foobaz", Mode: "data", Address: "cow-address"},
+ },
+ ChildModules: []*tfjson.StateModule{
+ {
+ Resources: []*tfjson.StateResource{
+ {Name: "child-cat", Type: "coder_parameter", Mode: "data", Address: "child-cat-address"},
+ {Name: "child-dog", Type: "foobar", Mode: "data", Address: "child-dog-address"},
+ {Name: "child-cow", Type: "foobaz", Mode: "magic", Address: "child-cow-address"},
+ },
+ Address: "child-module-1",
+ },
+ },
+ Address: "fake-module",
+ },
+ expected: &tfjson.StateModule{
+ Resources: []*tfjson.StateResource{
+ {Name: "cat", Type: "coder_parameter", Mode: "data", Address: "cat-address"},
+ {Name: "cow", Type: "foobaz", Mode: "data", Address: "cow-address"},
+ },
+ ChildModules: []*tfjson.StateModule{
+ {
+ Resources: []*tfjson.StateResource{
+ {Name: "child-cat", Type: "coder_parameter", Mode: "data", Address: "child-cat-address"},
+ {Name: "child-dog", Type: "foobar", Mode: "data", Address: "child-dog-address"},
+ },
+ Address: "child-module-1",
+ },
+ },
+ Address: "fake-module",
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ filtered := onlyDataResources(*tt.stateMod)
+
+ expected, err := json.Marshal(tt.expected)
+ require.NoError(t, err)
+ got, err := json.Marshal(filtered)
+ require.NoError(t, err)
+
+ require.Equal(t, string(expected), string(got))
+ })
+ }
+}
diff --git a/provisioner/terraform/install_test.go b/provisioner/terraform/install_test.go
index 67f7a5ddb1188..700ae237b1c9e 100644
--- a/provisioner/terraform/install_test.go
+++ b/provisioner/terraform/install_test.go
@@ -17,7 +17,7 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/provisioner/terraform"
+ "github.com/coder/coder/v2/provisioner/terraform"
)
func TestInstall(t *testing.T) {
diff --git a/provisioner/terraform/parse.go b/provisioner/terraform/parse.go
index dc57cde28a591..10ab7b801b071 100644
--- a/provisioner/terraform/parse.go
+++ b/provisioner/terraform/parse.go
@@ -11,19 +11,21 @@ import (
"github.com/mitchellh/go-wordwrap"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
// Parse extracts Terraform variables from source-code.
-func (s *server) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_ParseStream) error {
- _, span := s.startTrace(stream.Context(), tracing.FuncName())
+func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete {
+ ctx := sess.Context()
+ _, span := s.startTrace(ctx, tracing.FuncName())
defer span.End()
// Load the module and print any parse errors.
- module, diags := tfconfig.LoadModule(request.Directory)
+ module, diags := tfconfig.LoadModule(sess.WorkDirectory)
if diags.HasErrors() {
- return xerrors.Errorf("load module: %s", formatDiagnostics(request.Directory, diags))
+ return provisionersdk.ParseErrorf("load module: %s", formatDiagnostics(sess.WorkDirectory, diags))
}
// Sort variables by (filename, line) to make the ordering consistent
@@ -40,17 +42,13 @@ func (s *server) Parse(request *proto.Parse_Request, stream proto.DRPCProvisione
for _, v := range variables {
mv, err := convertTerraformVariable(v)
if err != nil {
- return xerrors.Errorf("can't convert the Terraform variable to a managed one: %w", err)
+ return provisionersdk.ParseErrorf("can't convert the Terraform variable to a managed one: %s", err)
}
templateVariables = append(templateVariables, mv)
}
- return stream.Send(&proto.Parse_Response{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
- TemplateVariables: templateVariables,
- },
- },
- })
+ return &proto.ParseComplete{
+ TemplateVariables: templateVariables,
+ }
}
// Converts a Terraform variable to a template-wide variable, processed by Coder.
diff --git a/provisioner/terraform/parse_test.go b/provisioner/terraform/parse_test.go
index e82bcba470857..c28532af25831 100644
--- a/provisioner/terraform/parse_test.go
+++ b/provisioner/terraform/parse_test.go
@@ -4,13 +4,11 @@ package terraform_test
import (
"encoding/json"
- "os"
- "path/filepath"
"testing"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
func TestParse(t *testing.T) {
@@ -21,9 +19,8 @@ func TestParse(t *testing.T) {
testCases := []struct {
Name string
Files map[string]string
- Response *proto.Parse_Response
- // If ErrorContains is not empty, then response.Recv() should return an
- // error containing this string before a Complete response is returned.
+ Response *proto.ParseComplete
+ // If ErrorContains is not empty, then the ParseComplete should have an Error containing the given string
ErrorContains string
}{
{
@@ -33,16 +30,12 @@ func TestParse(t *testing.T) {
description = "Testing!"
}`,
},
- Response: &proto.Parse_Response{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
- TemplateVariables: []*proto.TemplateVariable{
- {
- Name: "A",
- Description: "Testing!",
- Required: true,
- },
- },
+ Response: &proto.ParseComplete{
+ TemplateVariables: []*proto.TemplateVariable{
+ {
+ Name: "A",
+ Description: "Testing!",
+ Required: true,
},
},
},
@@ -54,15 +47,11 @@ func TestParse(t *testing.T) {
default = "wow"
}`,
},
- Response: &proto.Parse_Response{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
- TemplateVariables: []*proto.TemplateVariable{
- {
- Name: "A",
- DefaultValue: "wow",
- },
- },
+ Response: &proto.ParseComplete{
+ TemplateVariables: []*proto.TemplateVariable{
+ {
+ Name: "A",
+ DefaultValue: "wow",
},
},
},
@@ -76,15 +65,11 @@ func TestParse(t *testing.T) {
}
}`,
},
- Response: &proto.Parse_Response{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
- TemplateVariables: []*proto.TemplateVariable{
- {
- Name: "A",
- Required: true,
- },
- },
+ Response: &proto.ParseComplete{
+ TemplateVariables: []*proto.TemplateVariable{
+ {
+ Name: "A",
+ Required: true,
},
},
},
@@ -104,27 +89,23 @@ func TestParse(t *testing.T) {
"main2.tf": `variable "baz" { }
variable "quux" { }`,
},
- Response: &proto.Parse_Response{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
- TemplateVariables: []*proto.TemplateVariable{
- {
- Name: "foo",
- Required: true,
- },
- {
- Name: "bar",
- Required: true,
- },
- {
- Name: "baz",
- Required: true,
- },
- {
- Name: "quux",
- Required: true,
- },
- },
+ Response: &proto.ParseComplete{
+ TemplateVariables: []*proto.TemplateVariable{
+ {
+ Name: "foo",
+ Required: true,
+ },
+ {
+ Name: "bar",
+ Required: true,
+ },
+ {
+ Name: "baz",
+ Required: true,
+ },
+ {
+ Name: "quux",
+ Required: true,
},
},
},
@@ -139,19 +120,15 @@ func TestParse(t *testing.T) {
sensitive = true
}`,
},
- Response: &proto.Parse_Response{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
- TemplateVariables: []*proto.TemplateVariable{
- {
- Name: "A",
- Description: "Testing!",
- Type: "bool",
- DefaultValue: "true",
- Required: false,
- Sensitive: true,
- },
- },
+ Response: &proto.ParseComplete{
+ TemplateVariables: []*proto.TemplateVariable{
+ {
+ Name: "A",
+ Description: "Testing!",
+ Type: "bool",
+ DefaultValue: "true",
+ Required: false,
+ Sensitive: true,
},
},
},
@@ -166,19 +143,15 @@ func TestParse(t *testing.T) {
sensitive = true
}`,
},
- Response: &proto.Parse_Response{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
- TemplateVariables: []*proto.TemplateVariable{
- {
- Name: "A",
- Description: "Testing!",
- Type: "string",
- DefaultValue: "abc",
- Required: false,
- Sensitive: true,
- },
- },
+ Response: &proto.ParseComplete{
+ TemplateVariables: []*proto.TemplateVariable{
+ {
+ Name: "A",
+ Description: "Testing!",
+ Type: "string",
+ DefaultValue: "abc",
+ Required: false,
+ Sensitive: true,
},
},
},
@@ -193,19 +166,15 @@ func TestParse(t *testing.T) {
sensitive = true
}`,
},
- Response: &proto.Parse_Response{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
- TemplateVariables: []*proto.TemplateVariable{
- {
- Name: "A",
- Description: "Testing!",
- Type: "string",
- DefaultValue: "",
- Required: false,
- Sensitive: true,
- },
- },
+ Response: &proto.ParseComplete{
+ TemplateVariables: []*proto.TemplateVariable{
+ {
+ Name: "A",
+ Description: "Testing!",
+ Type: "string",
+ DefaultValue: "",
+ Required: false,
+ Sensitive: true,
},
},
},
@@ -219,19 +188,15 @@ func TestParse(t *testing.T) {
sensitive = true
}`,
},
- Response: &proto.Parse_Response{
- Type: &proto.Parse_Response_Complete{
- Complete: &proto.Parse_Complete{
- TemplateVariables: []*proto.TemplateVariable{
- {
- Name: "A",
- Description: "Testing!",
- Type: "string",
- DefaultValue: "",
- Required: true,
- Sensitive: true,
- },
- },
+ Response: &proto.ParseComplete{
+ TemplateVariables: []*proto.TemplateVariable{
+ {
+ Name: "A",
+ Description: "Testing!",
+ Type: "string",
+ DefaultValue: "",
+ Required: true,
+ Sensitive: true,
},
},
},
@@ -243,40 +208,31 @@ func TestParse(t *testing.T) {
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
- // Write all files to the temporary test directory.
- directory := t.TempDir()
- for path, content := range testCase.Files {
- err := os.WriteFile(filepath.Join(directory, path), []byte(content), 0o600)
- require.NoError(t, err)
- }
-
- response, err := api.Parse(ctx, &proto.Parse_Request{
- Directory: directory,
+ session := configure(ctx, t, api, &proto.Config{
+ TemplateSourceArchive: makeTar(t, testCase.Files),
})
+
+ err := session.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
require.NoError(t, err)
for {
- msg, err := response.Recv()
- if err != nil {
- if testCase.ErrorContains != "" {
- require.ErrorContains(t, err, testCase.ErrorContains)
- break
- }
+ msg, err := session.Recv()
+ require.NoError(t, err)
- require.NoError(t, err)
+ if testCase.ErrorContains != "" {
+ require.Contains(t, msg.GetParse().GetError(), testCase.ErrorContains)
+ break
}
- if msg.GetComplete() == nil {
+ // Ignore logs in this test
+ if msg.GetLog() != nil {
continue
}
- if testCase.ErrorContains != "" {
- t.Fatal("expected error but job completed successfully")
- }
// Ensure the want and got are equivalent!
want, err := json.Marshal(testCase.Response)
require.NoError(t, err)
- got, err := json.Marshal(msg)
+ got, err := json.Marshal(msg.GetParse())
require.NoError(t, err)
require.Equal(t, string(want), string(got))
diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go
index 5d249a30b30f6..ab832e4408683 100644
--- a/provisioner/terraform/provision.go
+++ b/provisioner/terraform/provision.go
@@ -4,60 +4,34 @@ import (
"context"
"fmt"
"os"
- "path/filepath"
"strings"
"time"
- "golang.org/x/xerrors"
+ "cdr.dev/slog"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/provisionersdk"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/terraform-provider-coder/provider"
)
-// Provision executes `terraform apply` or `terraform plan` for dry runs.
-func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
- ctx, span := s.startTrace(stream.Context(), tracing.FuncName())
- defer span.End()
-
- request, err := stream.Recv()
- if err != nil {
- return err
- }
- if request.GetCancel() != nil {
- return nil
- }
-
- var (
- applyRequest = request.GetApply()
- planRequest = request.GetPlan()
- )
-
- var config *proto.Provision_Config
- if applyRequest == nil && planRequest == nil {
- return nil
- } else if applyRequest != nil {
- config = applyRequest.Config
- } else if planRequest != nil {
- config = planRequest.Config
- }
-
- // Create a context for graceful cancellation bound to the stream
+func (s *server) setupContexts(parent context.Context, canceledOrComplete <-chan struct{}) (
+ ctx context.Context, cancel func(), killCtx context.Context, kill func(),
+) {
+ // Create a context for graceful cancellation bound to the session
// context. This ensures that we will perform graceful cancellation
// even on connection loss.
- ctx, cancel := context.WithCancel(ctx)
- defer cancel()
+ ctx, cancel = context.WithCancel(parent)
// Create a separate context for forceful cancellation not tied to
// the stream so that we can control when to terminate the process.
- killCtx, kill := context.WithCancel(context.Background())
- defer kill()
+ killCtx, kill = context.WithCancel(context.Background())
// Ensure processes are eventually cleaned up on graceful
// cancellation or disconnect.
go func() {
<-ctx.Done()
+ s.logger.Debug(ctx, "graceful context done")
// TODO(mafredri): We should track this provision request as
// part of graceful server shutdown procedure. Waiting on a
@@ -66,134 +40,131 @@ func (s *server) Provision(stream proto.DRPCProvisioner_ProvisionStream) error {
defer t.Stop()
select {
case <-t.C:
+ s.logger.Debug(ctx, "exit timeout hit")
kill()
case <-killCtx.Done():
+ s.logger.Debug(ctx, "kill context done")
}
}()
+ // Process cancel
go func() {
- for {
- request, err := stream.Recv()
- if err != nil {
- return
- }
- if request.GetCancel() == nil {
- // We only process cancellation requests here.
- continue
- }
- cancel()
- return
- }
+ <-canceledOrComplete
+ s.logger.Debug(ctx, "canceledOrComplete closed")
+ cancel()
}()
+ return ctx, cancel, killCtx, kill
+}
- sink := streamLogSink{
- logger: s.logger.Named("execution_logs"),
- stream: stream,
- }
-
- e := s.executor(config.Directory)
- if err = e.checkMinVersion(ctx); err != nil {
- return err
- }
- logTerraformEnvVars(sink)
+func (s *server) Plan(
+ sess *provisionersdk.Session, request *proto.PlanRequest, canceledOrComplete <-chan struct{},
+) *proto.PlanComplete {
+ ctx, span := s.startTrace(sess.Context(), tracing.FuncName())
+ defer span.End()
+ ctx, cancel, killCtx, kill := s.setupContexts(ctx, canceledOrComplete)
+ defer cancel()
+ defer kill()
- statefilePath := filepath.Join(config.Directory, "terraform.tfstate")
- if len(config.State) > 0 {
- err = os.WriteFile(statefilePath, config.State, 0o600)
- if err != nil {
- return xerrors.Errorf("write statefile %q: %w", statefilePath, err)
- }
+ e := s.executor(sess.WorkDirectory)
+ if err := e.checkMinVersion(ctx); err != nil {
+ return provisionersdk.PlanErrorf(err.Error())
}
+ logTerraformEnvVars(sess)
// If we're destroying, exit early if there's no state. This is necessary to
// avoid any cases where a workspace is "locked out" of terraform due to
// e.g. bad template param values and cannot be deleted. This is just for
// contingency, in the future we will try harder to prevent workspaces being
// broken this hard.
- if config.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY && len(config.State) == 0 {
- _ = stream.Send(&proto.Provision_Response{
- Type: &proto.Provision_Response_Log{
- Log: &proto.Log{
- Level: proto.LogLevel_INFO,
- Output: "The terraform state does not exist, there is nothing to do",
- },
- },
- })
+ if request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY && len(sess.Config.State) == 0 {
+ sess.ProvisionLog(proto.LogLevel_INFO, "The terraform state does not exist, there is nothing to do")
+ return &proto.PlanComplete{}
+ }
- return stream.Send(&proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{},
- },
- })
+ statefilePath := getStateFilePath(sess.WorkDirectory)
+ if len(sess.Config.State) > 0 {
+ err := os.WriteFile(statefilePath, sess.Config.State, 0o600)
+ if err != nil {
+ return provisionersdk.PlanErrorf("write statefile %q: %s", statefilePath, err)
+ }
}
s.logger.Debug(ctx, "running initialization")
- err = e.init(ctx, killCtx, sink)
+ err := e.init(ctx, killCtx, sess)
if err != nil {
- if ctx.Err() != nil {
- return stream.Send(&proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Error: err.Error(),
- },
- },
- })
- }
- return xerrors.Errorf("initialize terraform: %w", err)
+ s.logger.Debug(ctx, "init failed", slog.Error(err))
+ return provisionersdk.PlanErrorf("initialize terraform: %s", err)
}
s.logger.Debug(ctx, "ran initialization")
- env, err := provisionEnv(config, request.GetPlan().GetRichParameterValues(), request.GetPlan().GetGitAuthProviders())
+
+ env, err := provisionEnv(sess.Config, request.Metadata, request.RichParameterValues, request.GitAuthProviders)
if err != nil {
- return err
+ return provisionersdk.PlanErrorf("setup env: %s", err)
}
- var resp *proto.Provision_Response
- if planRequest != nil {
- vars, err := planVars(planRequest)
- if err != nil {
- return err
- }
+ vars, err := planVars(request)
+ if err != nil {
+ return provisionersdk.PlanErrorf("plan vars: %s", err)
+ }
- resp, err = e.plan(
- ctx, killCtx, env, vars, sink,
- config.Metadata.WorkspaceTransition == proto.WorkspaceTransition_DESTROY,
- )
- if err != nil {
- if ctx.Err() != nil {
- return stream.Send(&proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Error: err.Error(),
- },
- },
- })
- }
- return xerrors.Errorf("plan terraform: %w", err)
- }
- return stream.Send(resp)
+ resp, err := e.plan(
+ ctx, killCtx, env, vars, sess,
+ request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY,
+ )
+ if err != nil {
+ return provisionersdk.PlanErrorf(err.Error())
}
- // Must be apply
- resp, err = e.apply(
- ctx, killCtx, applyRequest.Plan, env, sink,
+ return resp
+}
+
+func (s *server) Apply(
+ sess *provisionersdk.Session, request *proto.ApplyRequest, canceledOrComplete <-chan struct{},
+) *proto.ApplyComplete {
+ ctx, span := s.startTrace(sess.Context(), tracing.FuncName())
+ defer span.End()
+ ctx, cancel, killCtx, kill := s.setupContexts(ctx, canceledOrComplete)
+ defer cancel()
+ defer kill()
+
+ e := s.executor(sess.WorkDirectory)
+ if err := e.checkMinVersion(ctx); err != nil {
+ return provisionersdk.ApplyErrorf(err.Error())
+ }
+ logTerraformEnvVars(sess)
+
+ // Exit early if there is no plan file. This is necessary to
+ // avoid any cases where a workspace is "locked out" of terraform due to
+ // e.g. bad template param values and cannot be deleted. This is just for
+ // contingency, in the future we will try harder to prevent workspaces being
+ // broken this hard.
+ if request.Metadata.GetWorkspaceTransition() == proto.WorkspaceTransition_DESTROY && len(sess.Config.State) == 0 {
+ sess.ProvisionLog(proto.LogLevel_INFO, "The terraform plan does not exist, there is nothing to do")
+ return &proto.ApplyComplete{}
+ }
+
+ // Earlier in the session, Plan() will have written the state file and the plan file.
+ statefilePath := getStateFilePath(sess.WorkDirectory)
+ env, err := provisionEnv(sess.Config, request.Metadata, nil, nil)
+ if err != nil {
+ return provisionersdk.ApplyErrorf("provision env: %s", err)
+ }
+ resp, err := e.apply(
+ ctx, killCtx, env, sess,
)
if err != nil {
errorMessage := err.Error()
// Terraform can fail and apply and still need to store it's state.
// In this case, we return Complete with an explicit error message.
stateData, _ := os.ReadFile(statefilePath)
- return stream.Send(&proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- State: stateData,
- Error: errorMessage,
- },
- },
- })
+ return &proto.ApplyComplete{
+ State: stateData,
+ Error: errorMessage,
+ }
}
- return stream.Send(resp)
+ return resp
}
-func planVars(plan *proto.Provision_Plan) ([]string, error) {
+func planVars(plan *proto.PlanRequest) ([]string, error) {
vars := []string{}
for _, variable := range plan.VariableValues {
vars = append(vars, fmt.Sprintf("%s=%s", variable.Name, variable.Value))
@@ -201,18 +172,21 @@ func planVars(plan *proto.Provision_Plan) ([]string, error) {
return vars, nil
}
-func provisionEnv(config *proto.Provision_Config, richParams []*proto.RichParameterValue, gitAuth []*proto.GitAuthProvider) ([]string, error) {
+func provisionEnv(
+ config *proto.Config, metadata *proto.Metadata,
+ richParams []*proto.RichParameterValue, gitAuth []*proto.GitAuthProvider,
+) ([]string, error) {
env := safeEnviron()
env = append(env,
- "CODER_AGENT_URL="+config.Metadata.CoderUrl,
- "CODER_WORKSPACE_TRANSITION="+strings.ToLower(config.Metadata.WorkspaceTransition.String()),
- "CODER_WORKSPACE_NAME="+config.Metadata.WorkspaceName,
- "CODER_WORKSPACE_OWNER="+config.Metadata.WorkspaceOwner,
- "CODER_WORKSPACE_OWNER_EMAIL="+config.Metadata.WorkspaceOwnerEmail,
- "CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN="+config.Metadata.WorkspaceOwnerOidcAccessToken,
- "CODER_WORKSPACE_ID="+config.Metadata.WorkspaceId,
- "CODER_WORKSPACE_OWNER_ID="+config.Metadata.WorkspaceOwnerId,
- "CODER_WORKSPACE_OWNER_SESSION_TOKEN="+config.Metadata.WorkspaceOwnerSessionToken,
+ "CODER_AGENT_URL="+metadata.GetCoderUrl(),
+ "CODER_WORKSPACE_TRANSITION="+strings.ToLower(metadata.GetWorkspaceTransition().String()),
+ "CODER_WORKSPACE_NAME="+metadata.GetWorkspaceName(),
+ "CODER_WORKSPACE_OWNER="+metadata.GetWorkspaceOwner(),
+ "CODER_WORKSPACE_OWNER_EMAIL="+metadata.GetWorkspaceOwnerEmail(),
+ "CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN="+metadata.GetWorkspaceOwnerOidcAccessToken(),
+ "CODER_WORKSPACE_ID="+metadata.GetWorkspaceId(),
+ "CODER_WORKSPACE_OWNER_ID="+metadata.GetWorkspaceOwnerId(),
+ "CODER_WORKSPACE_OWNER_SESSION_TOKEN="+metadata.GetWorkspaceOwnerSessionToken(),
)
for key, value := range provisionersdk.AgentScriptEnv() {
env = append(env, key+"="+value)
@@ -258,10 +232,10 @@ func logTerraformEnvVars(sink logSink) {
if !tfEnvSafeToPrint[parts[0]] {
parts[1] = ""
}
- sink.Log(&proto.Log{
- Level: proto.LogLevel_WARN,
- Output: fmt.Sprintf("terraform environment variable: %s=%s", parts[0], parts[1]),
- })
+ sink.ProvisionLog(
+ proto.LogLevel_WARN,
+ fmt.Sprintf("terraform environment variable: %s=%s", parts[0], parts[1]),
+ )
}
}
}
diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go
index c882807a8d434..254ddec45b5e6 100644
--- a/provisioner/terraform/provision_test.go
+++ b/provisioner/terraform/provision_test.go
@@ -3,6 +3,8 @@
package terraform_test
import (
+ "archive/tar"
+ "bytes"
"context"
"encoding/json"
"errors"
@@ -20,9 +22,9 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/provisioner/terraform"
- "github.com/coder/coder/provisionersdk"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/provisioner/terraform"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
type provisionerServeOptions struct {
@@ -35,6 +37,7 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont
opts = &provisionerServeOptions{}
}
cachePath := t.TempDir()
+ workDir := t.TempDir()
client, server := provisionersdk.MemTransportPipe()
ctx, cancelFunc := context.WithCancel(context.Background())
serverErr := make(chan error, 1)
@@ -50,40 +53,75 @@ func setupProvisioner(t *testing.T, opts *provisionerServeOptions) (context.Cont
go func() {
serverErr <- terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
- Listener: server,
+ Listener: server,
+ Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
+ WorkDirectory: workDir,
},
BinaryPath: opts.binaryPath,
CachePath: cachePath,
- Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
ExitTimeout: opts.exitTimeout,
})
}()
api := proto.NewDRPCProvisionerClient(client)
+
return ctx, api
}
-func readProvisionLog(t *testing.T, response proto.DRPCProvisioner_ProvisionClient) (
- string,
- *proto.Provision_Complete,
-) {
- var (
- logBuf strings.Builder
- c *proto.Provision_Complete
- )
+func makeTar(t *testing.T, files map[string]string) []byte {
+ t.Helper()
+ var buffer bytes.Buffer
+ writer := tar.NewWriter(&buffer)
+ for name, content := range files {
+ err := writer.WriteHeader(&tar.Header{
+ Name: name,
+ Size: int64(len(content)),
+ Mode: 0o644,
+ })
+ require.NoError(t, err)
+ _, err = writer.Write([]byte(content))
+ require.NoError(t, err)
+ }
+ err := writer.Flush()
+ require.NoError(t, err)
+ return buffer.Bytes()
+}
+
+func configure(ctx context.Context, t *testing.T, client proto.DRPCProvisionerClient, config *proto.Config) proto.DRPCProvisioner_SessionClient {
+ t.Helper()
+ sess, err := client.Session(ctx)
+ require.NoError(t, err)
+ err = sess.Send(&proto.Request{Type: &proto.Request_Config{Config: config}})
+ require.NoError(t, err)
+ return sess
+}
+
+func readProvisionLog(t *testing.T, response proto.DRPCProvisioner_SessionClient) string {
+ var logBuf strings.Builder
for {
msg, err := response.Recv()
require.NoError(t, err)
if log := msg.GetLog(); log != nil {
t.Log(log.Level.String(), log.Output)
- _, _ = logBuf.WriteString(log.Output)
- }
- if c = msg.GetComplete(); c != nil {
- require.Empty(t, c.Error)
- break
+ _, err = logBuf.WriteString(log.Output)
+ require.NoError(t, err)
+ continue
}
+ break
}
- return logBuf.String(), c
+ return logBuf.String()
+}
+
+func sendPlan(sess proto.DRPCProvisioner_SessionClient, transition proto.WorkspaceTransition) error {
+ return sess.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{
+ Metadata: &proto.Metadata{WorkspaceTransition: transition},
+ }}})
+}
+
+func sendApply(sess proto.DRPCProvisioner_SessionClient, transition proto.WorkspaceTransition) error {
+ return sess.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{
+ Metadata: &proto.Metadata{WorkspaceTransition: transition},
+ }}})
}
func TestProvision_Cancel(t *testing.T) {
@@ -109,9 +147,10 @@ func TestProvision_Cancel(t *testing.T) {
wantLog: []string{"interrupt", "exit"},
},
{
- name: "Cancel apply",
- mode: "apply",
- startSequence: []string{"init", "apply_start"},
+ // Provisioner requires a plan before an apply, so test cancel with plan.
+ name: "Cancel plan",
+ mode: "plan",
+ startSequence: []string{"init", "plan_start"},
wantLog: []string{"interrupt", "exit"},
},
}
@@ -131,24 +170,16 @@ func TestProvision_Cancel(t *testing.T) {
ctx, api := setupProvisioner(t, &provisionerServeOptions{
binaryPath: binPath,
})
-
- response, err := api.Provision(ctx)
- require.NoError(t, err)
- err = response.Send(&proto.Provision_Request{
- Type: &proto.Provision_Request_Apply{
- Apply: &proto.Provision_Apply{
- Config: &proto.Provision_Config{
- Directory: dir,
- Metadata: &proto.Provision_Metadata{},
- },
- },
- },
+ sess := configure(ctx, t, api, &proto.Config{
+ TemplateSourceArchive: makeTar(t, nil),
})
+
+ err = sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
for _, line := range tt.startSequence {
LoopStart:
- msg, err := response.Recv()
+ msg, err := sess.Recv()
require.NoError(t, err)
t.Log(msg.Type)
@@ -160,22 +191,22 @@ func TestProvision_Cancel(t *testing.T) {
require.Equal(t, line, log.Output)
}
- err = response.Send(&proto.Provision_Request{
- Type: &proto.Provision_Request_Cancel{
- Cancel: &proto.Provision_Cancel{},
+ err = sess.Send(&proto.Request{
+ Type: &proto.Request_Cancel{
+ Cancel: &proto.CancelRequest{},
},
})
require.NoError(t, err)
var gotLog []string
for {
- msg, err := response.Recv()
+ msg, err := sess.Recv()
require.NoError(t, err)
if log := msg.GetLog(); log != nil {
gotLog = append(gotLog, log.Output)
}
- if c := msg.GetComplete(); c != nil {
+ if c := msg.GetPlan(); c != nil {
require.Contains(t, c.Error, "exit status 1")
break
}
@@ -208,23 +239,17 @@ func TestProvision_CancelTimeout(t *testing.T) {
exitTimeout: time.Second,
})
- response, err := api.Provision(ctx)
- require.NoError(t, err)
- err = response.Send(&proto.Provision_Request{
- Type: &proto.Provision_Request_Apply{
- Apply: &proto.Provision_Apply{
- Config: &proto.Provision_Config{
- Directory: dir,
- Metadata: &proto.Provision_Metadata{},
- },
- },
- },
+ sess := configure(ctx, t, api, &proto.Config{
+ TemplateSourceArchive: makeTar(t, nil),
})
+
+ // provisioner requires plan before apply, so test cancel with plan.
+ err = sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
- for _, line := range []string{"init", "apply_start"} {
+ for _, line := range []string{"init", "plan_start"} {
LoopStart:
- msg, err := response.Recv()
+ msg, err := sess.Recv()
require.NoError(t, err)
t.Log(msg.Type)
@@ -236,18 +261,14 @@ func TestProvision_CancelTimeout(t *testing.T) {
require.Equal(t, line, log.Output)
}
- err = response.Send(&proto.Provision_Request{
- Type: &proto.Provision_Request_Cancel{
- Cancel: &proto.Provision_Cancel{},
- },
- })
+ err = sess.Send(&proto.Request{Type: &proto.Request_Cancel{Cancel: &proto.CancelRequest{}}})
require.NoError(t, err)
for {
- msg, err := response.Recv()
+ msg, err := sess.Recv()
require.NoError(t, err)
- if c := msg.GetComplete(); c != nil {
+ if c := msg.GetPlan(); c != nil {
require.Contains(t, c.Error, "killed")
break
}
@@ -258,17 +279,18 @@ func TestProvision(t *testing.T) {
t.Parallel()
testCases := []struct {
- Name string
- Files map[string]string
- Request *proto.Provision_Plan
+ Name string
+ Files map[string]string
+ Metadata *proto.Metadata
+ Request *proto.PlanRequest
// Response may be nil to not check the response.
- Response *proto.Provision_Response
- // If ErrorContains is not empty, then response.Recv() should return an
- // error containing this string before a Complete response is returned.
+ Response *proto.PlanComplete
+ // If ErrorContains is not empty, PlanComplete should have an Error containing the given string
ErrorContains string
// If ExpectLogContains is not empty, then the logs should contain it.
ExpectLogContains string
- Apply bool
+ // If Apply is true, then send an Apply request and check we get the same Resources as in Response.
+ Apply bool
}{
{
Name: "missing-variable",
@@ -293,15 +315,11 @@ func TestProvision(t *testing.T) {
Files: map[string]string{
"main.tf": `resource "null_resource" "A" {}`,
},
- Response: &proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Resources: []*proto.Resource{{
- Name: "A",
- Type: "null_resource",
- }},
- },
- },
+ Response: &proto.PlanComplete{
+ Resources: []*proto.Resource{{
+ Name: "A",
+ Type: "null_resource",
+ }},
},
},
{
@@ -309,15 +327,11 @@ func TestProvision(t *testing.T) {
Files: map[string]string{
"main.tf": `resource "null_resource" "A" {}`,
},
- Response: &proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Resources: []*proto.Resource{{
- Name: "A",
- Type: "null_resource",
- }},
- },
- },
+ Response: &proto.PlanComplete{
+ Resources: []*proto.Resource{{
+ Name: "A",
+ Type: "null_resource",
+ }},
},
Apply: true,
},
@@ -334,15 +348,11 @@ func TestProvision(t *testing.T) {
}
}`,
},
- Response: &proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Resources: []*proto.Resource{{
- Name: "A",
- Type: "null_resource",
- }},
- },
- },
+ Response: &proto.PlanComplete{
+ Resources: []*proto.Resource{{
+ Name: "A",
+ Type: "null_resource",
+ }},
},
Apply: true,
},
@@ -351,7 +361,7 @@ func TestProvision(t *testing.T) {
Files: map[string]string{
"main.tf": `a`,
},
- ErrorContains: "plan terraform",
+ ErrorContains: "initialize terraform",
ExpectLogContains: "Argument or block definition required",
},
{
@@ -359,7 +369,7 @@ func TestProvision(t *testing.T) {
Files: map[string]string{
"main.tf": `;asdf;`,
},
- ErrorContains: "plan terraform",
+ ErrorContains: "initialize terraform",
ExpectLogContains: `The ";" character is not valid.`,
},
{
@@ -367,12 +377,8 @@ func TestProvision(t *testing.T) {
Files: map[string]string{
"main.tf": `resource "null_resource" "A" {}`,
},
- Request: &proto.Provision_Plan{
- Config: &proto.Provision_Config{
- Metadata: &proto.Provision_Metadata{
- WorkspaceTransition: proto.WorkspaceTransition_DESTROY,
- },
- },
+ Metadata: &proto.Metadata{
+ WorkspaceTransition: proto.WorkspaceTransition_DESTROY,
},
ExpectLogContains: "nothing to do",
},
@@ -406,7 +412,7 @@ func TestProvision(t *testing.T) {
}
}`,
},
- Request: &proto.Provision_Plan{
+ Request: &proto.PlanRequest{
RichParameterValues: []*proto.RichParameterValue{
{
Name: "Example",
@@ -418,27 +424,23 @@ func TestProvision(t *testing.T) {
},
},
},
- Response: &proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: []*proto.RichParameter{
- {
- Name: "Example",
- Type: "string",
- DefaultValue: "foobar",
- },
- {
- Name: "Sample",
- Type: "string",
- DefaultValue: "foobaz",
- },
- },
- Resources: []*proto.Resource{{
- Name: "example",
- Type: "null_resource",
- }},
+ Response: &proto.PlanComplete{
+ Parameters: []*proto.RichParameter{
+ {
+ Name: "Example",
+ Type: "string",
+ DefaultValue: "foobar",
+ },
+ {
+ Name: "Sample",
+ Type: "string",
+ DefaultValue: "foobaz",
},
},
+ Resources: []*proto.Resource{{
+ Name: "example",
+ Type: "null_resource",
+ }},
},
},
{
@@ -488,7 +490,7 @@ func TestProvision(t *testing.T) {
]
}`,
},
- Request: &proto.Provision_Plan{
+ Request: &proto.PlanRequest{
RichParameterValues: []*proto.RichParameterValue{
{
Name: "Example",
@@ -500,27 +502,23 @@ func TestProvision(t *testing.T) {
},
},
},
- Response: &proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Parameters: []*proto.RichParameter{
- {
- Name: "Example",
- Type: "string",
- DefaultValue: "foobar",
- },
- {
- Name: "Sample",
- Type: "string",
- DefaultValue: "foobaz",
- },
- },
- Resources: []*proto.Resource{{
- Name: "example",
- Type: "null_resource",
- }},
+ Response: &proto.PlanComplete{
+ Parameters: []*proto.RichParameter{
+ {
+ Name: "Example",
+ Type: "string",
+ DefaultValue: "foobar",
+ },
+ {
+ Name: "Sample",
+ Type: "string",
+ DefaultValue: "foobaz",
},
},
+ Resources: []*proto.Resource{{
+ Name: "example",
+ Type: "null_resource",
+ }},
},
},
{
@@ -550,25 +548,21 @@ func TestProvision(t *testing.T) {
}
`,
},
- Request: &proto.Provision_Plan{
+ Request: &proto.PlanRequest{
GitAuthProviders: []*proto.GitAuthProvider{{
Id: "github",
AccessToken: "some-value",
}},
},
- Response: &proto.Provision_Response{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
- Resources: []*proto.Resource{{
- Name: "example",
- Type: "null_resource",
- Metadata: []*proto.Resource_Metadata{{
- Key: "token",
- Value: "some-value",
- }},
- }},
- },
- },
+ Response: &proto.PlanComplete{
+ Resources: []*proto.Resource{{
+ Name: "example",
+ Type: "null_resource",
+ Metadata: []*proto.Resource_Metadata{{
+ Key: "token",
+ Value: "some-value",
+ }},
+ }},
},
},
}
@@ -579,50 +573,26 @@ func TestProvision(t *testing.T) {
t.Parallel()
ctx, api := setupProvisioner(t, nil)
+ sess := configure(ctx, t, api, &proto.Config{
+ TemplateSourceArchive: makeTar(t, testCase.Files),
+ })
- directory := t.TempDir()
- for path, content := range testCase.Files {
- err := os.WriteFile(filepath.Join(directory, path), []byte(content), 0o600)
- require.NoError(t, err)
- }
-
- planRequest := &proto.Provision_Request{
- Type: &proto.Provision_Request_Plan{
- Plan: &proto.Provision_Plan{
- Config: &proto.Provision_Config{
- Directory: directory,
- },
- },
- },
- }
+ planRequest := &proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{
+ Metadata: testCase.Metadata,
+ }}}
if testCase.Request != nil {
- if planRequest.GetPlan().GetConfig() == nil {
- planRequest.GetPlan().Config = &proto.Provision_Config{}
- }
- planRequest.GetPlan().RichParameterValues = testCase.Request.RichParameterValues
- planRequest.GetPlan().GitAuthProviders = testCase.Request.GitAuthProviders
- if testCase.Request.Config != nil {
- planRequest.GetPlan().Config.State = testCase.Request.Config.State
- planRequest.GetPlan().Config.Metadata = testCase.Request.Config.Metadata
- }
- }
- if planRequest.GetPlan().Config.Metadata == nil {
- planRequest.GetPlan().Config.Metadata = &proto.Provision_Metadata{}
+ planRequest = &proto.Request{Type: &proto.Request_Plan{Plan: testCase.Request}}
}
gotExpectedLog := testCase.ExpectLogContains == ""
- provision := func(req *proto.Provision_Request) *proto.Provision_Complete {
- response, err := api.Provision(ctx)
- require.NoError(t, err)
- err = response.Send(req)
+ provision := func(req *proto.Request) *proto.Response {
+ err := sess.Send(req)
require.NoError(t, err)
-
- var complete *proto.Provision_Complete
-
for {
- msg, err := response.Recv()
- if msg != nil && msg.GetLog() != nil {
+ msg, err := sess.Recv()
+ require.NoError(t, err)
+ if msg.GetLog() != nil {
if testCase.ExpectLogContains != "" && strings.Contains(msg.GetLog().Output, testCase.ExpectLogContains) {
gotExpectedLog = true
}
@@ -630,67 +600,51 @@ func TestProvision(t *testing.T) {
t.Logf("log: [%s] %s", msg.GetLog().Level, msg.GetLog().Output)
continue
}
- if testCase.ErrorContains != "" {
- require.ErrorContains(t, err, testCase.ErrorContains)
- break
- }
- require.NoError(t, err)
-
- if complete = msg.GetComplete(); complete == nil {
- continue
- }
-
- require.NoError(t, err)
-
- // Remove randomly generated data.
- for _, resource := range msg.GetComplete().Resources {
- sort.Slice(resource.Agents, func(i, j int) bool {
- return resource.Agents[i].Name < resource.Agents[j].Name
- })
-
- for _, agent := range resource.Agents {
- agent.Id = ""
- if agent.GetToken() == "" {
- continue
- }
- agent.Auth = &proto.Agent_Token{}
- }
- }
+ return msg
+ }
+ }
- if testCase.Response != nil {
- require.Equal(t, testCase.Response.GetComplete().Error, msg.GetComplete().Error)
+ resp := provision(planRequest)
+ planComplete := resp.GetPlan()
+ require.NotNil(t, planComplete)
- resourcesGot, err := json.Marshal(msg.GetComplete().Resources)
- require.NoError(t, err)
- resourcesWant, err := json.Marshal(testCase.Response.GetComplete().Resources)
- require.NoError(t, err)
+ if testCase.ErrorContains != "" {
+ require.Contains(t, planComplete.GetError(), testCase.ErrorContains)
+ }
- require.Equal(t, string(resourcesWant), string(resourcesGot))
+ if testCase.Response != nil {
+ require.Equal(t, testCase.Response.Error, planComplete.Error)
- parametersGot, err := json.Marshal(msg.GetComplete().Parameters)
- require.NoError(t, err)
- parametersWant, err := json.Marshal(testCase.Response.GetComplete().Parameters)
- require.NoError(t, err)
- require.Equal(t, string(parametersWant), string(parametersGot))
- }
- break
- }
+ // Remove randomly generated data.
+ normalizeResources(planComplete.Resources)
+ resourcesGot, err := json.Marshal(planComplete.Resources)
+ require.NoError(t, err)
+ resourcesWant, err := json.Marshal(testCase.Response.Resources)
+ require.NoError(t, err)
+ require.Equal(t, string(resourcesWant), string(resourcesGot))
- return complete
+ parametersGot, err := json.Marshal(planComplete.Parameters)
+ require.NoError(t, err)
+ parametersWant, err := json.Marshal(testCase.Response.Parameters)
+ require.NoError(t, err)
+ require.Equal(t, string(parametersWant), string(parametersGot))
}
- planComplete := provision(planRequest)
-
if testCase.Apply {
- require.NotNil(t, planComplete.Plan)
- provision(&proto.Provision_Request{
- Type: &proto.Provision_Request_Apply{
- Apply: &proto.Provision_Apply{
- Config: planRequest.GetPlan().GetConfig(),
- Plan: planComplete.Plan,
- },
- },
- })
+ resp = provision(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{
+ Metadata: &proto.Metadata{WorkspaceTransition: proto.WorkspaceTransition_START},
+ }}})
+ applyComplete := resp.GetApply()
+ require.NotNil(t, applyComplete)
+
+ if testCase.Response != nil {
+ normalizeResources(applyComplete.Resources)
+ resourcesGot, err := json.Marshal(applyComplete.Resources)
+ require.NoError(t, err)
+ resourcesWant, err := json.Marshal(testCase.Response.Resources)
+ require.NoError(t, err)
+ require.Equal(t, string(resourcesWant), string(resourcesGot))
+ }
}
if !gotExpectedLog {
@@ -700,6 +654,22 @@ func TestProvision(t *testing.T) {
}
}
+func normalizeResources(resources []*proto.Resource) {
+ for _, resource := range resources {
+ sort.Slice(resource.Agents, func(i, j int) bool {
+ return resource.Agents[i].Name < resource.Agents[j].Name
+ })
+
+ for _, agent := range resource.Agents {
+ agent.Id = ""
+ if agent.GetToken() == "" {
+ continue
+ }
+ agent.Auth = &proto.Agent_Token{}
+ }
+ }
+}
+
// nolint:paralleltest
func TestProvision_ExtraEnv(t *testing.T) {
// #nosec
@@ -708,31 +678,15 @@ func TestProvision_ExtraEnv(t *testing.T) {
t.Setenv("TF_SUPERSECRET", secretValue)
ctx, api := setupProvisioner(t, nil)
+ sess := configure(ctx, t, api, &proto.Config{
+ TemplateSourceArchive: makeTar(t, map[string]string{"main.tf": `resource "null_resource" "A" {}`}),
+ })
- directory := t.TempDir()
- path := filepath.Join(directory, "main.tf")
- err := os.WriteFile(path, []byte(`resource "null_resource" "A" {}`), 0o600)
- require.NoError(t, err)
-
- request := &proto.Provision_Request{
- Type: &proto.Provision_Request_Plan{
- Plan: &proto.Provision_Plan{
- Config: &proto.Provision_Config{
- Directory: directory,
- Metadata: &proto.Provision_Metadata{
- WorkspaceTransition: proto.WorkspaceTransition_START,
- },
- },
- },
- },
- }
- response, err := api.Provision(ctx)
- require.NoError(t, err)
- err = response.Send(request)
+ err := sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
found := false
for {
- msg, err := response.Recv()
+ msg, err := sess.Recv()
require.NoError(t, err)
if log := msg.GetLog(); log != nil {
@@ -742,7 +696,7 @@ func TestProvision_ExtraEnv(t *testing.T) {
}
require.NotContains(t, log.Output, secretValue)
}
- if c := msg.GetComplete(); c != nil {
+ if c := msg.GetPlan(); c != nil {
require.Empty(t, c.Error)
break
}
@@ -774,48 +728,19 @@ func TestProvision_SafeEnv(t *testing.T) {
`
ctx, api := setupProvisioner(t, nil)
-
- directory := t.TempDir()
- path := filepath.Join(directory, "main.tf")
- err := os.WriteFile(path, []byte(echoResource), 0o600)
- require.NoError(t, err)
-
- response, err := api.Provision(ctx)
- require.NoError(t, err)
- err = response.Send(&proto.Provision_Request{
- Type: &proto.Provision_Request_Plan{
- Plan: &proto.Provision_Plan{
- Config: &proto.Provision_Config{
- Directory: directory,
- Metadata: &proto.Provision_Metadata{
- WorkspaceTransition: proto.WorkspaceTransition_START,
- },
- },
- },
- },
+ sess := configure(ctx, t, api, &proto.Config{
+ TemplateSourceArchive: makeTar(t, map[string]string{"main.tf": echoResource}),
})
+
+ err := sendPlan(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
- _, complete := readProvisionLog(t, response)
+ _ = readProvisionLog(t, sess)
- response, err = api.Provision(ctx)
- require.NoError(t, err)
- err = response.Send(&proto.Provision_Request{
- Type: &proto.Provision_Request_Apply{
- Apply: &proto.Provision_Apply{
- Config: &proto.Provision_Config{
- Directory: directory,
- Metadata: &proto.Provision_Metadata{
- WorkspaceTransition: proto.WorkspaceTransition_START,
- },
- },
- Plan: complete.GetPlan(),
- },
- },
- })
+ err = sendApply(sess, proto.WorkspaceTransition_START)
require.NoError(t, err)
- log, _ := readProvisionLog(t, response)
+ log := readProvisionLog(t, sess)
require.Contains(t, log, passedValue)
require.NotContains(t, log, secretValue)
require.Contains(t, log, "CODER_")
diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go
index c98e21fb21789..d2e1541c7c89b 100644
--- a/provisioner/terraform/resources.go
+++ b/provisioner/terraform/resources.go
@@ -11,11 +11,11 @@ import (
"github.com/coder/terraform-provider-coder/provider"
- "github.com/coder/coder/coderd/util/slice"
- stringutil "github.com/coder/coder/coderd/util/strings"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/provisioner"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/util/slice"
+ stringutil "github.com/coder/coder/v2/coderd/util/strings"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/provisioner"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
type agentMetadata struct {
@@ -97,7 +97,6 @@ type State struct {
// ConvertState consumes Terraform state and a GraphViz representation
// produced by `terraform graph` to produce resources consumable by Coder.
-// nolint:gocyclo
func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error) {
parsedGraph, err := gographviz.ParseString(rawGraph)
if err != nil {
diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go
index 5c85b829dd77a..f0057a22de3d6 100644
--- a/provisioner/terraform/resources_test.go
+++ b/provisioner/terraform/resources_test.go
@@ -13,9 +13,9 @@ import (
"github.com/stretchr/testify/require"
protobuf "google.golang.org/protobuf/proto"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/provisioner/terraform"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/provisioner/terraform"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
func TestConvertResources(t *testing.T) {
diff --git a/provisioner/terraform/serve.go b/provisioner/terraform/serve.go
index 23f880e6c0418..0fc12ea870896 100644
--- a/provisioner/terraform/serve.go
+++ b/provisioner/terraform/serve.go
@@ -12,8 +12,8 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/unhanger"
- "github.com/coder/coder/provisionersdk"
+ "github.com/coder/coder/v2/coderd/unhanger"
+ "github.com/coder/coder/v2/provisionersdk"
)
type ServeOptions struct {
@@ -24,7 +24,6 @@ type ServeOptions struct {
BinaryPath string
// CachePath must not be used by multiple processes at once.
CachePath string
- Logger slog.Logger
Tracer trace.Tracer
// ExitTimeout defines how long we will wait for a running Terraform
@@ -128,5 +127,6 @@ func (s *server) executor(workdir string) *executor {
binaryPath: s.binaryPath,
cachePath: s.cachePath,
workdir: workdir,
+ logger: s.logger.Named("executor"),
}
}
diff --git a/provisioner/terraform/testdata/fake_cancel.sh b/provisioner/terraform/testdata/fake_cancel.sh
index cd2511facf938..2ea713379cce9 100755
--- a/provisioner/terraform/testdata/fake_cancel.sh
+++ b/provisioner/terraform/testdata/fake_cancel.sh
@@ -22,8 +22,9 @@ version)
;;
init)
case "$MODE" in
- apply)
+ plan)
echo "init"
+ exit 0
;;
init)
sleep 10 &
@@ -39,7 +40,7 @@ init)
;;
esac
;;
-apply)
+plan)
sleep 10 &
sleep_pid=$!
@@ -47,14 +48,14 @@ apply)
trap 'json_print interrupt; exit 1' INT
trap 'json_print terminate; exit 2' TERM
- json_print apply_start
+ json_print plan_start
wait
- json_print apply_end
+ json_print plan_end
;;
-plan)
- echo "plan not supported"
+apply)
+ echo "apply not supported"
exit 1
;;
esac
-exit 0
+exit 10
diff --git a/provisioner/terraform/testdata/fake_cancel_hang.sh b/provisioner/terraform/testdata/fake_cancel_hang.sh
index c6d29c88c733f..e8db67f6837cd 100755
--- a/provisioner/terraform/testdata/fake_cancel_hang.sh
+++ b/provisioner/terraform/testdata/fake_cancel_hang.sh
@@ -23,19 +23,19 @@ init)
echo "init"
exit 0
;;
-apply)
+plan)
trap 'json_print interrupt' INT
- json_print apply_start
+ json_print plan_start
sleep 10 2>/dev/null >/dev/null
- json_print apply_end
+ json_print plan_end
exit 0
;;
-plan)
- echo "plan not supported"
+apply)
+ echo "apply not supported"
exit 1
;;
esac
-exit 0
+exit 10
diff --git a/provisionerd/proto/provisionerd.pb.go b/provisionerd/proto/provisionerd.pb.go
index d7db84d69813e..018e0f25ac8e1 100644
--- a/provisionerd/proto/provisionerd.pb.go
+++ b/provisionerd/proto/provisionerd.pb.go
@@ -7,7 +7,7 @@
package proto
import (
- proto "github.com/coder/coder/provisionersdk/proto"
+ proto "github.com/coder/coder/v2/provisionersdk/proto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@@ -819,7 +819,7 @@ type AcquiredJob_WorkspaceBuild struct {
RichParameterValues []*proto.RichParameterValue `protobuf:"bytes,4,rep,name=rich_parameter_values,json=richParameterValues,proto3" json:"rich_parameter_values,omitempty"`
VariableValues []*proto.VariableValue `protobuf:"bytes,5,rep,name=variable_values,json=variableValues,proto3" json:"variable_values,omitempty"`
GitAuthProviders []*proto.GitAuthProvider `protobuf:"bytes,6,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"`
- Metadata *proto.Provision_Metadata `protobuf:"bytes,7,opt,name=metadata,proto3" json:"metadata,omitempty"`
+ Metadata *proto.Metadata `protobuf:"bytes,7,opt,name=metadata,proto3" json:"metadata,omitempty"`
State []byte `protobuf:"bytes,8,opt,name=state,proto3" json:"state,omitempty"`
LogLevel string `protobuf:"bytes,9,opt,name=log_level,json=logLevel,proto3" json:"log_level,omitempty"`
}
@@ -891,7 +891,7 @@ func (x *AcquiredJob_WorkspaceBuild) GetGitAuthProviders() []*proto.GitAuthProvi
return nil
}
-func (x *AcquiredJob_WorkspaceBuild) GetMetadata() *proto.Provision_Metadata {
+func (x *AcquiredJob_WorkspaceBuild) GetMetadata() *proto.Metadata {
if x != nil {
return x.Metadata
}
@@ -917,8 +917,8 @@ type AcquiredJob_TemplateImport struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Metadata *proto.Provision_Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
- UserVariableValues []*proto.VariableValue `protobuf:"bytes,2,rep,name=user_variable_values,json=userVariableValues,proto3" json:"user_variable_values,omitempty"`
+ Metadata *proto.Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
+ UserVariableValues []*proto.VariableValue `protobuf:"bytes,2,rep,name=user_variable_values,json=userVariableValues,proto3" json:"user_variable_values,omitempty"`
}
func (x *AcquiredJob_TemplateImport) Reset() {
@@ -953,7 +953,7 @@ func (*AcquiredJob_TemplateImport) Descriptor() ([]byte, []int) {
return file_provisionerd_proto_provisionerd_proto_rawDescGZIP(), []int{1, 1}
}
-func (x *AcquiredJob_TemplateImport) GetMetadata() *proto.Provision_Metadata {
+func (x *AcquiredJob_TemplateImport) GetMetadata() *proto.Metadata {
if x != nil {
return x.Metadata
}
@@ -974,7 +974,7 @@ type AcquiredJob_TemplateDryRun struct {
RichParameterValues []*proto.RichParameterValue `protobuf:"bytes,2,rep,name=rich_parameter_values,json=richParameterValues,proto3" json:"rich_parameter_values,omitempty"`
VariableValues []*proto.VariableValue `protobuf:"bytes,3,rep,name=variable_values,json=variableValues,proto3" json:"variable_values,omitempty"`
- Metadata *proto.Provision_Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
+ Metadata *proto.Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
}
func (x *AcquiredJob_TemplateDryRun) Reset() {
@@ -1023,7 +1023,7 @@ func (x *AcquiredJob_TemplateDryRun) GetVariableValues() []*proto.VariableValue
return nil
}
-func (x *AcquiredJob_TemplateDryRun) GetMetadata() *proto.Provision_Metadata {
+func (x *AcquiredJob_TemplateDryRun) GetMetadata() *proto.Metadata {
if x != nil {
return x.Metadata
}
@@ -1335,7 +1335,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{
0x6f, 0x6e, 0x65, 0x72, 0x64, 0x1a, 0x26, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x76,
0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 0x0a,
- 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xab, 0x0b, 0x0a, 0x0b, 0x41, 0x63, 0x71, 0x75, 0x69,
+ 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x8d, 0x0b, 0x0a, 0x0b, 0x41, 0x63, 0x71, 0x75, 0x69,
0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x1d, 0x0a,
0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
@@ -1368,7 +1368,7 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{
0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69,
0x72, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x63, 0x65,
- 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xc1, 0x03, 0x0a, 0x0e, 0x57, 0x6f, 0x72,
+ 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xb7, 0x03, 0x0a, 0x0e, 0x57, 0x6f, 0x72,
0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77,
0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61,
@@ -1389,193 +1389,191 @@ var file_provisionerd_proto_provisionerd_proto_rawDesc = []byte{
0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x47, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76,
0x69, 0x64, 0x65, 0x72, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f,
- 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
- 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
- 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
- 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
- 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01,
- 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x6f, 0x67,
- 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f,
- 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x1a, 0x9b, 0x01, 0x0a,
- 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12,
- 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28,
- 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e,
- 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
- 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x4c, 0x0a, 0x14,
- 0x75, 0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61,
- 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f,
- 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c,
- 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72, 0x69,
- 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0xed, 0x01, 0x0a, 0x0e, 0x54,
- 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x53, 0x0a,
- 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f,
- 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70,
- 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50,
- 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72,
- 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75,
- 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76,
- 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72,
- 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62,
- 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c,
- 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
- 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
- 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61,
- 0x64, 0x61, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x1a, 0x40, 0x0a, 0x12, 0x54, 0x72,
- 0x61, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79,
- 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
- 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04,
- 0x74, 0x79, 0x70, 0x65, 0x22, 0xa5, 0x03, 0x0a, 0x09, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a,
- 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72,
- 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12,
- 0x51, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69,
- 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
- 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f,
- 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64,
- 0x48, 0x00, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69,
- 0x6c, 0x64, 0x12, 0x51, 0x0a, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69,
- 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72,
- 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65,
- 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70,
- 0x6f, 0x72, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49,
- 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x52, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74,
- 0x65, 0x5f, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32,
- 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46,
- 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74,
+ 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
+ 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
+ 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52,
+ 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61,
+ 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12,
+ 0x1b, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x09, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x4a, 0x04, 0x08, 0x03,
+ 0x10, 0x04, 0x1a, 0x91, 0x01, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49,
+ 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+ 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73,
+ 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08,
+ 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x4c, 0x0a, 0x14, 0x75, 0x73, 0x65, 0x72,
+ 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73,
+ 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
+ 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c,
+ 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65,
+ 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0xe3, 0x01, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c,
+ 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63,
+ 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75,
+ 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
+ 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d,
+ 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50,
+ 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43,
+ 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65,
+ 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73,
+ 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61,
+ 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c,
+ 0x75, 0x65, 0x73, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18,
+ 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
+ 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65,
+ 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x1a, 0x40, 0x0a, 0x12,
+ 0x54, 0x72, 0x61, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74,
+ 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06,
+ 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xa5, 0x03, 0x0a, 0x09, 0x46, 0x61, 0x69, 0x6c, 0x65,
+ 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
+ 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
+ 0x72, 0x12, 0x51, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62,
+ 0x75, 0x69, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72, 0x6f,
+ 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64,
+ 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69,
+ 0x6c, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42,
+ 0x75, 0x69, 0x6c, 0x64, 0x12, 0x51, 0x0a, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65,
+ 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e,
+ 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x46, 0x61, 0x69,
+ 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49,
+ 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74,
+ 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x52, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c,
+ 0x61, 0x74, 0x65, 0x5f, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64,
+ 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c,
+ 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d,
+ 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x65,
+ 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x1a, 0x26, 0x0a, 0x0e, 0x57, 0x6f,
+ 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05,
+ 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61,
+ 0x74, 0x65, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d,
+ 0x70, 0x6f, 0x72, 0x74, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65,
+ 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xd8,
+ 0x05, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12,
+ 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x54, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70,
+ 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43,
+ 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b,
+ 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x77, 0x6f,
+ 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x54, 0x0a, 0x0f,
+ 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
+ 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f,
+ 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74,
+ 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f,
+ 0x72, 0x74, 0x12, 0x55, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x64,
+ 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70,
+ 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70,
+ 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74,
0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c,
- 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x72, 0x72,
- 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65,
- 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x1a, 0x26, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b,
- 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74,
- 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65,
- 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f,
- 0x72, 0x74, 0x1a, 0x10, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72,
- 0x79, 0x52, 0x75, 0x6e, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xd8, 0x05, 0x0a,
- 0x0c, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x15, 0x0a,
- 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a,
- 0x6f, 0x62, 0x49, 0x64, 0x12, 0x54, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63,
- 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e,
- 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d,
- 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70,
- 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x48, 0x00, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b,
- 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x54, 0x0a, 0x0f, 0x74, 0x65,
- 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
- 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e,
- 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x00,
- 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74,
- 0x12, 0x55, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x64, 0x72, 0x79,
- 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f,
- 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65,
- 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44,
- 0x72, 0x79, 0x52, 0x75, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74,
- 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x5b, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b, 0x73,
- 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61,
- 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12,
- 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03,
+ 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x1a, 0x5b, 0x0a, 0x0e, 0x57, 0x6f, 0x72,
+ 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73,
+ 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74,
+ 0x65, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02,
+ 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
+ 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x81, 0x02, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c,
+ 0x61, 0x74, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x0f, 0x73, 0x74, 0x61,
+ 0x72, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
- 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x73, 0x1a, 0x81, 0x02, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74,
- 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x3e, 0x0a, 0x0f, 0x73, 0x74, 0x61, 0x72, 0x74,
- 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
- 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x73, 0x74, 0x6f, 0x70, 0x5f,
- 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
+ 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74,
+ 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x73, 0x74, 0x6f,
+ 0x70, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
+ 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e,
+ 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x70, 0x52, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x72, 0x69, 0x63, 0x68, 0x5f,
+ 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
+ 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52,
+ 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0e, 0x72, 0x69,
+ 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x12,
+ 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
+ 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74,
+ 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x1a, 0x45, 0x0a, 0x0e, 0x54, 0x65,
+ 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x33, 0x0a, 0x09,
+ 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0d, 0x73, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61,
- 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a,
- 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63,
- 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0e, 0x72, 0x69, 0x63, 0x68,
- 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69,
- 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
- 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50,
- 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x1a, 0x45, 0x0a, 0x0e, 0x54, 0x65, 0x6d, 0x70,
- 0x6c, 0x61, 0x74, 0x65, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e,
- 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42,
- 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12,
- 0x2f, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
- 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c,
- 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
- 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f,
- 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a,
- 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
- 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x14, 0x0a, 0x05,
- 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61,
- 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x8a, 0x02, 0x0a, 0x10, 0x55,
- 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
- 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x02,
- 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
- 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x12, 0x4c, 0x0a,
- 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62,
- 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65,
- 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61,
- 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x75,
- 0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c,
- 0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65,
- 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72, 0x69, 0x61,
- 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61,
- 0x64, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d,
- 0x65, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x7a, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74,
- 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08,
- 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08,
- 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69,
- 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
- 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e,
- 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76,
- 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x4a, 0x04, 0x08,
- 0x02, 0x10, 0x03, 0x22, 0x4a, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f,
- 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62,
- 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64,
- 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x22,
- 0x68, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74,
- 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05,
- 0x52, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65,
- 0x64, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
- 0x05, 0x52, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x2a, 0x34, 0x0a, 0x09, 0x4c, 0x6f, 0x67,
- 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53,
- 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, 0x4e, 0x10, 0x00, 0x12, 0x0f,
- 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x10, 0x01, 0x32,
- 0xec, 0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x44,
- 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65,
- 0x4a, 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
- 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
- 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64,
- 0x4a, 0x6f, 0x62, 0x12, 0x52, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f,
- 0x74, 0x61, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
- 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x73, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x03, 0x4c, 0x6f,
+ 0x67, 0x12, 0x2f, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0e, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64,
+ 0x2e, 0x4c, 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72,
+ 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x0e, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e,
+ 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12,
+ 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x14,
+ 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73,
+ 0x74, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x05,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x8a, 0x02, 0x0a,
+ 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x04, 0x6c, 0x6f, 0x67, 0x73,
+ 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
+ 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x12,
+ 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69,
+ 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72,
+ 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61,
+ 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70,
+ 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x4c, 0x0a,
+ 0x14, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76,
+ 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72,
+ 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62,
+ 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x75, 0x73, 0x65, 0x72, 0x56, 0x61, 0x72,
+ 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72,
+ 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61,
+ 0x64, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x7a, 0x0a, 0x11, 0x55, 0x70, 0x64,
+ 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a,
+ 0x0a, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
+ 0x52, 0x08, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61,
+ 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
+ 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
+ 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52,
+ 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x4a,
+ 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x4a, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51,
+ 0x75, 0x6f, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6a,
+ 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62,
+ 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73,
+ 0x74, 0x22, 0x68, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x72, 0x65, 0x64,
+ 0x69, 0x74, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x05, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x73, 0x75,
+ 0x6d, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x05, 0x52, 0x06, 0x62, 0x75, 0x64, 0x67, 0x65, 0x74, 0x2a, 0x34, 0x0a, 0x09, 0x4c,
+ 0x6f, 0x67, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x52, 0x4f, 0x56,
+ 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x5f, 0x44, 0x41, 0x45, 0x4d, 0x4f, 0x4e, 0x10, 0x00,
+ 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x52, 0x10,
+ 0x01, 0x32, 0xec, 0x02, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
+ 0x72, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0a, 0x41, 0x63, 0x71, 0x75, 0x69,
+ 0x72, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
+ 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f,
+ 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x41, 0x63, 0x71, 0x75, 0x69, 0x72,
+ 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x12, 0x52, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51,
+ 0x75, 0x6f, 0x74, 0x61, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x52,
- 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64, 0x61, 0x74,
- 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
- 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
- 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a, 0x6f, 0x62,
- 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e,
- 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3e,
- 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1a, 0x2e,
- 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d,
- 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2b,
- 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64,
- 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
- 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
- 0x74, 0x6f, 0x33,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
+ 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x51, 0x75, 0x6f, 0x74,
+ 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x55, 0x70, 0x64,
+ 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
+ 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
+ 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x46, 0x61, 0x69, 0x6c, 0x4a,
+ 0x6f, 0x62, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
+ 0x64, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72,
+ 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
+ 0x12, 0x3e, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x12,
+ 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x43,
+ 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x4a, 0x6f, 0x62, 0x1a, 0x13, 0x2e, 0x70, 0x72,
+ 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
+ 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63,
+ 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72,
+ 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -1618,7 +1616,7 @@ var file_provisionerd_proto_provisionerd_proto_goTypes = []interface{}{
(*proto.VariableValue)(nil), // 22: provisioner.VariableValue
(*proto.RichParameterValue)(nil), // 23: provisioner.RichParameterValue
(*proto.GitAuthProvider)(nil), // 24: provisioner.GitAuthProvider
- (*proto.Provision_Metadata)(nil), // 25: provisioner.Provision.Metadata
+ (*proto.Metadata)(nil), // 25: provisioner.Metadata
(*proto.Resource)(nil), // 26: provisioner.Resource
(*proto.RichParameter)(nil), // 27: provisioner.RichParameter
}
@@ -1642,12 +1640,12 @@ var file_provisionerd_proto_provisionerd_proto_depIdxs = []int32{
23, // 16: provisionerd.AcquiredJob.WorkspaceBuild.rich_parameter_values:type_name -> provisioner.RichParameterValue
22, // 17: provisionerd.AcquiredJob.WorkspaceBuild.variable_values:type_name -> provisioner.VariableValue
24, // 18: provisionerd.AcquiredJob.WorkspaceBuild.git_auth_providers:type_name -> provisioner.GitAuthProvider
- 25, // 19: provisionerd.AcquiredJob.WorkspaceBuild.metadata:type_name -> provisioner.Provision.Metadata
- 25, // 20: provisionerd.AcquiredJob.TemplateImport.metadata:type_name -> provisioner.Provision.Metadata
+ 25, // 19: provisionerd.AcquiredJob.WorkspaceBuild.metadata:type_name -> provisioner.Metadata
+ 25, // 20: provisionerd.AcquiredJob.TemplateImport.metadata:type_name -> provisioner.Metadata
22, // 21: provisionerd.AcquiredJob.TemplateImport.user_variable_values:type_name -> provisioner.VariableValue
23, // 22: provisionerd.AcquiredJob.TemplateDryRun.rich_parameter_values:type_name -> provisioner.RichParameterValue
22, // 23: provisionerd.AcquiredJob.TemplateDryRun.variable_values:type_name -> provisioner.VariableValue
- 25, // 24: provisionerd.AcquiredJob.TemplateDryRun.metadata:type_name -> provisioner.Provision.Metadata
+ 25, // 24: provisionerd.AcquiredJob.TemplateDryRun.metadata:type_name -> provisioner.Metadata
26, // 25: provisionerd.CompletedJob.WorkspaceBuild.resources:type_name -> provisioner.Resource
26, // 26: provisionerd.CompletedJob.TemplateImport.start_resources:type_name -> provisioner.Resource
26, // 27: provisionerd.CompletedJob.TemplateImport.stop_resources:type_name -> provisioner.Resource
diff --git a/provisionerd/proto/provisionerd.proto b/provisionerd/proto/provisionerd.proto
index 1c44ceef37dc4..8d4fadffc6373 100644
--- a/provisionerd/proto/provisionerd.proto
+++ b/provisionerd/proto/provisionerd.proto
@@ -1,6 +1,6 @@
syntax = "proto3";
-option go_package = "github.com/coder/coder/provisionerd/proto";
+option go_package = "github.com/coder/coder/v2/provisionerd/proto";
package provisionerd;
@@ -19,12 +19,12 @@ message AcquiredJob {
repeated provisioner.RichParameterValue rich_parameter_values = 4;
repeated provisioner.VariableValue variable_values = 5;
repeated provisioner.GitAuthProvider git_auth_providers = 6;
- provisioner.Provision.Metadata metadata = 7;
+ provisioner.Metadata metadata = 7;
bytes state = 8;
string log_level = 9;
}
message TemplateImport {
- provisioner.Provision.Metadata metadata = 1;
+ provisioner.Metadata metadata = 1;
repeated provisioner.VariableValue user_variable_values = 2;
}
message TemplateDryRun {
@@ -32,7 +32,7 @@ message AcquiredJob {
repeated provisioner.RichParameterValue rich_parameter_values = 2;
repeated provisioner.VariableValue variable_values = 3;
- provisioner.Provision.Metadata metadata = 4;
+ provisioner.Metadata metadata = 4;
}
string job_id = 1;
@@ -45,9 +45,9 @@ message AcquiredJob {
TemplateImport template_import = 7;
TemplateDryRun template_dry_run = 8;
}
- // trace_metadata is currently used for tracing information only. It allows
- // jobs to be tied to the request that created them.
- map trace_metadata = 9;
+ // trace_metadata is currently used for tracing information only. It allows
+ // jobs to be tied to the request that created them.
+ map trace_metadata = 9;
}
message FailedJob {
@@ -113,7 +113,7 @@ message UpdateJobRequest {
string job_id = 1;
repeated Log logs = 2;
repeated provisioner.TemplateVariable template_variables = 4;
- repeated provisioner.VariableValue user_variable_values = 5;
+ repeated provisioner.VariableValue user_variable_values = 5;
bytes readme = 6;
}
@@ -121,7 +121,7 @@ message UpdateJobResponse {
reserved 2;
bool canceled = 1;
- repeated provisioner.VariableValue variable_values = 3;
+ repeated provisioner.VariableValue variable_values = 3;
}
message CommitQuotaRequest {
diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go
index d7e50f96bfab1..a341bd5a3df85 100644
--- a/provisionerd/provisionerd.go
+++ b/provisionerd/provisionerd.go
@@ -12,7 +12,6 @@ import (
"github.com/hashicorp/yamux"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
- "github.com/spf13/afero"
"github.com/valyala/fasthttp/fasthttputil"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.14.0"
@@ -21,12 +20,12 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/provisionerd/proto"
- "github.com/coder/coder/provisionerd/runner"
- sdkproto "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/provisionerd/proto"
+ "github.com/coder/coder/v2/provisionerd/runner"
+ sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/retry"
)
@@ -44,7 +43,6 @@ type Provisioners map[string]sdkproto.DRPCProvisionerClient
// Options provides customizations to the behavior of a provisioner daemon.
type Options struct {
- Filesystem afero.Fs
Logger slog.Logger
TracerProvider trace.TracerProvider
Metrics *Metrics
@@ -56,8 +54,6 @@ type Options struct {
JobPollJitter time.Duration
JobPollDebounce time.Duration
Provisioners Provisioners
- // WorkDirectory must not be used by multiple processes at once.
- WorkDirectory string
}
// New creates and starts a provisioner daemon.
@@ -80,9 +76,6 @@ func New(clientDialer Dialer, opts *Options) *Server {
if opts.LogBufferInterval == 0 {
opts.LogBufferInterval = 250 * time.Millisecond
}
- if opts.Filesystem == nil {
- opts.Filesystem = afero.NewOsFs()
- }
if opts.TracerProvider == nil {
opts.TracerProvider = trace.NewNoopTracerProvider()
}
@@ -204,7 +197,7 @@ func (p *Server) connect(ctx context.Context) {
p.clientValue.Store(ptr.Ref(client))
p.mutex.Unlock()
- p.opts.Logger.Debug(context.Background(), "connected")
+ p.opts.Logger.Debug(ctx, "successfully connected to coderd")
break
}
select {
@@ -404,9 +397,7 @@ func (p *Server) acquireJob(ctx context.Context) {
runner.Options{
Updater: p,
QuotaCommitter: p,
- Logger: p.opts.Logger,
- Filesystem: p.opts.Filesystem,
- WorkDirectory: p.opts.WorkDirectory,
+ Logger: p.opts.Logger.Named("runner"),
Provisioner: provisioner,
UpdateInterval: p.opts.UpdateInterval,
ForceCancelInterval: p.opts.ForceCancelInterval,
diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go
index 32e70fb8e1483..ee379e0ab9929 100644
--- a/provisionerd/provisionerd_test.go
+++ b/provisionerd/provisionerd_test.go
@@ -23,12 +23,11 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/provisionerd"
- "github.com/coder/coder/provisionerd/proto"
- "github.com/coder/coder/provisionerd/runner"
- "github.com/coder/coder/provisionersdk"
- sdkproto "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/provisionerd"
+ "github.com/coder/coder/v2/provisionerd/proto"
+ "github.com/coder/coder/v2/provisionersdk"
+ sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/testutil"
)
func TestMain(m *testing.M) {
@@ -129,7 +128,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_TemplateImport_{
TemplateImport: &proto.AcquiredJob_TemplateImport{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -144,10 +143,15 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- parse: func(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error {
+ parse: func(_ *provisionersdk.Session, _ *sdkproto.ParseRequest, _ <-chan struct{}) *sdkproto.ParseComplete {
closerMutex.Lock()
defer closerMutex.Unlock()
- return closer.Close()
+ err := closer.Close()
+ c := &sdkproto.ParseComplete{}
+ if err != nil {
+ c.Error = err.Error()
+ }
+ return c
},
}),
})
@@ -180,7 +184,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_TemplateImport_{
TemplateImport: &proto.AcquiredJob_TemplateImport{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -220,7 +224,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_TemplateImport_{
TemplateImport: &proto.AcquiredJob_TemplateImport{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -235,9 +239,13 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- parse: func(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error {
- <-stream.Context().Done()
- return nil
+ parse: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ParseRequest,
+ cancelOrComplete <-chan struct{},
+ ) *sdkproto.ParseComplete {
+ <-cancelOrComplete
+ return &sdkproto.ParseComplete{}
},
}),
})
@@ -255,7 +263,6 @@ func TestProvisionerd(t *testing.T) {
didComplete atomic.Bool
didLog atomic.Bool
didAcquireJob atomic.Bool
- didDryRun = atomic.NewBool(true)
didReadme atomic.Bool
completeChan = make(chan struct{})
completeOnce sync.Once
@@ -273,12 +280,12 @@ func TestProvisionerd(t *testing.T) {
JobId: "test",
Provisioner: "someprovisioner",
TemplateSourceArchive: createTar(t, map[string]string{
- "test.txt": "content",
- runner.ReadmeFile: "# A cool template 😎\n",
+ "test.txt": "content",
+ provisionersdk.ReadmeFile: "# A cool template 😎\n",
}),
Type: &proto.AcquiredJob_TemplateImport_{
TemplateImport: &proto.AcquiredJob_TemplateImport{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -299,54 +306,34 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- parse: func(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error {
- data, err := os.ReadFile(filepath.Join(request.Directory, "test.txt"))
+ parse: func(
+ s *provisionersdk.Session,
+ _ *sdkproto.ParseRequest,
+ cancelOrComplete <-chan struct{},
+ ) *sdkproto.ParseComplete {
+ data, err := os.ReadFile(filepath.Join(s.WorkDirectory, "test.txt"))
require.NoError(t, err)
require.Equal(t, "content", string(data))
-
- err = stream.Send(&sdkproto.Parse_Response{
- Type: &sdkproto.Parse_Response_Log{
- Log: &sdkproto.Log{
- Level: sdkproto.LogLevel_INFO,
- Output: "hello",
- },
- },
- })
- require.NoError(t, err)
-
- err = stream.Send(&sdkproto.Parse_Response{
- Type: &sdkproto.Parse_Response_Complete{
- Complete: &sdkproto.Parse_Complete{},
- },
- })
- require.NoError(t, err)
- return nil
+ s.ProvisionLog(sdkproto.LogLevel_INFO, "hello")
+ return &sdkproto.ParseComplete{}
},
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- request, err := stream.Recv()
- require.NoError(t, err)
- if request.GetApply() != nil {
- didDryRun.Store(false)
+ plan: func(
+ s *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ cancelOrComplete <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ s.ProvisionLog(sdkproto.LogLevel_INFO, "hello")
+ return &sdkproto.PlanComplete{
+ Resources: []*sdkproto.Resource{},
}
- err = stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Log{
- Log: &sdkproto.Log{
- Level: sdkproto.LogLevel_INFO,
- Output: "hello",
- },
- },
- })
- require.NoError(t, err)
-
- err = stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{
- Resources: []*sdkproto.Resource{},
- },
- },
- })
- require.NoError(t, err)
- return nil
+ },
+ apply: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ t.Error("dry run should not apply")
+ return &sdkproto.ApplyComplete{}
},
}),
})
@@ -355,7 +342,6 @@ func TestProvisionerd(t *testing.T) {
require.NoError(t, closer.Close())
assert.True(t, didLog.Load(), "should log some updates")
assert.True(t, didComplete.Load(), "should complete the job")
- assert.True(t, didDryRun.Load(), "should be a dry run")
})
t.Run("TemplateDryRun", func(t *testing.T) {
@@ -371,7 +357,7 @@ func TestProvisionerd(t *testing.T) {
completeChan = make(chan struct{})
completeOnce sync.Once
- metadata = &sdkproto.Provision_Metadata{}
+ metadata = &sdkproto.Metadata{}
)
closer := createProvisionerd(t, func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
@@ -414,16 +400,22 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- err := stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{
- Resources: []*sdkproto.Resource{},
- },
- },
- })
- require.NoError(t, err)
- return nil
+ plan: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ _ <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ return &sdkproto.PlanComplete{
+ Resources: []*sdkproto.Resource{},
+ }
+ },
+ apply: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ t.Error("dry run should not apply")
+ return &sdkproto.ApplyComplete{}
},
}),
})
@@ -464,7 +456,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -482,24 +474,20 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- err := stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Log{
- Log: &sdkproto.Log{
- Level: sdkproto.LogLevel_DEBUG,
- Output: "wow",
- },
- },
- })
- require.NoError(t, err)
-
- err = stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{},
- },
- })
- require.NoError(t, err)
- return nil
+ plan: func(
+ s *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ cancelOrComplete <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow")
+ return &sdkproto.PlanComplete{}
+ },
+ apply: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ return &sdkproto.ApplyComplete{}
},
}),
})
@@ -540,7 +528,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -567,40 +555,46 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- err := stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Log{
- Log: &sdkproto.Log{
- Level: sdkproto.LogLevel_DEBUG,
- Output: "wow",
+ plan: func(
+ s *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ cancelOrComplete <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow")
+ return &sdkproto.PlanComplete{
+ Resources: []*sdkproto.Resource{
+ {
+ DailyCost: 10,
+ },
+ {
+ DailyCost: 15,
},
},
- })
- require.NoError(t, err)
-
- err = stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{
- Resources: []*sdkproto.Resource{
- {
- DailyCost: 10,
- },
- {
- DailyCost: 15,
- },
- },
+ }
+ },
+ apply: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ t.Error("should not apply when resources exceed quota")
+ return &sdkproto.ApplyComplete{
+ Resources: []*sdkproto.Resource{
+ {
+ DailyCost: 10,
+ },
+ {
+ DailyCost: 15,
},
},
- })
- require.NoError(t, err)
- return nil
+ }
},
}),
})
require.Condition(t, closedWithin(completeChan, testutil.WaitShort))
require.NoError(t, closer.Close())
assert.True(t, didLog.Load(), "should log some updates")
- assert.False(t, didComplete.Load(), "should complete the job")
+ assert.False(t, didComplete.Load(), "should not complete the job")
assert.True(t, didFail.Load(), "should fail the job")
})
@@ -633,7 +627,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -646,14 +640,24 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- return stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{
- Error: "some error",
- },
- },
- })
+ plan: func(
+ s *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ cancelOrComplete <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ return &sdkproto.PlanComplete{
+ Error: "some error",
+ }
+ },
+ apply: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ t.Error("should not apply when plan errors")
+ return &sdkproto.ApplyComplete{
+ Error: "some error",
+ }
},
}),
})
@@ -683,7 +687,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -712,31 +716,24 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- // Ignore the first provision message!
- _, _ = stream.Recv()
-
- err := stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Log{
- Log: &sdkproto.Log{
- Level: sdkproto.LogLevel_DEBUG,
- Output: "in progress",
- },
- },
- })
- require.NoError(t, err)
-
- msg, err := stream.Recv()
- require.NoError(t, err)
- require.NotNil(t, msg.GetCancel())
-
- return stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{
- Error: "some error",
- },
- },
- })
+ plan: func(
+ s *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ canceledOrComplete <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ s.ProvisionLog(sdkproto.LogLevel_DEBUG, "in progress")
+ <-canceledOrComplete
+ return &sdkproto.PlanComplete{
+ Error: "some error",
+ }
+ },
+ apply: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ t.Error("should not apply when shut down during plan")
+ return &sdkproto.ApplyComplete{}
},
}),
})
@@ -768,7 +765,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -805,31 +802,24 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- // Ignore the first provision message!
- _, _ = stream.Recv()
-
- err := stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Log{
- Log: &sdkproto.Log{
- Level: sdkproto.LogLevel_DEBUG,
- Output: "in progress",
- },
- },
- })
- require.NoError(t, err)
-
- msg, err := stream.Recv()
- require.NoError(t, err)
- require.NotNil(t, msg.GetCancel())
-
- return stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{
- Error: "some error",
- },
- },
- })
+ plan: func(
+ s *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ canceledOrComplete <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ s.ProvisionLog(sdkproto.LogLevel_DEBUG, "in progress")
+ <-canceledOrComplete
+ return &sdkproto.PlanComplete{
+ Error: "some error",
+ }
+ },
+ apply: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ t.Error("should not apply when shut down during plan")
+ return &sdkproto.ApplyComplete{}
},
}),
})
@@ -867,7 +857,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -898,16 +888,22 @@ func TestProvisionerd(t *testing.T) {
return client, nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- // Ignore the first provision message!
- _, _ = stream.Recv()
- return stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{
- Error: "some error",
- },
- },
- })
+ plan: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ _ <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ return &sdkproto.PlanComplete{
+ Error: "some error",
+ }
+ },
+ apply: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ t.Error("should not apply when error during plan")
+ return &sdkproto.ApplyComplete{}
},
}),
})
@@ -945,7 +941,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -977,14 +973,19 @@ func TestProvisionerd(t *testing.T) {
return client, nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- // Ignore the first provision message!
- _, _ = stream.Recv()
- return stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{},
- },
- })
+ plan: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ _ <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ return &sdkproto.PlanComplete{}
+ },
+ apply: func(
+ _ *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ return &sdkproto.ApplyComplete{}
},
}),
})
@@ -1023,7 +1024,7 @@ func TestProvisionerd(t *testing.T) {
}),
Type: &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
- Metadata: &sdkproto.Provision_Metadata{},
+ Metadata: &sdkproto.Metadata{},
},
},
}, nil
@@ -1056,24 +1057,21 @@ func TestProvisionerd(t *testing.T) {
}), nil
}, provisionerd.Provisioners{
"someprovisioner": createProvisionerClient(t, done, provisionerTestServer{
- provision: func(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- err := stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Log{
- Log: &sdkproto.Log{
- Level: sdkproto.LogLevel_DEBUG,
- Output: "wow",
- },
- },
- })
- require.NoError(t, err)
-
- err = stream.Send(&sdkproto.Provision_Response{
- Type: &sdkproto.Provision_Response_Complete{
- Complete: &sdkproto.Provision_Complete{},
- },
- })
- require.NoError(t, err)
- return nil
+ plan: func(
+ s *provisionersdk.Session,
+ _ *sdkproto.PlanRequest,
+ _ <-chan struct{},
+ ) *sdkproto.PlanComplete {
+ s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow")
+ return &sdkproto.PlanComplete{}
+ },
+ apply: func(
+ s *provisionersdk.Session,
+ _ *sdkproto.ApplyRequest,
+ _ <-chan struct{},
+ ) *sdkproto.ApplyComplete {
+ s.ProvisionLog(sdkproto.LogLevel_DEBUG, "wow")
+ return &sdkproto.ApplyComplete{}
},
}),
})
@@ -1111,7 +1109,6 @@ func createProvisionerd(t *testing.T, dialer provisionerd.Dialer, provisioners p
JobPollInterval: 50 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
Provisioners: provisioners,
- WorkDirectory: t.TempDir(),
})
t.Cleanup(func() {
_ = server.Close()
@@ -1172,15 +1169,15 @@ func createProvisionerClient(t *testing.T, done <-chan struct{}, server provisio
_ = clientPipe.Close()
_ = serverPipe.Close()
})
- mux := drpcmux.New()
- err := sdkproto.DRPCRegisterProvisioner(mux, &server)
- require.NoError(t, err)
- srv := drpcserver.New(mux)
ctx, cancelFunc := context.WithCancel(context.Background())
closed := make(chan struct{})
go func() {
defer close(closed)
- _ = srv.Serve(ctx, serverPipe)
+ _ = provisionersdk.Serve(ctx, &server, &provisionersdk.ServeOptions{
+ Listener: serverPipe,
+ Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug).Named("test-provisioner"),
+ WorkDirectory: t.TempDir(),
+ })
}()
t.Cleanup(func() {
cancelFunc()
@@ -1200,16 +1197,21 @@ func createProvisionerClient(t *testing.T, done <-chan struct{}, server provisio
}
type provisionerTestServer struct {
- parse func(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error
- provision func(stream sdkproto.DRPCProvisioner_ProvisionStream) error
+ parse func(s *provisionersdk.Session, r *sdkproto.ParseRequest, canceledOrComplete <-chan struct{}) *sdkproto.ParseComplete
+ plan func(s *provisionersdk.Session, r *sdkproto.PlanRequest, canceledOrComplete <-chan struct{}) *sdkproto.PlanComplete
+ apply func(s *provisionersdk.Session, r *sdkproto.ApplyRequest, canceledOrComplete <-chan struct{}) *sdkproto.ApplyComplete
+}
+
+func (p *provisionerTestServer) Parse(s *provisionersdk.Session, r *sdkproto.ParseRequest, canceledOrComplete <-chan struct{}) *sdkproto.ParseComplete {
+ return p.parse(s, r, canceledOrComplete)
}
-func (p *provisionerTestServer) Parse(request *sdkproto.Parse_Request, stream sdkproto.DRPCProvisioner_ParseStream) error {
- return p.parse(request, stream)
+func (p *provisionerTestServer) Plan(s *provisionersdk.Session, r *sdkproto.PlanRequest, canceledOrComplete <-chan struct{}) *sdkproto.PlanComplete {
+ return p.plan(s, r, canceledOrComplete)
}
-func (p *provisionerTestServer) Provision(stream sdkproto.DRPCProvisioner_ProvisionStream) error {
- return p.provision(stream)
+func (p *provisionerTestServer) Apply(s *provisionersdk.Session, r *sdkproto.ApplyRequest, canceledOrComplete <-chan struct{}) *sdkproto.ApplyComplete {
+ return p.apply(s, r, canceledOrComplete)
}
// Fulfills the protobuf interface for a ProvisionerDaemon with
diff --git a/provisionerd/runner/quota.go b/provisionerd/runner/quota.go
index d773721713e6c..26c7e0478ec2c 100644
--- a/provisionerd/runner/quota.go
+++ b/provisionerd/runner/quota.go
@@ -1,6 +1,6 @@
package runner
-import "github.com/coder/coder/provisionersdk/proto"
+import "github.com/coder/coder/v2/provisionersdk/proto"
func sumDailyCost(resources []*proto.Resource) int {
var sum int
diff --git a/provisionerd/runner/runner.go b/provisionerd/runner/runner.go
index e074800b03d36..7afa7a0999627 100644
--- a/provisionerd/runner/runner.go
+++ b/provisionerd/runner/runner.go
@@ -1,15 +1,9 @@
package runner
import (
- "archive/tar"
- "bytes"
"context"
"errors"
"fmt"
- "io"
- "os"
- "path"
- "path/filepath"
"reflect"
"strings"
"sync"
@@ -18,7 +12,6 @@ import (
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
- "github.com/spf13/afero"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.14.0"
@@ -26,10 +19,10 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/coderd/util/ptr"
- "github.com/coder/coder/provisionerd/proto"
- sdkproto "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/provisionerd/proto"
+ sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
)
const (
@@ -54,14 +47,14 @@ type Runner struct {
sender JobUpdater
quotaCommitter QuotaCommitter
logger slog.Logger
- filesystem afero.Fs
- workDirectory string
provisioner sdkproto.DRPCProvisionerClient
lastUpdate atomic.Pointer[time.Time]
updateInterval time.Duration
forceCancelInterval time.Duration
logBufferInterval time.Duration
+ // session is the provisioning session with the (possibly remote) provisioner
+ session sdkproto.DRPCProvisioner_SessionClient
// closed when the Runner is finished sending any updates/failed/complete.
done chan struct{}
// active as long as we are not canceled
@@ -108,8 +101,6 @@ type Options struct {
Updater JobUpdater
QuotaCommitter QuotaCommitter
Logger slog.Logger
- Filesystem afero.Fs
- WorkDirectory string
Provisioner sdkproto.DRPCProvisionerClient
UpdateInterval time.Duration
ForceCancelInterval time.Duration
@@ -149,8 +140,6 @@ func New(
sender: opts.Updater,
quotaCommitter: opts.QuotaCommitter,
logger: logger,
- filesystem: opts.Filesystem,
- workDirectory: opts.WorkDirectory,
provisioner: opts.Provisioner,
updateInterval: opts.UpdateInterval,
forceCancelInterval: opts.ForceCancelInterval,
@@ -386,6 +375,14 @@ func (r *Runner) doCleanFinish(ctx context.Context) {
r.setComplete(completedJob)
}()
+ var err error
+ r.session, err = r.provisioner.Session(ctx)
+ if err != nil {
+ failedJob = r.failedJobf("open session: %s", err)
+ return
+ }
+ defer r.session.Close()
+
defer func() {
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
@@ -396,23 +393,6 @@ func (r *Runner) doCleanFinish(ctx context.Context) {
Stage: "Cleaning Up",
CreatedAt: time.Now().UnixMilli(),
})
-
- // Cleanup the work directory after execution.
- for attempt := 0; attempt < 5; attempt++ {
- err := r.filesystem.RemoveAll(r.workDirectory)
- if err != nil {
- // On Windows, open files cannot be removed.
- // When the provisioner daemon is shutting down,
- // it may take a few milliseconds for processes to exit.
- // See: https://github.com/golang/go/issues/50510
- r.logger.Debug(ctx, "failed to clean work directory; trying again", slog.Error(err))
- time.Sleep(250 * time.Millisecond)
- continue
- }
- r.logger.Debug(ctx, "cleaned up work directory")
- break
- }
-
r.flushQueuedLogs(ctx)
}()
@@ -424,85 +404,19 @@ func (r *Runner) do(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob)
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
- err := r.filesystem.MkdirAll(r.workDirectory, 0o700)
- if err != nil {
- return nil, r.failedJobf("create work directory %q: %s", r.workDirectory, err)
- }
-
r.queueLog(ctx, &proto.Log{
Source: proto.LogSource_PROVISIONER_DAEMON,
Level: sdkproto.LogLevel_INFO,
Stage: "Setting up",
CreatedAt: time.Now().UnixMilli(),
})
- if err != nil {
- return nil, r.failedJobf("write log: %s", err)
- }
- r.logger.Info(ctx, "unpacking template source archive",
- slog.F("size_bytes", len(r.job.TemplateSourceArchive)),
- )
-
- reader := tar.NewReader(bytes.NewBuffer(r.job.TemplateSourceArchive))
- for {
- header, err := reader.Next()
- if err != nil {
- if errors.Is(err, io.EOF) {
- break
- }
- return nil, r.failedJobf("read template source archive: %s", err)
- }
- // #nosec
- headerPath := filepath.Join(r.workDirectory, header.Name)
- if !strings.HasPrefix(headerPath, filepath.Clean(r.workDirectory)) {
- return nil, r.failedJobf("tar attempts to target relative upper directory")
- }
- mode := header.FileInfo().Mode()
- if mode == 0 {
- mode = 0o600
- }
- switch header.Typeflag {
- case tar.TypeDir:
- err = r.filesystem.MkdirAll(headerPath, mode)
- if err != nil {
- return nil, r.failedJobf("mkdir %q: %s", headerPath, err)
- }
- r.logger.Debug(context.Background(), "extracted directory", slog.F("path", headerPath))
- case tar.TypeReg:
- file, err := r.filesystem.OpenFile(headerPath, os.O_CREATE|os.O_RDWR, mode)
- if err != nil {
- return nil, r.failedJobf("create file %q (mode %s): %s", headerPath, mode, err)
- }
- // Max file size of 10MiB.
- size, err := io.CopyN(file, reader, 10<<20)
- if errors.Is(err, io.EOF) {
- err = nil
- }
- if err != nil {
- _ = file.Close()
- return nil, r.failedJobf("copy file %q: %s", headerPath, err)
- }
- err = file.Close()
- if err != nil {
- return nil, r.failedJobf("close file %q: %s", headerPath, err)
- }
- r.logger.Debug(context.Background(), "extracted file",
- slog.F("size_bytes", size),
- slog.F("path", headerPath),
- slog.F("mode", mode),
- )
- }
- }
switch jobType := r.job.Type.(type) {
case *proto.AcquiredJob_TemplateImport_:
r.logger.Debug(context.Background(), "acquired job is template import",
slog.F("user_variable_values", redactVariableValues(jobType.TemplateImport.UserVariableValues)),
)
- failedJob := r.runReadmeParse(ctx)
- if failedJob != nil {
- return nil, failedJob
- }
return r.runTemplateImport(ctx)
case *proto.AcquiredJob_TemplateDryRun_:
r.logger.Debug(context.Background(), "acquired job is template dry-run",
@@ -525,6 +439,14 @@ func (r *Runner) do(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob)
}
}
+func (r *Runner) configure(config *sdkproto.Config) *proto.FailedJob {
+ err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Config{Config: config}})
+ if err != nil {
+ return r.failedJobf("send config: %s", err)
+ }
+ return nil
+}
+
// heartbeatRoutine periodically sends updates on the job, which keeps coder server
// from assuming the job is stalled, and allows the runner to learn if the job
// has been canceled by the user.
@@ -577,44 +499,16 @@ func (r *Runner) heartbeatRoutine(ctx context.Context) {
}
}
-// ReadmeFile is the location we look for to extract documentation from template
-// versions.
-const ReadmeFile = "README.md"
-
-func (r *Runner) runReadmeParse(ctx context.Context) *proto.FailedJob {
+func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob) {
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
- fi, err := afero.ReadFile(r.filesystem, path.Join(r.workDirectory, ReadmeFile))
- if err != nil {
- r.queueLog(ctx, &proto.Log{
- Source: proto.LogSource_PROVISIONER_DAEMON,
- Level: sdkproto.LogLevel_DEBUG,
- Stage: "No README.md provided",
- CreatedAt: time.Now().UnixMilli(),
- })
- return nil
- }
-
- _, err = r.update(ctx, &proto.UpdateJobRequest{
- JobId: r.job.JobId,
- Logs: []*proto.Log{{
- Source: proto.LogSource_PROVISIONER_DAEMON,
- Level: sdkproto.LogLevel_INFO,
- Stage: "Adding README.md...",
- CreatedAt: time.Now().UnixMilli(),
- }},
- Readme: fi,
+ failedJob := r.configure(&sdkproto.Config{
+ TemplateSourceArchive: r.job.GetTemplateSourceArchive(),
})
- if err != nil {
- return r.failedJobf("write log: %s", err)
+ if failedJob != nil {
+ return nil, failedJob
}
- return nil
-}
-
-func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *proto.FailedJob) {
- ctx, span := r.startTrace(ctx, tracing.FuncName())
- defer span.End()
// Parse parameters and update the job with the parameter specs
r.queueLog(ctx, &proto.Log{
@@ -623,7 +517,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
Stage: "Parsing template parameters",
CreatedAt: time.Now().UnixMilli(),
})
- templateVariables, err := r.runTemplateImportParse(ctx)
+ templateVariables, readme, err := r.runTemplateImportParse(ctx)
if err != nil {
return nil, r.failedJobf("run parse: %s", err)
}
@@ -634,6 +528,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
JobId: r.job.JobId,
TemplateVariables: templateVariables,
UserVariableValues: r.job.GetTemplateImport().GetUserVariableValues(),
+ Readme: readme,
})
if err != nil {
return nil, r.failedJobf("update job: %s", err)
@@ -646,7 +541,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
Stage: "Detecting persistent resources",
CreatedAt: time.Now().UnixMilli(),
})
- startProvision, err := r.runTemplateImportProvision(ctx, updateResponse.VariableValues, &sdkproto.Provision_Metadata{
+ startProvision, err := r.runTemplateImportProvision(ctx, updateResponse.VariableValues, &sdkproto.Metadata{
CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl,
WorkspaceTransition: sdkproto.WorkspaceTransition_START,
})
@@ -661,7 +556,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
Stage: "Detecting ephemeral resources",
CreatedAt: time.Now().UnixMilli(),
})
- stopProvision, err := r.runTemplateImportProvision(ctx, updateResponse.VariableValues, &sdkproto.Provision_Metadata{
+ stopProvision, err := r.runTemplateImportProvision(ctx, updateResponse.VariableValues, &sdkproto.Metadata{
CoderUrl: r.job.GetTemplateImport().Metadata.CoderUrl,
WorkspaceTransition: sdkproto.WorkspaceTransition_STOP,
})
@@ -682,25 +577,24 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
}, nil
}
-// Parses template variables and parameter schemas from source.
-func (r *Runner) runTemplateImportParse(ctx context.Context) ([]*sdkproto.TemplateVariable, error) {
+// Parses template variables and README from source.
+func (r *Runner) runTemplateImportParse(ctx context.Context) (
+ vars []*sdkproto.TemplateVariable, readme []byte, err error,
+) {
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
- stream, err := r.provisioner.Parse(ctx, &sdkproto.Parse_Request{
- Directory: r.workDirectory,
- })
+ err = r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Parse{Parse: &sdkproto.ParseRequest{}}})
if err != nil {
- return nil, xerrors.Errorf("parse source: %w", err)
+ return nil, nil, xerrors.Errorf("parse source: %w", err)
}
- defer stream.Close()
for {
- msg, err := stream.Recv()
+ msg, err := r.session.Recv()
if err != nil {
- return nil, xerrors.Errorf("recv parse source: %w", err)
+ return nil, nil, xerrors.Errorf("recv parse source: %w", err)
}
switch msgType := msg.Type.(type) {
- case *sdkproto.Parse_Response_Log:
+ case *sdkproto.Response_Log:
r.logger.Debug(context.Background(), "parse job logged",
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
@@ -713,14 +607,20 @@ func (r *Runner) runTemplateImportParse(ctx context.Context) ([]*sdkproto.Templa
Output: msgType.Log.Output,
Stage: "Parse parameters",
})
- case *sdkproto.Parse_Response_Complete:
+ case *sdkproto.Response_Parse:
+ pc := msgType.Parse
r.logger.Debug(context.Background(), "parse complete",
- slog.F("template_variables", msgType.Complete.TemplateVariables),
+ slog.F("template_variables", pc.TemplateVariables),
+ slog.F("readme_len", len(pc.Readme)),
+ slog.F("error", pc.Error),
)
+ if pc.Error != "" {
+ return nil, nil, xerrors.Errorf("parse error: %s", pc.Error)
+ }
- return msgType.Complete.TemplateVariables, nil
+ return msgType.Parse.TemplateVariables, msgType.Parse.Readme, nil
default:
- return nil, xerrors.Errorf("invalid message type %q received from provisioner",
+ return nil, nil, xerrors.Errorf("invalid message type %q received from provisioner",
reflect.TypeOf(msg.Type).String())
}
}
@@ -735,13 +635,18 @@ type templateImportProvision struct {
// Performs a dry-run provision when importing a template.
// This is used to detect resources that would be provisioned for a workspace in various states.
// It doesn't define values for rich parameters as they're unknown during template import.
-func (r *Runner) runTemplateImportProvision(ctx context.Context, variableValues []*sdkproto.VariableValue, metadata *sdkproto.Provision_Metadata) (*templateImportProvision, error) {
+func (r *Runner) runTemplateImportProvision(ctx context.Context, variableValues []*sdkproto.VariableValue, metadata *sdkproto.Metadata) (*templateImportProvision, error) {
return r.runTemplateImportProvisionWithRichParameters(ctx, variableValues, nil, metadata)
}
// Performs a dry-run provision with provided rich parameters.
// This is used to detect resources that would be provisioned for a workspace in various states.
-func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Context, variableValues []*sdkproto.VariableValue, richParameterValues []*sdkproto.RichParameterValue, metadata *sdkproto.Provision_Metadata) (*templateImportProvision, error) {
+func (r *Runner) runTemplateImportProvisionWithRichParameters(
+ ctx context.Context,
+ variableValues []*sdkproto.VariableValue,
+ richParameterValues []*sdkproto.RichParameterValue,
+ metadata *sdkproto.Metadata,
+) (*templateImportProvision, error) {
ctx, span := r.startTrace(ctx, tracing.FuncName())
defer span.End()
@@ -754,46 +659,38 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Contex
}
// use the notStopped so that if we attempt to gracefully cancel, the stream will still be available for us
// to send the cancel to the provisioner
- stream, err := r.provisioner.Provision(ctx)
+ err := r.session.Send(&sdkproto.Request{Type: &sdkproto.Request_Plan{Plan: &sdkproto.PlanRequest{
+ Metadata: metadata,
+ RichParameterValues: richParameterValues,
+ VariableValues: variableValues,
+ }}})
if err != nil {
- return nil, xerrors.Errorf("provision: %w", err)
+ return nil, xerrors.Errorf("start provision: %w", err)
}
- defer stream.Close()
+ nevermind := make(chan struct{})
+ defer close(nevermind)
go func() {
select {
+ case <-nevermind:
+ return
case <-r.notStopped.Done():
return
case <-r.notCanceled.Done():
- _ = stream.Send(&sdkproto.Provision_Request{
- Type: &sdkproto.Provision_Request_Cancel{
- Cancel: &sdkproto.Provision_Cancel{},
+ _ = r.session.Send(&sdkproto.Request{
+ Type: &sdkproto.Request_Cancel{
+ Cancel: &sdkproto.CancelRequest{},
},
})
}
}()
- err = stream.Send(&sdkproto.Provision_Request{
- Type: &sdkproto.Provision_Request_Plan{
- Plan: &sdkproto.Provision_Plan{
- Config: &sdkproto.Provision_Config{
- Directory: r.workDirectory,
- Metadata: metadata,
- },
- RichParameterValues: richParameterValues,
- VariableValues: variableValues,
- },
- },
- })
- if err != nil {
- return nil, xerrors.Errorf("start provision: %w", err)
- }
for {
- msg, err := stream.Recv()
+ msg, err := r.session.Recv()
if err != nil {
return nil, xerrors.Errorf("recv import provision: %w", err)
}
switch msgType := msg.Type.(type) {
- case *sdkproto.Provision_Response_Log:
+ case *sdkproto.Response_Log:
r.logger.Debug(context.Background(), "template import provision job logged",
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
@@ -805,25 +702,25 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(ctx context.Contex
Output: msgType.Log.Output,
Stage: stage,
})
- case *sdkproto.Provision_Response_Complete:
- if msgType.Complete.Error != "" {
+ case *sdkproto.Response_Plan:
+ c := msgType.Plan
+ if c.Error != "" {
r.logger.Info(context.Background(), "dry-run provision failure",
- slog.F("error", msgType.Complete.Error),
+ slog.F("error", c.Error),
)
- return nil, xerrors.New(msgType.Complete.Error)
+ return nil, xerrors.New(c.Error)
}
r.logger.Info(context.Background(), "parse dry-run provision successful",
- slog.F("resource_count", len(msgType.Complete.Resources)),
- slog.F("resources", msgType.Complete.Resources),
- slog.F("state_length", len(msgType.Complete.State)),
+ slog.F("resource_count", len(c.Resources)),
+ slog.F("resources", c.Resources),
)
return &templateImportProvision{
- Resources: msgType.Complete.Resources,
- Parameters: msgType.Complete.Parameters,
- GitAuthProviders: msgType.Complete.GitAuthProviders,
+ Resources: c.Resources,
+ Parameters: c.Parameters,
+ GitAuthProviders: c.GitAuthProviders,
}, nil
default:
return nil, xerrors.Errorf("invalid message type %q received from provisioner",
@@ -864,6 +761,13 @@ func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *p
metadata.WorkspaceOwnerId = id.String()
}
+ failedJob := r.configure(&sdkproto.Config{
+ TemplateSourceArchive: r.job.GetTemplateSourceArchive(),
+ })
+ if failedJob != nil {
+ return nil, failedJob
+ }
+
// Run the template import provision task since it's already a dry run.
provision, err := r.runTemplateImportProvisionWithRichParameters(ctx,
r.job.GetTemplateDryRun().GetVariableValues(),
@@ -884,41 +788,39 @@ func (r *Runner) runTemplateDryRun(ctx context.Context) (*proto.CompletedJob, *p
}, nil
}
-func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto.Provision_Request) (
- *sdkproto.Provision_Complete, *proto.FailedJob,
+func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto.Request) (
+ *sdkproto.Response, *proto.FailedJob,
) {
// use the notStopped so that if we attempt to gracefully cancel, the stream
// will still be available for us to send the cancel to the provisioner
- stream, err := r.provisioner.Provision(ctx)
+ err := r.session.Send(req)
if err != nil {
- return nil, r.failedWorkspaceBuildf("provision: %s", err)
+ return nil, r.failedWorkspaceBuildf("start provision: %s", err)
}
- defer stream.Close()
+ nevermind := make(chan struct{})
+ defer close(nevermind)
go func() {
select {
+ case <-nevermind:
+ return
case <-r.notStopped.Done():
return
case <-r.notCanceled.Done():
- _ = stream.Send(&sdkproto.Provision_Request{
- Type: &sdkproto.Provision_Request_Cancel{
- Cancel: &sdkproto.Provision_Cancel{},
+ _ = r.session.Send(&sdkproto.Request{
+ Type: &sdkproto.Request_Cancel{
+ Cancel: &sdkproto.CancelRequest{},
},
})
}
}()
- err = stream.Send(req)
- if err != nil {
- return nil, r.failedWorkspaceBuildf("start provision: %s", err)
- }
-
for {
- msg, err := stream.Recv()
+ msg, err := r.session.Recv()
if err != nil {
return nil, r.failedWorkspaceBuildf("recv workspace provision: %s", err)
}
switch msgType := msg.Type.(type) {
- case *sdkproto.Provision_Response_Log:
+ case *sdkproto.Response_Log:
r.logProvisionerJobLog(context.Background(), msgType.Log.Level, "workspace provisioner job logged",
slog.F("level", msgType.Log.Level),
slog.F("output", msgType.Log.Output),
@@ -932,39 +834,19 @@ func (r *Runner) buildWorkspace(ctx context.Context, stage string, req *sdkproto
Output: msgType.Log.Output,
Stage: stage,
})
- case *sdkproto.Provision_Response_Complete:
- if msgType.Complete.Error != "" {
- r.logger.Warn(context.Background(), "provision failed; updating state",
- slog.F("state_length", len(msgType.Complete.State)),
- slog.F("error", msgType.Complete.Error),
- )
-
- return nil, &proto.FailedJob{
- JobId: r.job.JobId,
- Error: msgType.Complete.Error,
- Type: &proto.FailedJob_WorkspaceBuild_{
- WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
- State: msgType.Complete.State,
- },
- },
- }
- }
-
- r.logger.Info(context.Background(), "provision successful",
- slog.F("resource_count", len(msgType.Complete.Resources)),
- slog.F("resources", msgType.Complete.Resources),
- slog.F("state_length", len(msgType.Complete.State)),
- )
- // Stop looping!
- return msgType.Complete, nil
default:
- return nil, r.failedWorkspaceBuildf("invalid message type %T received from provisioner", msg.Type)
+ // Stop looping!
+ return msg, nil
}
}
}
func (r *Runner) commitQuota(ctx context.Context, resources []*sdkproto.Resource) *proto.FailedJob {
cost := sumDailyCost(resources)
+ r.logger.Debug(ctx, "committing quota",
+ slog.F("resources", resources),
+ slog.F("cost", cost),
+ )
if cost == 0 {
return nil
}
@@ -1031,18 +913,19 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
applyStage = "Destroying workspace"
}
- config := &sdkproto.Provision_Config{
- Directory: r.workDirectory,
- Metadata: r.job.GetWorkspaceBuild().Metadata,
- State: r.job.GetWorkspaceBuild().State,
-
- ProvisionerLogLevel: r.job.GetWorkspaceBuild().LogLevel,
+ failedJob := r.configure(&sdkproto.Config{
+ TemplateSourceArchive: r.job.GetTemplateSourceArchive(),
+ State: r.job.GetWorkspaceBuild().State,
+ ProvisionerLogLevel: r.job.GetWorkspaceBuild().LogLevel,
+ })
+ if failedJob != nil {
+ return nil, failedJob
}
- completedPlan, failed := r.buildWorkspace(ctx, "Planning infrastructure", &sdkproto.Provision_Request{
- Type: &sdkproto.Provision_Request_Plan{
- Plan: &sdkproto.Provision_Plan{
- Config: config,
+ resp, failed := r.buildWorkspace(ctx, "Planning infrastructure", &sdkproto.Request{
+ Type: &sdkproto.Request_Plan{
+ Plan: &sdkproto.PlanRequest{
+ Metadata: r.job.GetWorkspaceBuild().Metadata,
RichParameterValues: r.job.GetWorkspaceBuild().RichParameterValues,
VariableValues: r.job.GetWorkspaceBuild().VariableValues,
GitAuthProviders: r.job.GetWorkspaceBuild().GitAuthProviders,
@@ -1052,9 +935,31 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
if failed != nil {
return nil, failed
}
+ planComplete := resp.GetPlan()
+ if planComplete == nil {
+ return nil, r.failedWorkspaceBuildf("invalid message type %T received from provisioner", resp.Type)
+ }
+ if planComplete.Error != "" {
+ r.logger.Warn(context.Background(), "plan request failed",
+ slog.F("error", planComplete.Error),
+ )
+
+ return nil, &proto.FailedJob{
+ JobId: r.job.JobId,
+ Error: planComplete.Error,
+ Type: &proto.FailedJob_WorkspaceBuild_{
+ WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{},
+ },
+ }
+ }
+
+ r.logger.Info(context.Background(), "plan request successful",
+ slog.F("resource_count", len(planComplete.Resources)),
+ slog.F("resources", planComplete.Resources),
+ )
r.flushQueuedLogs(ctx)
if commitQuota {
- failed = r.commitQuota(ctx, completedPlan.GetResources())
+ failed = r.commitQuota(ctx, planComplete.Resources)
r.flushQueuedLogs(ctx)
if failed != nil {
return nil, failed
@@ -1068,25 +973,50 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
CreatedAt: time.Now().UnixMilli(),
})
- completedApply, failed := r.buildWorkspace(ctx, applyStage, &sdkproto.Provision_Request{
- Type: &sdkproto.Provision_Request_Apply{
- Apply: &sdkproto.Provision_Apply{
- Config: config,
- Plan: completedPlan.GetPlan(),
+ resp, failed = r.buildWorkspace(ctx, applyStage, &sdkproto.Request{
+ Type: &sdkproto.Request_Apply{
+ Apply: &sdkproto.ApplyRequest{
+ Metadata: r.job.GetWorkspaceBuild().Metadata,
},
},
})
if failed != nil {
return nil, failed
}
+ applyComplete := resp.GetApply()
+ if applyComplete == nil {
+ return nil, r.failedWorkspaceBuildf("invalid message type %T received from provisioner", resp.Type)
+ }
+ if applyComplete.Error != "" {
+ r.logger.Warn(context.Background(), "apply failed; updating state",
+ slog.F("error", applyComplete.Error),
+ slog.F("state_len", len(applyComplete.State)),
+ )
+
+ return nil, &proto.FailedJob{
+ JobId: r.job.JobId,
+ Error: applyComplete.Error,
+ Type: &proto.FailedJob_WorkspaceBuild_{
+ WorkspaceBuild: &proto.FailedJob_WorkspaceBuild{
+ State: applyComplete.State,
+ },
+ },
+ }
+ }
+
+ r.logger.Info(context.Background(), "apply successful",
+ slog.F("resource_count", len(applyComplete.Resources)),
+ slog.F("resources", applyComplete.Resources),
+ slog.F("state_len", len(applyComplete.State)),
+ )
r.flushQueuedLogs(ctx)
return &proto.CompletedJob{
JobId: r.job.JobId,
Type: &proto.CompletedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{
- State: completedApply.GetState(),
- Resources: completedApply.GetResources(),
+ State: applyComplete.State,
+ Resources: applyComplete.Resources,
},
},
}, nil
diff --git a/provisionersdk/agent_test.go b/provisionersdk/agent_test.go
index 305813719f6c9..c10127b03d5d1 100644
--- a/provisionersdk/agent_test.go
+++ b/provisionersdk/agent_test.go
@@ -20,7 +20,7 @@ import (
"github.com/go-chi/render"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/provisionersdk"
+ "github.com/coder/coder/v2/provisionersdk"
)
func TestAgentScript(t *testing.T) {
diff --git a/provisionersdk/archive.go b/provisionersdk/archive.go
index f54a0e76022af..df6eabc3b0c05 100644
--- a/provisionersdk/archive.go
+++ b/provisionersdk/archive.go
@@ -9,7 +9,7 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/util/xio"
+ "github.com/coder/coder/v2/coderd/util/xio"
)
const (
diff --git a/provisionersdk/archive_test.go b/provisionersdk/archive_test.go
index 034d886bc7116..abda7f6bb6d4a 100644
--- a/provisionersdk/archive_test.go
+++ b/provisionersdk/archive_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/provisionersdk"
+ "github.com/coder/coder/v2/provisionersdk"
)
func TestTar(t *testing.T) {
diff --git a/provisionersdk/errors.go b/provisionersdk/errors.go
new file mode 100644
index 0000000000000..0dc66e6e6b301
--- /dev/null
+++ b/provisionersdk/errors.go
@@ -0,0 +1,19 @@
+package provisionersdk
+
+import (
+ "fmt"
+
+ "github.com/coder/coder/v2/provisionersdk/proto"
+)
+
+func ParseErrorf(format string, args ...any) *proto.ParseComplete {
+ return &proto.ParseComplete{Error: fmt.Sprintf(format, args...)}
+}
+
+func PlanErrorf(format string, args ...any) *proto.PlanComplete {
+ return &proto.PlanComplete{Error: fmt.Sprintf(format, args...)}
+}
+
+func ApplyErrorf(format string, args ...any) *proto.ApplyComplete {
+ return &proto.ApplyComplete{Error: fmt.Sprintf(format, args...)}
+}
diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go
index 24281d4c252db..c0ea0be327953 100644
--- a/provisionersdk/proto/provisioner.pb.go
+++ b/provisionersdk/proto/provisioner.pb.go
@@ -125,6 +125,7 @@ func (AppSharingLevel) EnumDescriptor() ([]byte, []int) {
return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{1}
}
+// WorkspaceTransition is the desired outcome of a build
type WorkspaceTransition int32
const (
@@ -1313,15 +1314,27 @@ func (x *Resource) GetDailyCost() int32 {
return 0
}
-// Parse consumes source-code from a directory to produce inputs.
-type Parse struct {
+// Metadata is information about a workspace used in the execution of a build
+type Metadata struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
+
+ CoderUrl string `protobuf:"bytes,1,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"`
+ WorkspaceTransition WorkspaceTransition `protobuf:"varint,2,opt,name=workspace_transition,json=workspaceTransition,proto3,enum=provisioner.WorkspaceTransition" json:"workspace_transition,omitempty"`
+ WorkspaceName string `protobuf:"bytes,3,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"`
+ WorkspaceOwner string `protobuf:"bytes,4,opt,name=workspace_owner,json=workspaceOwner,proto3" json:"workspace_owner,omitempty"`
+ WorkspaceId string `protobuf:"bytes,5,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"`
+ WorkspaceOwnerId string `protobuf:"bytes,6,opt,name=workspace_owner_id,json=workspaceOwnerId,proto3" json:"workspace_owner_id,omitempty"`
+ WorkspaceOwnerEmail string `protobuf:"bytes,7,opt,name=workspace_owner_email,json=workspaceOwnerEmail,proto3" json:"workspace_owner_email,omitempty"`
+ TemplateName string `protobuf:"bytes,8,opt,name=template_name,json=templateName,proto3" json:"template_name,omitempty"`
+ TemplateVersion string `protobuf:"bytes,9,opt,name=template_version,json=templateVersion,proto3" json:"template_version,omitempty"`
+ WorkspaceOwnerOidcAccessToken string `protobuf:"bytes,10,opt,name=workspace_owner_oidc_access_token,json=workspaceOwnerOidcAccessToken,proto3" json:"workspace_owner_oidc_access_token,omitempty"`
+ WorkspaceOwnerSessionToken string `protobuf:"bytes,11,opt,name=workspace_owner_session_token,json=workspaceOwnerSessionToken,proto3" json:"workspace_owner_session_token,omitempty"`
}
-func (x *Parse) Reset() {
- *x = Parse{}
+func (x *Metadata) Reset() {
+ *x = Metadata{}
if protoimpl.UnsafeEnabled {
mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -1329,13 +1342,13 @@ func (x *Parse) Reset() {
}
}
-func (x *Parse) String() string {
+func (x *Metadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Parse) ProtoMessage() {}
+func (*Metadata) ProtoMessage() {}
-func (x *Parse) ProtoReflect() protoreflect.Message {
+func (x *Metadata) ProtoReflect() protoreflect.Message {
mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[13]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -1347,158 +1360,118 @@ func (x *Parse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Parse.ProtoReflect.Descriptor instead.
-func (*Parse) Descriptor() ([]byte, []int) {
+// Deprecated: Use Metadata.ProtoReflect.Descriptor instead.
+func (*Metadata) Descriptor() ([]byte, []int) {
return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13}
}
-// Provision consumes source-code from a directory to produce resources.
-// Exactly one of Plan or Apply must be provided in a single session.
-type Provision struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-}
-
-func (x *Provision) Reset() {
- *x = Provision{}
- if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
+func (x *Metadata) GetCoderUrl() string {
+ if x != nil {
+ return x.CoderUrl
}
+ return ""
}
-func (x *Provision) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*Provision) ProtoMessage() {}
-
-func (x *Provision) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
+func (x *Metadata) GetWorkspaceTransition() WorkspaceTransition {
+ if x != nil {
+ return x.WorkspaceTransition
}
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use Provision.ProtoReflect.Descriptor instead.
-func (*Provision) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14}
-}
-
-type Agent_Metadata struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
- DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
- Script string `protobuf:"bytes,3,opt,name=script,proto3" json:"script,omitempty"`
- Interval int64 `protobuf:"varint,4,opt,name=interval,proto3" json:"interval,omitempty"`
- Timeout int64 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"`
+ return WorkspaceTransition_START
}
-func (x *Agent_Metadata) Reset() {
- *x = Agent_Metadata{}
- if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
+func (x *Metadata) GetWorkspaceName() string {
+ if x != nil {
+ return x.WorkspaceName
}
+ return ""
}
-func (x *Agent_Metadata) String() string {
- return protoimpl.X.MessageStringOf(x)
+func (x *Metadata) GetWorkspaceOwner() string {
+ if x != nil {
+ return x.WorkspaceOwner
+ }
+ return ""
}
-func (*Agent_Metadata) ProtoMessage() {}
-
-func (x *Agent_Metadata) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
+func (x *Metadata) GetWorkspaceId() string {
+ if x != nil {
+ return x.WorkspaceId
}
- return mi.MessageOf(x)
+ return ""
}
-// Deprecated: Use Agent_Metadata.ProtoReflect.Descriptor instead.
-func (*Agent_Metadata) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 0}
+func (x *Metadata) GetWorkspaceOwnerId() string {
+ if x != nil {
+ return x.WorkspaceOwnerId
+ }
+ return ""
}
-func (x *Agent_Metadata) GetKey() string {
+func (x *Metadata) GetWorkspaceOwnerEmail() string {
if x != nil {
- return x.Key
+ return x.WorkspaceOwnerEmail
}
return ""
}
-func (x *Agent_Metadata) GetDisplayName() string {
+func (x *Metadata) GetTemplateName() string {
if x != nil {
- return x.DisplayName
+ return x.TemplateName
}
return ""
}
-func (x *Agent_Metadata) GetScript() string {
+func (x *Metadata) GetTemplateVersion() string {
if x != nil {
- return x.Script
+ return x.TemplateVersion
}
return ""
}
-func (x *Agent_Metadata) GetInterval() int64 {
+func (x *Metadata) GetWorkspaceOwnerOidcAccessToken() string {
if x != nil {
- return x.Interval
+ return x.WorkspaceOwnerOidcAccessToken
}
- return 0
+ return ""
}
-func (x *Agent_Metadata) GetTimeout() int64 {
+func (x *Metadata) GetWorkspaceOwnerSessionToken() string {
if x != nil {
- return x.Timeout
+ return x.WorkspaceOwnerSessionToken
}
- return 0
+ return ""
}
-type Resource_Metadata struct {
+// Config represents execution configuration shared by all subsequent requests in the Session
+type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
- Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
- Sensitive bool `protobuf:"varint,3,opt,name=sensitive,proto3" json:"sensitive,omitempty"`
- IsNull bool `protobuf:"varint,4,opt,name=is_null,json=isNull,proto3" json:"is_null,omitempty"`
+ // template_source_archive is a tar of the template source files
+ TemplateSourceArchive []byte `protobuf:"bytes,1,opt,name=template_source_archive,json=templateSourceArchive,proto3" json:"template_source_archive,omitempty"`
+ // state is the provisioner state (if any)
+ State []byte `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"`
+ ProvisionerLogLevel string `protobuf:"bytes,3,opt,name=provisioner_log_level,json=provisionerLogLevel,proto3" json:"provisioner_log_level,omitempty"`
}
-func (x *Resource_Metadata) Reset() {
- *x = Resource_Metadata{}
+func (x *Config) Reset() {
+ *x = Config{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Resource_Metadata) String() string {
+func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Resource_Metadata) ProtoMessage() {}
+func (*Config) ProtoMessage() {}
-func (x *Resource_Metadata) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17]
+func (x *Config) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[14]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1509,64 +1482,56 @@ func (x *Resource_Metadata) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Resource_Metadata.ProtoReflect.Descriptor instead.
-func (*Resource_Metadata) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 0}
-}
-
-func (x *Resource_Metadata) GetKey() string {
- if x != nil {
- return x.Key
- }
- return ""
+// Deprecated: Use Config.ProtoReflect.Descriptor instead.
+func (*Config) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14}
}
-func (x *Resource_Metadata) GetValue() string {
+func (x *Config) GetTemplateSourceArchive() []byte {
if x != nil {
- return x.Value
+ return x.TemplateSourceArchive
}
- return ""
+ return nil
}
-func (x *Resource_Metadata) GetSensitive() bool {
+func (x *Config) GetState() []byte {
if x != nil {
- return x.Sensitive
+ return x.State
}
- return false
+ return nil
}
-func (x *Resource_Metadata) GetIsNull() bool {
+func (x *Config) GetProvisionerLogLevel() string {
if x != nil {
- return x.IsNull
+ return x.ProvisionerLogLevel
}
- return false
+ return ""
}
-type Parse_Request struct {
+// ParseRequest consumes source-code to produce inputs.
+type ParseRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
-
- Directory string `protobuf:"bytes,1,opt,name=directory,proto3" json:"directory,omitempty"`
}
-func (x *Parse_Request) Reset() {
- *x = Parse_Request{}
+func (x *ParseRequest) Reset() {
+ *x = ParseRequest{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Parse_Request) String() string {
+func (x *ParseRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Parse_Request) ProtoMessage() {}
+func (*ParseRequest) ProtoMessage() {}
-func (x *Parse_Request) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18]
+func (x *ParseRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[15]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1577,43 +1542,39 @@ func (x *Parse_Request) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Parse_Request.ProtoReflect.Descriptor instead.
-func (*Parse_Request) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13, 0}
+// Deprecated: Use ParseRequest.ProtoReflect.Descriptor instead.
+func (*ParseRequest) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{15}
}
-func (x *Parse_Request) GetDirectory() string {
- if x != nil {
- return x.Directory
- }
- return ""
-}
-
-type Parse_Complete struct {
+// ParseComplete indicates a request to parse completed.
+type ParseComplete struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- TemplateVariables []*TemplateVariable `protobuf:"bytes,1,rep,name=template_variables,json=templateVariables,proto3" json:"template_variables,omitempty"`
+ Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
+ TemplateVariables []*TemplateVariable `protobuf:"bytes,2,rep,name=template_variables,json=templateVariables,proto3" json:"template_variables,omitempty"`
+ Readme []byte `protobuf:"bytes,3,opt,name=readme,proto3" json:"readme,omitempty"`
}
-func (x *Parse_Complete) Reset() {
- *x = Parse_Complete{}
+func (x *ParseComplete) Reset() {
+ *x = ParseComplete{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Parse_Complete) String() string {
+func (x *ParseComplete) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Parse_Complete) ProtoMessage() {}
+func (*ParseComplete) ProtoMessage() {}
-func (x *Parse_Complete) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19]
+func (x *ParseComplete) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[16]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1624,47 +1585,61 @@ func (x *Parse_Complete) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Parse_Complete.ProtoReflect.Descriptor instead.
-func (*Parse_Complete) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13, 1}
+// Deprecated: Use ParseComplete.ProtoReflect.Descriptor instead.
+func (*ParseComplete) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{16}
}
-func (x *Parse_Complete) GetTemplateVariables() []*TemplateVariable {
+func (x *ParseComplete) GetError() string {
+ if x != nil {
+ return x.Error
+ }
+ return ""
+}
+
+func (x *ParseComplete) GetTemplateVariables() []*TemplateVariable {
if x != nil {
return x.TemplateVariables
}
return nil
}
-type Parse_Response struct {
+func (x *ParseComplete) GetReadme() []byte {
+ if x != nil {
+ return x.Readme
+ }
+ return nil
+}
+
+// PlanRequest asks the provisioner to plan what resources & parameters it will create
+type PlanRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- // Types that are assignable to Type:
- //
- // *Parse_Response_Log
- // *Parse_Response_Complete
- Type isParse_Response_Type `protobuf_oneof:"type"`
+ Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
+ RichParameterValues []*RichParameterValue `protobuf:"bytes,2,rep,name=rich_parameter_values,json=richParameterValues,proto3" json:"rich_parameter_values,omitempty"`
+ VariableValues []*VariableValue `protobuf:"bytes,3,rep,name=variable_values,json=variableValues,proto3" json:"variable_values,omitempty"`
+ GitAuthProviders []*GitAuthProvider `protobuf:"bytes,4,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"`
}
-func (x *Parse_Response) Reset() {
- *x = Parse_Response{}
+func (x *PlanRequest) Reset() {
+ *x = PlanRequest{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Parse_Response) String() string {
+func (x *PlanRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Parse_Response) ProtoMessage() {}
+func (*PlanRequest) ProtoMessage() {}
-func (x *Parse_Response) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20]
+func (x *PlanRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[17]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1675,83 +1650,68 @@ func (x *Parse_Response) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Parse_Response.ProtoReflect.Descriptor instead.
-func (*Parse_Response) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13, 2}
+// Deprecated: Use PlanRequest.ProtoReflect.Descriptor instead.
+func (*PlanRequest) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{17}
}
-func (m *Parse_Response) GetType() isParse_Response_Type {
- if m != nil {
- return m.Type
+func (x *PlanRequest) GetMetadata() *Metadata {
+ if x != nil {
+ return x.Metadata
}
return nil
}
-func (x *Parse_Response) GetLog() *Log {
- if x, ok := x.GetType().(*Parse_Response_Log); ok {
- return x.Log
+func (x *PlanRequest) GetRichParameterValues() []*RichParameterValue {
+ if x != nil {
+ return x.RichParameterValues
}
return nil
}
-func (x *Parse_Response) GetComplete() *Parse_Complete {
- if x, ok := x.GetType().(*Parse_Response_Complete); ok {
- return x.Complete
+func (x *PlanRequest) GetVariableValues() []*VariableValue {
+ if x != nil {
+ return x.VariableValues
}
return nil
}
-type isParse_Response_Type interface {
- isParse_Response_Type()
-}
-
-type Parse_Response_Log struct {
- Log *Log `protobuf:"bytes,1,opt,name=log,proto3,oneof"`
-}
-
-type Parse_Response_Complete struct {
- Complete *Parse_Complete `protobuf:"bytes,2,opt,name=complete,proto3,oneof"`
+func (x *PlanRequest) GetGitAuthProviders() []*GitAuthProvider {
+ if x != nil {
+ return x.GitAuthProviders
+ }
+ return nil
}
-func (*Parse_Response_Log) isParse_Response_Type() {}
-
-func (*Parse_Response_Complete) isParse_Response_Type() {}
-
-type Provision_Metadata struct {
+// PlanComplete indicates a request to plan completed.
+type PlanComplete struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- CoderUrl string `protobuf:"bytes,1,opt,name=coder_url,json=coderUrl,proto3" json:"coder_url,omitempty"`
- WorkspaceTransition WorkspaceTransition `protobuf:"varint,2,opt,name=workspace_transition,json=workspaceTransition,proto3,enum=provisioner.WorkspaceTransition" json:"workspace_transition,omitempty"`
- WorkspaceName string `protobuf:"bytes,3,opt,name=workspace_name,json=workspaceName,proto3" json:"workspace_name,omitempty"`
- WorkspaceOwner string `protobuf:"bytes,4,opt,name=workspace_owner,json=workspaceOwner,proto3" json:"workspace_owner,omitempty"`
- WorkspaceId string `protobuf:"bytes,5,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"`
- WorkspaceOwnerId string `protobuf:"bytes,6,opt,name=workspace_owner_id,json=workspaceOwnerId,proto3" json:"workspace_owner_id,omitempty"`
- WorkspaceOwnerEmail string `protobuf:"bytes,7,opt,name=workspace_owner_email,json=workspaceOwnerEmail,proto3" json:"workspace_owner_email,omitempty"`
- TemplateName string `protobuf:"bytes,8,opt,name=template_name,json=templateName,proto3" json:"template_name,omitempty"`
- TemplateVersion string `protobuf:"bytes,9,opt,name=template_version,json=templateVersion,proto3" json:"template_version,omitempty"`
- WorkspaceOwnerOidcAccessToken string `protobuf:"bytes,10,opt,name=workspace_owner_oidc_access_token,json=workspaceOwnerOidcAccessToken,proto3" json:"workspace_owner_oidc_access_token,omitempty"`
- WorkspaceOwnerSessionToken string `protobuf:"bytes,11,opt,name=workspace_owner_session_token,json=workspaceOwnerSessionToken,proto3" json:"workspace_owner_session_token,omitempty"`
+ Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
+ Resources []*Resource `protobuf:"bytes,2,rep,name=resources,proto3" json:"resources,omitempty"`
+ Parameters []*RichParameter `protobuf:"bytes,3,rep,name=parameters,proto3" json:"parameters,omitempty"`
+ GitAuthProviders []string `protobuf:"bytes,4,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"`
}
-func (x *Provision_Metadata) Reset() {
- *x = Provision_Metadata{}
+func (x *PlanComplete) Reset() {
+ *x = PlanComplete{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Provision_Metadata) String() string {
+func (x *PlanComplete) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Provision_Metadata) ProtoMessage() {}
+func (*PlanComplete) ProtoMessage() {}
-func (x *Provision_Metadata) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21]
+func (x *PlanComplete) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[18]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1762,118 +1722,118 @@ func (x *Provision_Metadata) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Provision_Metadata.ProtoReflect.Descriptor instead.
-func (*Provision_Metadata) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 0}
+// Deprecated: Use PlanComplete.ProtoReflect.Descriptor instead.
+func (*PlanComplete) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{18}
}
-func (x *Provision_Metadata) GetCoderUrl() string {
+func (x *PlanComplete) GetError() string {
if x != nil {
- return x.CoderUrl
+ return x.Error
}
return ""
}
-func (x *Provision_Metadata) GetWorkspaceTransition() WorkspaceTransition {
+func (x *PlanComplete) GetResources() []*Resource {
if x != nil {
- return x.WorkspaceTransition
+ return x.Resources
}
- return WorkspaceTransition_START
+ return nil
}
-func (x *Provision_Metadata) GetWorkspaceName() string {
+func (x *PlanComplete) GetParameters() []*RichParameter {
if x != nil {
- return x.WorkspaceName
+ return x.Parameters
}
- return ""
+ return nil
}
-func (x *Provision_Metadata) GetWorkspaceOwner() string {
+func (x *PlanComplete) GetGitAuthProviders() []string {
if x != nil {
- return x.WorkspaceOwner
+ return x.GitAuthProviders
}
- return ""
+ return nil
}
-func (x *Provision_Metadata) GetWorkspaceId() string {
- if x != nil {
- return x.WorkspaceId
- }
- return ""
-}
+// ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response
+// in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session.
+type ApplyRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
-func (x *Provision_Metadata) GetWorkspaceOwnerId() string {
- if x != nil {
- return x.WorkspaceOwnerId
- }
- return ""
+ Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
}
-func (x *Provision_Metadata) GetWorkspaceOwnerEmail() string {
- if x != nil {
- return x.WorkspaceOwnerEmail
+func (x *ApplyRequest) Reset() {
+ *x = ApplyRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
}
- return ""
}
-func (x *Provision_Metadata) GetTemplateName() string {
- if x != nil {
- return x.TemplateName
- }
- return ""
+func (x *ApplyRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
}
-func (x *Provision_Metadata) GetTemplateVersion() string {
- if x != nil {
- return x.TemplateVersion
+func (*ApplyRequest) ProtoMessage() {}
+
+func (x *ApplyRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[19]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
}
- return ""
+ return mi.MessageOf(x)
}
-func (x *Provision_Metadata) GetWorkspaceOwnerOidcAccessToken() string {
- if x != nil {
- return x.WorkspaceOwnerOidcAccessToken
- }
- return ""
+// Deprecated: Use ApplyRequest.ProtoReflect.Descriptor instead.
+func (*ApplyRequest) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{19}
}
-func (x *Provision_Metadata) GetWorkspaceOwnerSessionToken() string {
+func (x *ApplyRequest) GetMetadata() *Metadata {
if x != nil {
- return x.WorkspaceOwnerSessionToken
+ return x.Metadata
}
- return ""
+ return nil
}
-// Config represents execution configuration shared by both Plan and
-// Apply commands.
-type Provision_Config struct {
+// ApplyComplete indicates a request to apply completed.
+type ApplyComplete struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Directory string `protobuf:"bytes,1,opt,name=directory,proto3" json:"directory,omitempty"`
- State []byte `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"`
- Metadata *Provision_Metadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"`
- ProvisionerLogLevel string `protobuf:"bytes,4,opt,name=provisioner_log_level,json=provisionerLogLevel,proto3" json:"provisioner_log_level,omitempty"`
+ State []byte `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"`
+ Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
+ Resources []*Resource `protobuf:"bytes,3,rep,name=resources,proto3" json:"resources,omitempty"`
+ Parameters []*RichParameter `protobuf:"bytes,4,rep,name=parameters,proto3" json:"parameters,omitempty"`
+ GitAuthProviders []string `protobuf:"bytes,5,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"`
}
-func (x *Provision_Config) Reset() {
- *x = Provision_Config{}
+func (x *ApplyComplete) Reset() {
+ *x = ApplyComplete{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Provision_Config) String() string {
+func (x *ApplyComplete) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Provision_Config) ProtoMessage() {}
+func (*ApplyComplete) ProtoMessage() {}
-func (x *Provision_Config) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22]
+func (x *ApplyComplete) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[20]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1884,67 +1844,70 @@ func (x *Provision_Config) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Provision_Config.ProtoReflect.Descriptor instead.
-func (*Provision_Config) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 1}
+// Deprecated: Use ApplyComplete.ProtoReflect.Descriptor instead.
+func (*ApplyComplete) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{20}
}
-func (x *Provision_Config) GetDirectory() string {
+func (x *ApplyComplete) GetState() []byte {
if x != nil {
- return x.Directory
+ return x.State
+ }
+ return nil
+}
+
+func (x *ApplyComplete) GetError() string {
+ if x != nil {
+ return x.Error
}
return ""
}
-func (x *Provision_Config) GetState() []byte {
+func (x *ApplyComplete) GetResources() []*Resource {
if x != nil {
- return x.State
+ return x.Resources
}
return nil
}
-func (x *Provision_Config) GetMetadata() *Provision_Metadata {
+func (x *ApplyComplete) GetParameters() []*RichParameter {
if x != nil {
- return x.Metadata
+ return x.Parameters
}
return nil
}
-func (x *Provision_Config) GetProvisionerLogLevel() string {
+func (x *ApplyComplete) GetGitAuthProviders() []string {
if x != nil {
- return x.ProvisionerLogLevel
+ return x.GitAuthProviders
}
- return ""
+ return nil
}
-type Provision_Plan struct {
+// CancelRequest requests that the previous request be canceled gracefully.
+type CancelRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
-
- Config *Provision_Config `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
- RichParameterValues []*RichParameterValue `protobuf:"bytes,3,rep,name=rich_parameter_values,json=richParameterValues,proto3" json:"rich_parameter_values,omitempty"`
- VariableValues []*VariableValue `protobuf:"bytes,4,rep,name=variable_values,json=variableValues,proto3" json:"variable_values,omitempty"`
- GitAuthProviders []*GitAuthProvider `protobuf:"bytes,5,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"`
}
-func (x *Provision_Plan) Reset() {
- *x = Provision_Plan{}
+func (x *CancelRequest) Reset() {
+ *x = CancelRequest{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Provision_Plan) String() string {
+func (x *CancelRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Provision_Plan) ProtoMessage() {}
+func (*CancelRequest) ProtoMessage() {}
-func (x *Provision_Plan) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23]
+func (x *CancelRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[21]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1955,65 +1918,43 @@ func (x *Provision_Plan) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Provision_Plan.ProtoReflect.Descriptor instead.
-func (*Provision_Plan) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 2}
-}
-
-func (x *Provision_Plan) GetConfig() *Provision_Config {
- if x != nil {
- return x.Config
- }
- return nil
-}
-
-func (x *Provision_Plan) GetRichParameterValues() []*RichParameterValue {
- if x != nil {
- return x.RichParameterValues
- }
- return nil
-}
-
-func (x *Provision_Plan) GetVariableValues() []*VariableValue {
- if x != nil {
- return x.VariableValues
- }
- return nil
-}
-
-func (x *Provision_Plan) GetGitAuthProviders() []*GitAuthProvider {
- if x != nil {
- return x.GitAuthProviders
- }
- return nil
+// Deprecated: Use CancelRequest.ProtoReflect.Descriptor instead.
+func (*CancelRequest) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{21}
}
-type Provision_Apply struct {
+type Request struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Config *Provision_Config `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
- Plan []byte `protobuf:"bytes,2,opt,name=plan,proto3" json:"plan,omitempty"`
+ // Types that are assignable to Type:
+ //
+ // *Request_Config
+ // *Request_Parse
+ // *Request_Plan
+ // *Request_Apply
+ // *Request_Cancel
+ Type isRequest_Type `protobuf_oneof:"type"`
}
-func (x *Provision_Apply) Reset() {
- *x = Provision_Apply{}
+func (x *Request) Reset() {
+ *x = Request{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Provision_Apply) String() string {
+func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Provision_Apply) ProtoMessage() {}
+func (*Request) ProtoMessage() {}
-func (x *Provision_Apply) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24]
+func (x *Request) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[22]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2024,93 +1965,118 @@ func (x *Provision_Apply) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Provision_Apply.ProtoReflect.Descriptor instead.
-func (*Provision_Apply) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 3}
+// Deprecated: Use Request.ProtoReflect.Descriptor instead.
+func (*Request) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{22}
}
-func (x *Provision_Apply) GetConfig() *Provision_Config {
- if x != nil {
+func (m *Request) GetType() isRequest_Type {
+ if m != nil {
+ return m.Type
+ }
+ return nil
+}
+
+func (x *Request) GetConfig() *Config {
+ if x, ok := x.GetType().(*Request_Config); ok {
return x.Config
}
return nil
}
-func (x *Provision_Apply) GetPlan() []byte {
- if x != nil {
+func (x *Request) GetParse() *ParseRequest {
+ if x, ok := x.GetType().(*Request_Parse); ok {
+ return x.Parse
+ }
+ return nil
+}
+
+func (x *Request) GetPlan() *PlanRequest {
+ if x, ok := x.GetType().(*Request_Plan); ok {
return x.Plan
}
return nil
}
-type Provision_Cancel struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
+func (x *Request) GetApply() *ApplyRequest {
+ if x, ok := x.GetType().(*Request_Apply); ok {
+ return x.Apply
+ }
+ return nil
}
-func (x *Provision_Cancel) Reset() {
- *x = Provision_Cancel{}
- if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
+func (x *Request) GetCancel() *CancelRequest {
+ if x, ok := x.GetType().(*Request_Cancel); ok {
+ return x.Cancel
}
+ return nil
}
-func (x *Provision_Cancel) String() string {
- return protoimpl.X.MessageStringOf(x)
+type isRequest_Type interface {
+ isRequest_Type()
+}
+
+type Request_Config struct {
+ Config *Config `protobuf:"bytes,1,opt,name=config,proto3,oneof"`
}
-func (*Provision_Cancel) ProtoMessage() {}
+type Request_Parse struct {
+ Parse *ParseRequest `protobuf:"bytes,2,opt,name=parse,proto3,oneof"`
+}
-func (x *Provision_Cancel) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[25]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
+type Request_Plan struct {
+ Plan *PlanRequest `protobuf:"bytes,3,opt,name=plan,proto3,oneof"`
+}
+
+type Request_Apply struct {
+ Apply *ApplyRequest `protobuf:"bytes,4,opt,name=apply,proto3,oneof"`
}
-// Deprecated: Use Provision_Cancel.ProtoReflect.Descriptor instead.
-func (*Provision_Cancel) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 4}
+type Request_Cancel struct {
+ Cancel *CancelRequest `protobuf:"bytes,5,opt,name=cancel,proto3,oneof"`
}
-type Provision_Request struct {
+func (*Request_Config) isRequest_Type() {}
+
+func (*Request_Parse) isRequest_Type() {}
+
+func (*Request_Plan) isRequest_Type() {}
+
+func (*Request_Apply) isRequest_Type() {}
+
+func (*Request_Cancel) isRequest_Type() {}
+
+type Response struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to Type:
//
- // *Provision_Request_Plan
- // *Provision_Request_Apply
- // *Provision_Request_Cancel
- Type isProvision_Request_Type `protobuf_oneof:"type"`
+ // *Response_Log
+ // *Response_Parse
+ // *Response_Plan
+ // *Response_Apply
+ Type isResponse_Type `protobuf_oneof:"type"`
}
-func (x *Provision_Request) Reset() {
- *x = Provision_Request{}
+func (x *Response) Reset() {
+ *x = Response{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Provision_Request) String() string {
+func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Provision_Request) ProtoMessage() {}
+func (*Response) ProtoMessage() {}
-func (x *Provision_Request) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26]
+func (x *Response) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[23]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2121,91 +2087,103 @@ func (x *Provision_Request) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Provision_Request.ProtoReflect.Descriptor instead.
-func (*Provision_Request) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 5}
+// Deprecated: Use Response.ProtoReflect.Descriptor instead.
+func (*Response) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{23}
}
-func (m *Provision_Request) GetType() isProvision_Request_Type {
+func (m *Response) GetType() isResponse_Type {
if m != nil {
return m.Type
}
return nil
}
-func (x *Provision_Request) GetPlan() *Provision_Plan {
- if x, ok := x.GetType().(*Provision_Request_Plan); ok {
- return x.Plan
+func (x *Response) GetLog() *Log {
+ if x, ok := x.GetType().(*Response_Log); ok {
+ return x.Log
}
return nil
}
-func (x *Provision_Request) GetApply() *Provision_Apply {
- if x, ok := x.GetType().(*Provision_Request_Apply); ok {
- return x.Apply
+func (x *Response) GetParse() *ParseComplete {
+ if x, ok := x.GetType().(*Response_Parse); ok {
+ return x.Parse
}
return nil
}
-func (x *Provision_Request) GetCancel() *Provision_Cancel {
- if x, ok := x.GetType().(*Provision_Request_Cancel); ok {
- return x.Cancel
+func (x *Response) GetPlan() *PlanComplete {
+ if x, ok := x.GetType().(*Response_Plan); ok {
+ return x.Plan
+ }
+ return nil
+}
+
+func (x *Response) GetApply() *ApplyComplete {
+ if x, ok := x.GetType().(*Response_Apply); ok {
+ return x.Apply
}
return nil
}
-type isProvision_Request_Type interface {
- isProvision_Request_Type()
+type isResponse_Type interface {
+ isResponse_Type()
+}
+
+type Response_Log struct {
+ Log *Log `protobuf:"bytes,1,opt,name=log,proto3,oneof"`
}
-type Provision_Request_Plan struct {
- Plan *Provision_Plan `protobuf:"bytes,1,opt,name=plan,proto3,oneof"`
+type Response_Parse struct {
+ Parse *ParseComplete `protobuf:"bytes,2,opt,name=parse,proto3,oneof"`
}
-type Provision_Request_Apply struct {
- Apply *Provision_Apply `protobuf:"bytes,2,opt,name=apply,proto3,oneof"`
+type Response_Plan struct {
+ Plan *PlanComplete `protobuf:"bytes,3,opt,name=plan,proto3,oneof"`
}
-type Provision_Request_Cancel struct {
- Cancel *Provision_Cancel `protobuf:"bytes,3,opt,name=cancel,proto3,oneof"`
+type Response_Apply struct {
+ Apply *ApplyComplete `protobuf:"bytes,4,opt,name=apply,proto3,oneof"`
}
-func (*Provision_Request_Plan) isProvision_Request_Type() {}
+func (*Response_Log) isResponse_Type() {}
-func (*Provision_Request_Apply) isProvision_Request_Type() {}
+func (*Response_Parse) isResponse_Type() {}
-func (*Provision_Request_Cancel) isProvision_Request_Type() {}
+func (*Response_Plan) isResponse_Type() {}
-type Provision_Complete struct {
+func (*Response_Apply) isResponse_Type() {}
+
+type Agent_Metadata struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- State []byte `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"`
- Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
- Resources []*Resource `protobuf:"bytes,3,rep,name=resources,proto3" json:"resources,omitempty"`
- Parameters []*RichParameter `protobuf:"bytes,4,rep,name=parameters,proto3" json:"parameters,omitempty"`
- GitAuthProviders []string `protobuf:"bytes,5,rep,name=git_auth_providers,json=gitAuthProviders,proto3" json:"git_auth_providers,omitempty"`
- Plan []byte `protobuf:"bytes,6,opt,name=plan,proto3" json:"plan,omitempty"`
+ Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
+ DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
+ Script string `protobuf:"bytes,3,opt,name=script,proto3" json:"script,omitempty"`
+ Interval int64 `protobuf:"varint,4,opt,name=interval,proto3" json:"interval,omitempty"`
+ Timeout int64 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"`
}
-func (x *Provision_Complete) Reset() {
- *x = Provision_Complete{}
+func (x *Agent_Metadata) Reset() {
+ *x = Agent_Metadata{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Provision_Complete) String() string {
+func (x *Agent_Metadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Provision_Complete) ProtoMessage() {}
+func (*Agent_Metadata) ProtoMessage() {}
-func (x *Provision_Complete) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[27]
+func (x *Agent_Metadata) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[24]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2216,82 +2194,74 @@ func (x *Provision_Complete) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Provision_Complete.ProtoReflect.Descriptor instead.
-func (*Provision_Complete) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 6}
-}
-
-func (x *Provision_Complete) GetState() []byte {
- if x != nil {
- return x.State
- }
- return nil
+// Deprecated: Use Agent_Metadata.ProtoReflect.Descriptor instead.
+func (*Agent_Metadata) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9, 0}
}
-func (x *Provision_Complete) GetError() string {
+func (x *Agent_Metadata) GetKey() string {
if x != nil {
- return x.Error
+ return x.Key
}
return ""
}
-func (x *Provision_Complete) GetResources() []*Resource {
+func (x *Agent_Metadata) GetDisplayName() string {
if x != nil {
- return x.Resources
+ return x.DisplayName
}
- return nil
+ return ""
}
-func (x *Provision_Complete) GetParameters() []*RichParameter {
+func (x *Agent_Metadata) GetScript() string {
if x != nil {
- return x.Parameters
+ return x.Script
}
- return nil
+ return ""
}
-func (x *Provision_Complete) GetGitAuthProviders() []string {
+func (x *Agent_Metadata) GetInterval() int64 {
if x != nil {
- return x.GitAuthProviders
+ return x.Interval
}
- return nil
+ return 0
}
-func (x *Provision_Complete) GetPlan() []byte {
+func (x *Agent_Metadata) GetTimeout() int64 {
if x != nil {
- return x.Plan
+ return x.Timeout
}
- return nil
+ return 0
}
-type Provision_Response struct {
+type Resource_Metadata struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- // Types that are assignable to Type:
- //
- // *Provision_Response_Log
- // *Provision_Response_Complete
- Type isProvision_Response_Type `protobuf_oneof:"type"`
+ Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
+ Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
+ Sensitive bool `protobuf:"varint,3,opt,name=sensitive,proto3" json:"sensitive,omitempty"`
+ IsNull bool `protobuf:"varint,4,opt,name=is_null,json=isNull,proto3" json:"is_null,omitempty"`
}
-func (x *Provision_Response) Reset() {
- *x = Provision_Response{}
+func (x *Resource_Metadata) Reset() {
+ *x = Resource_Metadata{}
if protoimpl.UnsafeEnabled {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28]
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
-func (x *Provision_Response) String() string {
+func (x *Resource_Metadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*Provision_Response) ProtoMessage() {}
+func (*Resource_Metadata) ProtoMessage() {}
-func (x *Provision_Response) ProtoReflect() protoreflect.Message {
- mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[28]
+func (x *Resource_Metadata) ProtoReflect() protoreflect.Message {
+ mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[26]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2302,48 +2272,39 @@ func (x *Provision_Response) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use Provision_Response.ProtoReflect.Descriptor instead.
-func (*Provision_Response) Descriptor() ([]byte, []int) {
- return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14, 7}
+// Deprecated: Use Resource_Metadata.ProtoReflect.Descriptor instead.
+func (*Resource_Metadata) Descriptor() ([]byte, []int) {
+ return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12, 0}
}
-func (m *Provision_Response) GetType() isProvision_Response_Type {
- if m != nil {
- return m.Type
+func (x *Resource_Metadata) GetKey() string {
+ if x != nil {
+ return x.Key
}
- return nil
+ return ""
}
-func (x *Provision_Response) GetLog() *Log {
- if x, ok := x.GetType().(*Provision_Response_Log); ok {
- return x.Log
+func (x *Resource_Metadata) GetValue() string {
+ if x != nil {
+ return x.Value
}
- return nil
+ return ""
}
-func (x *Provision_Response) GetComplete() *Provision_Complete {
- if x, ok := x.GetType().(*Provision_Response_Complete); ok {
- return x.Complete
+func (x *Resource_Metadata) GetSensitive() bool {
+ if x != nil {
+ return x.Sensitive
}
- return nil
-}
-
-type isProvision_Response_Type interface {
- isProvision_Response_Type()
-}
-
-type Provision_Response_Log struct {
- Log *Log `protobuf:"bytes,1,opt,name=log,proto3,oneof"`
+ return false
}
-type Provision_Response_Complete struct {
- Complete *Provision_Complete `protobuf:"bytes,2,opt,name=complete,proto3,oneof"`
+func (x *Resource_Metadata) GetIsNull() bool {
+ if x != nil {
+ return x.IsNull
+ }
+ return false
}
-func (*Provision_Response_Log) isProvision_Response_Type() {}
-
-func (*Provision_Response_Complete) isProvision_Response_Type() {}
-
var File_provisionersdk_proto_provisioner_proto protoreflect.FileDescriptor
var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{
@@ -2543,154 +2504,161 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74,
0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69,
0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18,
- 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0x85, 0x02,
- 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79,
- 0x1a, 0x5e, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x4c, 0x0a, 0x12,
- 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c,
- 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
- 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56,
- 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74,
- 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03,
- 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03,
- 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c,
- 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
- 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74,
- 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a,
- 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x91, 0x0d, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73,
- 0x69, 0x6f, 0x6e, 0x1a, 0xae, 0x04, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
- 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a,
- 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73,
- 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72,
- 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70,
- 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77,
- 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69,
- 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f,
- 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b,
- 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72,
- 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e,
- 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f,
- 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70,
- 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61,
- 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65,
- 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65,
- 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e,
- 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, 0x6c,
- 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
- 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10,
- 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
- 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65,
- 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, 0x73,
- 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f,
- 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e,
- 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65,
- 0x6e, 0x12, 0x41, 0x0a, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f,
- 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b,
- 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70,
- 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54,
- 0x6f, 0x6b, 0x65, 0x6e, 0x1a, 0xad, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
- 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x14, 0x0a,
- 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74,
- 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18,
- 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
- 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65,
- 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
- 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f,
- 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c,
- 0x65, 0x76, 0x65, 0x6c, 0x1a, 0xa9, 0x02, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x35, 0x0a,
- 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e,
- 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f,
- 0x6e, 0x66, 0x69, 0x67, 0x12, 0x53, 0x0a, 0x15, 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72,
- 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
- 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
- 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56,
- 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65,
- 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72,
- 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03,
+ 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0xae, 0x04,
+ 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f,
+ 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63,
+ 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73,
+ 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
+ 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61,
+ 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61,
+ 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e,
+ 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e,
+ 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65,
+ 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f,
+ 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c,
+ 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12,
+ 0x2c, 0x0a, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e,
+ 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72,
+ 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a,
+ 0x15, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72,
+ 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f,
+ 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69,
+ 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61,
+ 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61,
+ 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61,
+ 0x74, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
+ 0x6e, 0x12, 0x48, 0x0a, 0x21, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f,
+ 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73,
+ 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f,
+ 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63,
+ 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x0a, 0x1d, 0x77,
+ 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73,
+ 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e,
+ 0x65, 0x72, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x8a,
+ 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x17, 0x74, 0x65, 0x6d,
+ 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x72, 0x63,
+ 0x68, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x74, 0x65, 0x6d, 0x70,
+ 0x6c, 0x61, 0x74, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76,
+ 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
+ 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69,
+ 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c,
+ 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
+ 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x0e, 0x0a, 0x0c, 0x50,
+ 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8b, 0x01, 0x0a, 0x0d,
+ 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a,
+ 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72,
+ 0x72, 0x6f, 0x72, 0x12, 0x4c, 0x0a, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f,
+ 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
+ 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x65,
+ 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x11,
+ 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65,
+ 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x0c, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, 0x22, 0xa6, 0x02, 0x0a, 0x0b, 0x50, 0x6c,
+ 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74,
+ 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72,
+ 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
+ 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x53, 0x0a, 0x15,
+ 0x72, 0x69, 0x63, 0x68, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76,
+ 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72,
+ 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61,
+ 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x13, 0x72, 0x69,
+ 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65,
+ 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x76, 0x61,
+ 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f,
+ 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c,
+ 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65,
+ 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75,
+ 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03,
+ 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
+ 0x2e, 0x47, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
+ 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
+ 0x72, 0x73, 0x22, 0xc3, 0x01, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c,
+ 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70,
+ 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a,
+ 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03,
+ 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
+ 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a,
+ 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69,
+ 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
+ 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50,
+ 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x41, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c,
+ 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
+ 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f,
+ 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+ 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xda, 0x01, 0x0a, 0x0d,
+ 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a,
+ 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74,
+ 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70,
+ 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a,
+ 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
- 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e,
- 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x4a,
- 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69,
- 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f,
- 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x47, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68,
- 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74,
- 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03,
- 0x1a, 0x52, 0x0a, 0x05, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x35, 0x0a, 0x06, 0x63, 0x6f, 0x6e,
- 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
- 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
- 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04,
- 0x70, 0x6c, 0x61, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0xb3,
- 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x04, 0x70, 0x6c,
- 0x61, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69,
- 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
- 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x34, 0x0a,
- 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70,
- 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69,
- 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70,
- 0x70, 0x6c, 0x79, 0x12, 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x03, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
- 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63,
- 0x65, 0x6c, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04,
- 0x74, 0x79, 0x70, 0x65, 0x1a, 0xe9, 0x01, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74,
- 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
- 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a,
- 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
- 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73,
- 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
- 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74,
- 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c,
- 0x0a, 0x12, 0x67, 0x69, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69,
- 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41,
- 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04,
- 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e,
- 0x1a, 0x77, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03,
- 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c,
- 0x6f, 0x67, 0x12, 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
- 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d,
- 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74,
- 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67,
- 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00,
- 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49,
- 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12,
- 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70,
- 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a,
- 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48,
- 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50,
- 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73,
- 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09,
- 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f,
- 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02,
- 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
- 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76,
- 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
- 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
- 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e,
- 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
- 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e,
- 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
- 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72,
- 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f,
- 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a,
+ 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69,
+ 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73,
+ 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x74, 0x41, 0x75, 0x74, 0x68, 0x50,
+ 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63,
+ 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
+ 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f,
+ 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
+ 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00,
+ 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f,
+ 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48,
+ 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79,
+ 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69,
+ 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61,
+ 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f,
+ 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c,
+ 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72,
+ 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70,
+ 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f,
+ 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f,
+ 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12,
+ 0x2f, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
+ 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e,
+ 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e,
+ 0x12, 0x32, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70,
+ 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61,
+ 0x70, 0x70, 0x6c, 0x79, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08,
+ 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43,
+ 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08,
+ 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e,
+ 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a,
+ 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c,
+ 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41,
+ 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a,
+ 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f,
+ 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f,
+ 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04,
+ 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f,
+ 0x59, 0x10, 0x02, 0x32, 0x49, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
+ 0x65, 0x72, 0x12, 0x3a, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e,
+ 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65,
+ 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30,
+ 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64,
+ 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76,
+ 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -2706,7 +2674,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte {
}
var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
-var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
+var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{
(LogLevel)(0), // 0: provisioner.LogLevel
(AppSharingLevel)(0), // 1: provisioner.AppSharingLevel
@@ -2724,59 +2692,58 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{
(*App)(nil), // 13: provisioner.App
(*Healthcheck)(nil), // 14: provisioner.Healthcheck
(*Resource)(nil), // 15: provisioner.Resource
- (*Parse)(nil), // 16: provisioner.Parse
- (*Provision)(nil), // 17: provisioner.Provision
- (*Agent_Metadata)(nil), // 18: provisioner.Agent.Metadata
- nil, // 19: provisioner.Agent.EnvEntry
- (*Resource_Metadata)(nil), // 20: provisioner.Resource.Metadata
- (*Parse_Request)(nil), // 21: provisioner.Parse.Request
- (*Parse_Complete)(nil), // 22: provisioner.Parse.Complete
- (*Parse_Response)(nil), // 23: provisioner.Parse.Response
- (*Provision_Metadata)(nil), // 24: provisioner.Provision.Metadata
- (*Provision_Config)(nil), // 25: provisioner.Provision.Config
- (*Provision_Plan)(nil), // 26: provisioner.Provision.Plan
- (*Provision_Apply)(nil), // 27: provisioner.Provision.Apply
- (*Provision_Cancel)(nil), // 28: provisioner.Provision.Cancel
- (*Provision_Request)(nil), // 29: provisioner.Provision.Request
- (*Provision_Complete)(nil), // 30: provisioner.Provision.Complete
- (*Provision_Response)(nil), // 31: provisioner.Provision.Response
+ (*Metadata)(nil), // 16: provisioner.Metadata
+ (*Config)(nil), // 17: provisioner.Config
+ (*ParseRequest)(nil), // 18: provisioner.ParseRequest
+ (*ParseComplete)(nil), // 19: provisioner.ParseComplete
+ (*PlanRequest)(nil), // 20: provisioner.PlanRequest
+ (*PlanComplete)(nil), // 21: provisioner.PlanComplete
+ (*ApplyRequest)(nil), // 22: provisioner.ApplyRequest
+ (*ApplyComplete)(nil), // 23: provisioner.ApplyComplete
+ (*CancelRequest)(nil), // 24: provisioner.CancelRequest
+ (*Request)(nil), // 25: provisioner.Request
+ (*Response)(nil), // 26: provisioner.Response
+ (*Agent_Metadata)(nil), // 27: provisioner.Agent.Metadata
+ nil, // 28: provisioner.Agent.EnvEntry
+ (*Resource_Metadata)(nil), // 29: provisioner.Resource.Metadata
}
var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{
5, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption
0, // 1: provisioner.Log.level:type_name -> provisioner.LogLevel
- 19, // 2: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry
+ 28, // 2: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry
13, // 3: provisioner.Agent.apps:type_name -> provisioner.App
- 18, // 4: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata
+ 27, // 4: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata
14, // 5: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck
1, // 6: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel
12, // 7: provisioner.Resource.agents:type_name -> provisioner.Agent
- 20, // 8: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata
- 4, // 9: provisioner.Parse.Complete.template_variables:type_name -> provisioner.TemplateVariable
- 9, // 10: provisioner.Parse.Response.log:type_name -> provisioner.Log
- 22, // 11: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete
- 2, // 12: provisioner.Provision.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition
- 24, // 13: provisioner.Provision.Config.metadata:type_name -> provisioner.Provision.Metadata
- 25, // 14: provisioner.Provision.Plan.config:type_name -> provisioner.Provision.Config
- 7, // 15: provisioner.Provision.Plan.rich_parameter_values:type_name -> provisioner.RichParameterValue
- 8, // 16: provisioner.Provision.Plan.variable_values:type_name -> provisioner.VariableValue
- 11, // 17: provisioner.Provision.Plan.git_auth_providers:type_name -> provisioner.GitAuthProvider
- 25, // 18: provisioner.Provision.Apply.config:type_name -> provisioner.Provision.Config
- 26, // 19: provisioner.Provision.Request.plan:type_name -> provisioner.Provision.Plan
- 27, // 20: provisioner.Provision.Request.apply:type_name -> provisioner.Provision.Apply
- 28, // 21: provisioner.Provision.Request.cancel:type_name -> provisioner.Provision.Cancel
- 15, // 22: provisioner.Provision.Complete.resources:type_name -> provisioner.Resource
- 6, // 23: provisioner.Provision.Complete.parameters:type_name -> provisioner.RichParameter
- 9, // 24: provisioner.Provision.Response.log:type_name -> provisioner.Log
- 30, // 25: provisioner.Provision.Response.complete:type_name -> provisioner.Provision.Complete
- 21, // 26: provisioner.Provisioner.Parse:input_type -> provisioner.Parse.Request
- 29, // 27: provisioner.Provisioner.Provision:input_type -> provisioner.Provision.Request
- 23, // 28: provisioner.Provisioner.Parse:output_type -> provisioner.Parse.Response
- 31, // 29: provisioner.Provisioner.Provision:output_type -> provisioner.Provision.Response
- 28, // [28:30] is the sub-list for method output_type
- 26, // [26:28] is the sub-list for method input_type
- 26, // [26:26] is the sub-list for extension type_name
- 26, // [26:26] is the sub-list for extension extendee
- 0, // [0:26] is the sub-list for field type_name
+ 29, // 8: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata
+ 2, // 9: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition
+ 4, // 10: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable
+ 16, // 11: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata
+ 7, // 12: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue
+ 8, // 13: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue
+ 11, // 14: provisioner.PlanRequest.git_auth_providers:type_name -> provisioner.GitAuthProvider
+ 15, // 15: provisioner.PlanComplete.resources:type_name -> provisioner.Resource
+ 6, // 16: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter
+ 16, // 17: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata
+ 15, // 18: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource
+ 6, // 19: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter
+ 17, // 20: provisioner.Request.config:type_name -> provisioner.Config
+ 18, // 21: provisioner.Request.parse:type_name -> provisioner.ParseRequest
+ 20, // 22: provisioner.Request.plan:type_name -> provisioner.PlanRequest
+ 22, // 23: provisioner.Request.apply:type_name -> provisioner.ApplyRequest
+ 24, // 24: provisioner.Request.cancel:type_name -> provisioner.CancelRequest
+ 9, // 25: provisioner.Response.log:type_name -> provisioner.Log
+ 19, // 26: provisioner.Response.parse:type_name -> provisioner.ParseComplete
+ 21, // 27: provisioner.Response.plan:type_name -> provisioner.PlanComplete
+ 23, // 28: provisioner.Response.apply:type_name -> provisioner.ApplyComplete
+ 25, // 29: provisioner.Provisioner.Session:input_type -> provisioner.Request
+ 26, // 30: provisioner.Provisioner.Session:output_type -> provisioner.Response
+ 30, // [30:31] is the sub-list for method output_type
+ 29, // [29:30] is the sub-list for method input_type
+ 29, // [29:29] is the sub-list for extension type_name
+ 29, // [29:29] is the sub-list for extension extendee
+ 0, // [0:29] is the sub-list for field type_name
}
func init() { file_provisionersdk_proto_provisioner_proto_init() }
@@ -2942,7 +2909,7 @@ func file_provisionersdk_proto_provisioner_proto_init() {
}
}
file_provisionersdk_proto_provisioner_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Parse); i {
+ switch v := v.(*Metadata); i {
case 0:
return &v.state
case 1:
@@ -2954,7 +2921,7 @@ func file_provisionersdk_proto_provisioner_proto_init() {
}
}
file_provisionersdk_proto_provisioner_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Provision); i {
+ switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
@@ -2966,7 +2933,7 @@ func file_provisionersdk_proto_provisioner_proto_init() {
}
}
file_provisionersdk_proto_provisioner_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Agent_Metadata); i {
+ switch v := v.(*ParseRequest); i {
case 0:
return &v.state
case 1:
@@ -2977,8 +2944,8 @@ func file_provisionersdk_proto_provisioner_proto_init() {
return nil
}
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Resource_Metadata); i {
+ file_provisionersdk_proto_provisioner_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ParseComplete); i {
case 0:
return &v.state
case 1:
@@ -2989,8 +2956,8 @@ func file_provisionersdk_proto_provisioner_proto_init() {
return nil
}
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Parse_Request); i {
+ file_provisionersdk_proto_provisioner_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PlanRequest); i {
case 0:
return &v.state
case 1:
@@ -3001,8 +2968,8 @@ func file_provisionersdk_proto_provisioner_proto_init() {
return nil
}
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Parse_Complete); i {
+ file_provisionersdk_proto_provisioner_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*PlanComplete); i {
case 0:
return &v.state
case 1:
@@ -3013,8 +2980,8 @@ func file_provisionersdk_proto_provisioner_proto_init() {
return nil
}
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Parse_Response); i {
+ file_provisionersdk_proto_provisioner_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ApplyRequest); i {
case 0:
return &v.state
case 1:
@@ -3025,8 +2992,8 @@ func file_provisionersdk_proto_provisioner_proto_init() {
return nil
}
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Provision_Metadata); i {
+ file_provisionersdk_proto_provisioner_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ApplyComplete); i {
case 0:
return &v.state
case 1:
@@ -3037,8 +3004,8 @@ func file_provisionersdk_proto_provisioner_proto_init() {
return nil
}
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Provision_Config); i {
+ file_provisionersdk_proto_provisioner_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CancelRequest); i {
case 0:
return &v.state
case 1:
@@ -3049,8 +3016,8 @@ func file_provisionersdk_proto_provisioner_proto_init() {
return nil
}
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Provision_Plan); i {
+ file_provisionersdk_proto_provisioner_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Request); i {
case 0:
return &v.state
case 1:
@@ -3061,8 +3028,8 @@ func file_provisionersdk_proto_provisioner_proto_init() {
return nil
}
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Provision_Apply); i {
+ file_provisionersdk_proto_provisioner_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Response); i {
case 0:
return &v.state
case 1:
@@ -3073,8 +3040,8 @@ func file_provisionersdk_proto_provisioner_proto_init() {
return nil
}
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Provision_Cancel); i {
+ file_provisionersdk_proto_provisioner_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*Agent_Metadata); i {
case 0:
return &v.state
case 1:
@@ -3086,31 +3053,7 @@ func file_provisionersdk_proto_provisioner_proto_init() {
}
}
file_provisionersdk_proto_provisioner_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Provision_Request); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_provisionersdk_proto_provisioner_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Provision_Complete); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_provisionersdk_proto_provisioner_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Provision_Response); i {
+ switch v := v.(*Resource_Metadata); i {
case 0:
return &v.state
case 1:
@@ -3127,18 +3070,18 @@ func file_provisionersdk_proto_provisioner_proto_init() {
(*Agent_Token)(nil),
(*Agent_InstanceId)(nil),
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[20].OneofWrappers = []interface{}{
- (*Parse_Response_Log)(nil),
- (*Parse_Response_Complete)(nil),
- }
- file_provisionersdk_proto_provisioner_proto_msgTypes[26].OneofWrappers = []interface{}{
- (*Provision_Request_Plan)(nil),
- (*Provision_Request_Apply)(nil),
- (*Provision_Request_Cancel)(nil),
+ file_provisionersdk_proto_provisioner_proto_msgTypes[22].OneofWrappers = []interface{}{
+ (*Request_Config)(nil),
+ (*Request_Parse)(nil),
+ (*Request_Plan)(nil),
+ (*Request_Apply)(nil),
+ (*Request_Cancel)(nil),
}
- file_provisionersdk_proto_provisioner_proto_msgTypes[28].OneofWrappers = []interface{}{
- (*Provision_Response_Log)(nil),
- (*Provision_Response_Complete)(nil),
+ file_provisionersdk_proto_provisioner_proto_msgTypes[23].OneofWrappers = []interface{}{
+ (*Response_Log)(nil),
+ (*Response_Parse)(nil),
+ (*Response_Plan)(nil),
+ (*Response_Apply)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -3146,7 +3089,7 @@ func file_provisionersdk_proto_provisioner_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc,
NumEnums: 3,
- NumMessages: 29,
+ NumMessages: 27,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto
index 2b581f9620ce7..5670fbde2675d 100644
--- a/provisionersdk/proto/provisioner.proto
+++ b/provisionersdk/proto/provisioner.proto
@@ -1,6 +1,6 @@
syntax = "proto3";
-option go_package = "github.com/coder/coder/provisionersdk/proto";
+option go_package = "github.com/coder/coder/v2/provisionersdk/proto";
package provisioner;
@@ -82,8 +82,8 @@ message InstanceIdentityAuth {
}
message GitAuthProvider {
- string id = 1;
- string access_token = 2;
+ string id = 1;
+ string access_token = 2;
}
// Agent represents a running agent on the workspace.
@@ -110,15 +110,15 @@ message Agent {
string token = 9;
string instance_id = 10;
}
- int32 connection_timeout_seconds = 11;
- string troubleshooting_url = 12;
- string motd_file = 13;
- // Field 14 was bool login_before_ready = 14, now removed.
- int32 startup_script_timeout_seconds = 15;
- string shutdown_script = 16;
- int32 shutdown_script_timeout_seconds = 17;
+ int32 connection_timeout_seconds = 11;
+ string troubleshooting_url = 12;
+ string motd_file = 13;
+ // Field 14 was bool login_before_ready = 14, now removed.
+ int32 startup_script_timeout_seconds = 15;
+ string shutdown_script = 16;
+ int32 shutdown_script_timeout_seconds = 17;
repeated Metadata metadata = 18;
- string startup_script_behavior = 19;
+ string startup_script_behavior = 19;
}
enum AppSharingLevel {
@@ -168,96 +168,111 @@ message Resource {
int32 daily_cost = 8;
}
-// Parse consumes source-code from a directory to produce inputs.
-message Parse {
- message Request {
- string directory = 1;
- }
- message Complete {
- reserved 2;
-
- repeated TemplateVariable template_variables = 1;
- }
- message Response {
- oneof type {
- Log log = 1;
- Complete complete = 2;
- }
- }
-}
-
+// WorkspaceTransition is the desired outcome of a build
enum WorkspaceTransition {
START = 0;
STOP = 1;
DESTROY = 2;
}
-// Provision consumes source-code from a directory to produce resources.
-// Exactly one of Plan or Apply must be provided in a single session.
-message Provision {
- message Metadata {
- string coder_url = 1;
- WorkspaceTransition workspace_transition = 2;
- string workspace_name = 3;
- string workspace_owner = 4;
- string workspace_id = 5;
- string workspace_owner_id = 6;
- string workspace_owner_email = 7;
- string template_name = 8;
- string template_version = 9;
- string workspace_owner_oidc_access_token = 10;
- string workspace_owner_session_token = 11;
- }
+// Metadata is information about a workspace used in the execution of a build
+message Metadata {
+ string coder_url = 1;
+ WorkspaceTransition workspace_transition = 2;
+ string workspace_name = 3;
+ string workspace_owner = 4;
+ string workspace_id = 5;
+ string workspace_owner_id = 6;
+ string workspace_owner_email = 7;
+ string template_name = 8;
+ string template_version = 9;
+ string workspace_owner_oidc_access_token = 10;
+ string workspace_owner_session_token = 11;
+}
- // Config represents execution configuration shared by both Plan and
- // Apply commands.
- message Config {
- string directory = 1;
- bytes state = 2;
- Metadata metadata = 3;
+// Config represents execution configuration shared by all subsequent requests in the Session
+message Config {
+ // template_source_archive is a tar of the template source files
+ bytes template_source_archive = 1;
+ // state is the provisioner state (if any)
+ bytes state = 2;
+ string provisioner_log_level = 3;
+}
- string provisioner_log_level = 4;
- }
+// ParseRequest consumes source-code to produce inputs.
+message ParseRequest {
+}
- message Plan {
- reserved 2;
+// ParseComplete indicates a request to parse completed.
+message ParseComplete {
+ string error = 1;
+ repeated TemplateVariable template_variables = 2;
+ bytes readme = 3;
+}
- Config config = 1;
- repeated RichParameterValue rich_parameter_values = 3;
- repeated VariableValue variable_values = 4;
- repeated GitAuthProvider git_auth_providers = 5;
- }
+// PlanRequest asks the provisioner to plan what resources & parameters it will create
+message PlanRequest {
+ Metadata metadata = 1;
+ repeated RichParameterValue rich_parameter_values = 2;
+ repeated VariableValue variable_values = 3;
+ repeated GitAuthProvider git_auth_providers = 4;
+}
+
+// PlanComplete indicates a request to plan completed.
+message PlanComplete {
+ string error = 1;
+ repeated Resource resources = 2;
+ repeated RichParameter parameters = 3;
+ repeated string git_auth_providers = 4;
+}
+
+// ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response
+// in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session.
+message ApplyRequest {
+ Metadata metadata = 1;
+}
- message Apply {
+// ApplyComplete indicates a request to apply completed.
+message ApplyComplete {
+ bytes state = 1;
+ string error = 2;
+ repeated Resource resources = 3;
+ repeated RichParameter parameters = 4;
+ repeated string git_auth_providers = 5;
+}
+
+// CancelRequest requests that the previous request be canceled gracefully.
+message CancelRequest {}
+
+message Request {
+ oneof type {
Config config = 1;
- bytes plan = 2;
+ ParseRequest parse = 2;
+ PlanRequest plan = 3;
+ ApplyRequest apply = 4;
+ CancelRequest cancel = 5;
}
+}
- message Cancel {}
- message Request {
- oneof type {
- Plan plan = 1;
- Apply apply = 2;
- Cancel cancel = 3;
- }
- }
- message Complete {
- bytes state = 1;
- string error = 2;
- repeated Resource resources = 3;
- repeated RichParameter parameters = 4;
- repeated string git_auth_providers = 5;
- bytes plan = 6;
- }
- message Response {
- oneof type {
- Log log = 1;
- Complete complete = 2;
- }
+message Response {
+ oneof type {
+ Log log = 1;
+ ParseComplete parse = 2;
+ PlanComplete plan = 3;
+ ApplyComplete apply = 4;
}
}
service Provisioner {
- rpc Parse(Parse.Request) returns (stream Parse.Response);
- rpc Provision(stream Provision.Request) returns (stream Provision.Response);
+ // Session represents provisioning a single template import or workspace. The daemon always sends Config followed
+ // by one of the requests (ParseRequest, PlanRequest, ApplyRequest). The provisioner should respond with a stream
+ // of zero or more Logs, followed by the corresponding complete message (ParseComplete, PlanComplete,
+ // ApplyComplete). The daemon may then send a new request. A request to apply MUST be preceded by a request plan,
+ // and the provisioner should store the plan data on the Session after a successful plan, so that the daemon may
+ // request an apply. If the daemon closes the Session without an apply, the plan data may be safely discarded.
+ //
+ // The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous ParseRequest,
+ // PlanRequest, or ApplyRequest. The provisioner MUST reply with a complete message corresponding to the request
+ // that was canceled. If the provisioner has already completed the request, it may ignore the CancelRequest.
+ rpc Session(stream Request) returns (stream Response);
}
diff --git a/provisionersdk/proto/provisioner_drpc.pb.go b/provisionersdk/proto/provisioner_drpc.pb.go
index d8b40060cd376..de310e779dcaa 100644
--- a/provisionersdk/proto/provisioner_drpc.pb.go
+++ b/provisionersdk/proto/provisioner_drpc.pb.go
@@ -38,8 +38,7 @@ func (drpcEncoding_File_provisionersdk_proto_provisioner_proto) JSONUnmarshal(bu
type DRPCProvisionerClient interface {
DRPCConn() drpc.Conn
- Parse(ctx context.Context, in *Parse_Request) (DRPCProvisioner_ParseClient, error)
- Provision(ctx context.Context) (DRPCProvisioner_ProvisionClient, error)
+ Session(ctx context.Context) (DRPCProvisioner_SessionClient, error)
}
type drpcProvisionerClient struct {
@@ -52,123 +51,69 @@ func NewDRPCProvisionerClient(cc drpc.Conn) DRPCProvisionerClient {
func (c *drpcProvisionerClient) DRPCConn() drpc.Conn { return c.cc }
-func (c *drpcProvisionerClient) Parse(ctx context.Context, in *Parse_Request) (DRPCProvisioner_ParseClient, error) {
- stream, err := c.cc.NewStream(ctx, "/provisioner.Provisioner/Parse", drpcEncoding_File_provisionersdk_proto_provisioner_proto{})
+func (c *drpcProvisionerClient) Session(ctx context.Context) (DRPCProvisioner_SessionClient, error) {
+ stream, err := c.cc.NewStream(ctx, "/provisioner.Provisioner/Session", drpcEncoding_File_provisionersdk_proto_provisioner_proto{})
if err != nil {
return nil, err
}
- x := &drpcProvisioner_ParseClient{stream}
- if err := x.MsgSend(in, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}); err != nil {
- return nil, err
- }
- if err := x.CloseSend(); err != nil {
- return nil, err
- }
+ x := &drpcProvisioner_SessionClient{stream}
return x, nil
}
-type DRPCProvisioner_ParseClient interface {
+type DRPCProvisioner_SessionClient interface {
drpc.Stream
- Recv() (*Parse_Response, error)
+ Send(*Request) error
+ Recv() (*Response, error)
}
-type drpcProvisioner_ParseClient struct {
+type drpcProvisioner_SessionClient struct {
drpc.Stream
}
-func (x *drpcProvisioner_ParseClient) GetStream() drpc.Stream {
+func (x *drpcProvisioner_SessionClient) GetStream() drpc.Stream {
return x.Stream
}
-func (x *drpcProvisioner_ParseClient) Recv() (*Parse_Response, error) {
- m := new(Parse_Response)
- if err := x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}); err != nil {
- return nil, err
- }
- return m, nil
-}
-
-func (x *drpcProvisioner_ParseClient) RecvMsg(m *Parse_Response) error {
- return x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{})
-}
-
-func (c *drpcProvisionerClient) Provision(ctx context.Context) (DRPCProvisioner_ProvisionClient, error) {
- stream, err := c.cc.NewStream(ctx, "/provisioner.Provisioner/Provision", drpcEncoding_File_provisionersdk_proto_provisioner_proto{})
- if err != nil {
- return nil, err
- }
- x := &drpcProvisioner_ProvisionClient{stream}
- return x, nil
-}
-
-type DRPCProvisioner_ProvisionClient interface {
- drpc.Stream
- Send(*Provision_Request) error
- Recv() (*Provision_Response, error)
-}
-
-type drpcProvisioner_ProvisionClient struct {
- drpc.Stream
-}
-
-func (x *drpcProvisioner_ProvisionClient) GetStream() drpc.Stream {
- return x.Stream
-}
-
-func (x *drpcProvisioner_ProvisionClient) Send(m *Provision_Request) error {
+func (x *drpcProvisioner_SessionClient) Send(m *Request) error {
return x.MsgSend(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{})
}
-func (x *drpcProvisioner_ProvisionClient) Recv() (*Provision_Response, error) {
- m := new(Provision_Response)
+func (x *drpcProvisioner_SessionClient) Recv() (*Response, error) {
+ m := new(Response)
if err := x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}); err != nil {
return nil, err
}
return m, nil
}
-func (x *drpcProvisioner_ProvisionClient) RecvMsg(m *Provision_Response) error {
+func (x *drpcProvisioner_SessionClient) RecvMsg(m *Response) error {
return x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{})
}
type DRPCProvisionerServer interface {
- Parse(*Parse_Request, DRPCProvisioner_ParseStream) error
- Provision(DRPCProvisioner_ProvisionStream) error
+ Session(DRPCProvisioner_SessionStream) error
}
type DRPCProvisionerUnimplementedServer struct{}
-func (s *DRPCProvisionerUnimplementedServer) Parse(*Parse_Request, DRPCProvisioner_ParseStream) error {
- return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
-}
-
-func (s *DRPCProvisionerUnimplementedServer) Provision(DRPCProvisioner_ProvisionStream) error {
+func (s *DRPCProvisionerUnimplementedServer) Session(DRPCProvisioner_SessionStream) error {
return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
type DRPCProvisionerDescription struct{}
-func (DRPCProvisionerDescription) NumMethods() int { return 2 }
+func (DRPCProvisionerDescription) NumMethods() int { return 1 }
func (DRPCProvisionerDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
switch n {
case 0:
- return "/provisioner.Provisioner/Parse", drpcEncoding_File_provisionersdk_proto_provisioner_proto{},
+ return "/provisioner.Provisioner/Session", drpcEncoding_File_provisionersdk_proto_provisioner_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return nil, srv.(DRPCProvisionerServer).
- Parse(
- in1.(*Parse_Request),
- &drpcProvisioner_ParseStream{in2.(drpc.Stream)},
+ Session(
+ &drpcProvisioner_SessionStream{in1.(drpc.Stream)},
)
- }, DRPCProvisionerServer.Parse, true
- case 1:
- return "/provisioner.Provisioner/Provision", drpcEncoding_File_provisionersdk_proto_provisioner_proto{},
- func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
- return nil, srv.(DRPCProvisionerServer).
- Provision(
- &drpcProvisioner_ProvisionStream{in1.(drpc.Stream)},
- )
- }, DRPCProvisionerServer.Provision, true
+ }, DRPCProvisionerServer.Session, true
default:
return "", nil, nil, nil, false
}
@@ -178,41 +123,28 @@ func DRPCRegisterProvisioner(mux drpc.Mux, impl DRPCProvisionerServer) error {
return mux.Register(impl, DRPCProvisionerDescription{})
}
-type DRPCProvisioner_ParseStream interface {
- drpc.Stream
- Send(*Parse_Response) error
-}
-
-type drpcProvisioner_ParseStream struct {
- drpc.Stream
-}
-
-func (x *drpcProvisioner_ParseStream) Send(m *Parse_Response) error {
- return x.MsgSend(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{})
-}
-
-type DRPCProvisioner_ProvisionStream interface {
+type DRPCProvisioner_SessionStream interface {
drpc.Stream
- Send(*Provision_Response) error
- Recv() (*Provision_Request, error)
+ Send(*Response) error
+ Recv() (*Request, error)
}
-type drpcProvisioner_ProvisionStream struct {
+type drpcProvisioner_SessionStream struct {
drpc.Stream
}
-func (x *drpcProvisioner_ProvisionStream) Send(m *Provision_Response) error {
+func (x *drpcProvisioner_SessionStream) Send(m *Response) error {
return x.MsgSend(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{})
}
-func (x *drpcProvisioner_ProvisionStream) Recv() (*Provision_Request, error) {
- m := new(Provision_Request)
+func (x *drpcProvisioner_SessionStream) Recv() (*Request, error) {
+ m := new(Request)
if err := x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{}); err != nil {
return nil, err
}
return m, nil
}
-func (x *drpcProvisioner_ProvisionStream) RecvMsg(m *Provision_Request) error {
+func (x *drpcProvisioner_SessionStream) RecvMsg(m *Request) error {
return x.MsgRecv(m, drpcEncoding_File_provisionersdk_proto_provisioner_proto{})
}
diff --git a/provisionersdk/serve.go b/provisionersdk/serve.go
index cb1647a54d1ac..924c7ad013982 100644
--- a/provisionersdk/serve.go
+++ b/provisionersdk/serve.go
@@ -13,18 +13,28 @@ import (
"storj.io/drpc/drpcmux"
"storj.io/drpc/drpcserver"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/provisionersdk/proto"
+ "cdr.dev/slog"
+
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
// ServeOptions are configurations to serve a provisioner.
type ServeOptions struct {
// Conn specifies a custom transport to serve the dRPC connection.
- Listener net.Listener
+ Listener net.Listener
+ Logger slog.Logger
+ WorkDirectory string
+}
+
+type Server interface {
+ Parse(s *Session, r *proto.ParseRequest, canceledOrComplete <-chan struct{}) *proto.ParseComplete
+ Plan(s *Session, r *proto.PlanRequest, canceledOrComplete <-chan struct{}) *proto.PlanComplete
+ Apply(s *Session, r *proto.ApplyRequest, canceledOrComplete <-chan struct{}) *proto.ApplyComplete
}
// Serve starts a dRPC connection for the provisioner and transport provided.
-func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *ServeOptions) error {
+func Serve(ctx context.Context, server Server, options *ServeOptions) error {
if options == nil {
options = &ServeOptions{}
}
@@ -45,11 +55,22 @@ func Serve(ctx context.Context, server proto.DRPCProvisionerServer, options *Ser
}()
options.Listener = stdio
}
+ if options.WorkDirectory == "" {
+ var err error
+ options.WorkDirectory, err = os.MkdirTemp("", "coderprovisioner")
+ if err != nil {
+ return xerrors.Errorf("failed to init temp work dir: %w", err)
+ }
+ }
// dRPC is a drop-in replacement for gRPC with less generated code, and faster transports.
// See: https://www.storj.io/blog/introducing-drpc-our-replacement-for-grpc
mux := drpcmux.New()
- err := proto.DRPCRegisterProvisioner(mux, server)
+ ps := &protoServer{
+ server: server,
+ opts: *options,
+ }
+ err := proto.DRPCRegisterProvisioner(mux, ps)
if err != nil {
return xerrors.Errorf("register provisioner: %w", err)
}
diff --git a/provisionersdk/serve_test.go b/provisionersdk/serve_test.go
index 28c7f0dfe5ef5..baa5d2ba62b28 100644
--- a/provisionersdk/serve_test.go
+++ b/provisionersdk/serve_test.go
@@ -7,10 +7,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
- "storj.io/drpc/drpcerr"
- "github.com/coder/coder/provisionersdk"
- "github.com/coder/coder/provisionersdk/proto"
+ "github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/coder/v2/provisionersdk/proto"
)
func TestMain(m *testing.M) {
@@ -28,17 +27,37 @@ func TestProvisionerSDK(t *testing.T) {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
go func() {
- err := provisionersdk.Serve(ctx, &proto.DRPCProvisionerUnimplementedServer{}, &provisionersdk.ServeOptions{
- Listener: server,
+ err := provisionersdk.Serve(ctx, unimplementedServer{}, &provisionersdk.ServeOptions{
+ Listener: server,
+ WorkDirectory: t.TempDir(),
})
assert.NoError(t, err)
}()
api := proto.NewDRPCProvisionerClient(client)
- stream, err := api.Parse(context.Background(), &proto.Parse_Request{})
+ s, err := api.Session(ctx)
require.NoError(t, err)
- _, err = stream.Recv()
- require.Equal(t, drpcerr.Unimplemented, int(drpcerr.Code(err)))
+ err = s.Send(&proto.Request{Type: &proto.Request_Config{Config: &proto.Config{}}})
+ require.NoError(t, err)
+
+ err = s.Send(&proto.Request{Type: &proto.Request_Parse{Parse: &proto.ParseRequest{}}})
+ require.NoError(t, err)
+ msg, err := s.Recv()
+ require.NoError(t, err)
+ require.Equal(t, "unimplemented", msg.GetParse().GetError())
+
+ err = s.Send(&proto.Request{Type: &proto.Request_Plan{Plan: &proto.PlanRequest{}}})
+ require.NoError(t, err)
+ msg, err = s.Recv()
+ require.NoError(t, err)
+ // Plan has no error so that we're allowed to run Apply
+ require.Equal(t, "", msg.GetPlan().GetError())
+
+ err = s.Send(&proto.Request{Type: &proto.Request_Apply{Apply: &proto.ApplyRequest{}}})
+ require.NoError(t, err)
+ msg, err = s.Recv()
+ require.NoError(t, err)
+ require.Equal(t, "unimplemented", msg.GetApply().GetError())
})
t.Run("ServeClosedPipe", func(t *testing.T) {
@@ -47,9 +66,24 @@ func TestProvisionerSDK(t *testing.T) {
_ = client.Close()
_ = server.Close()
- err := provisionersdk.Serve(context.Background(), &proto.DRPCProvisionerUnimplementedServer{}, &provisionersdk.ServeOptions{
- Listener: server,
+ err := provisionersdk.Serve(context.Background(), unimplementedServer{}, &provisionersdk.ServeOptions{
+ Listener: server,
+ WorkDirectory: t.TempDir(),
})
require.NoError(t, err)
})
}
+
+type unimplementedServer struct{}
+
+func (unimplementedServer) Parse(_ *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete {
+ return &proto.ParseComplete{Error: "unimplemented"}
+}
+
+func (unimplementedServer) Plan(_ *provisionersdk.Session, _ *proto.PlanRequest, _ <-chan struct{}) *proto.PlanComplete {
+ return &proto.PlanComplete{}
+}
+
+func (unimplementedServer) Apply(_ *provisionersdk.Session, _ *proto.ApplyRequest, _ <-chan struct{}) *proto.ApplyComplete {
+ return &proto.ApplyComplete{Error: "unimplemented"}
+}
diff --git a/provisionersdk/session.go b/provisionersdk/session.go
new file mode 100644
index 0000000000000..dfcd981ce77f5
--- /dev/null
+++ b/provisionersdk/session.go
@@ -0,0 +1,318 @@
+package provisionersdk
+
+import (
+ "archive/tar"
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+ "golang.org/x/xerrors"
+
+ "cdr.dev/slog"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+)
+
+// ReadmeFile is the location we look for to extract documentation from template
+// versions.
+const ReadmeFile = "README.md"
+
+// protoServer is a wrapper that translates the dRPC protocol into a Session with method calls into the Server.
+type protoServer struct {
+ server Server
+ opts ServeOptions
+}
+
+func (p *protoServer) Session(stream proto.DRPCProvisioner_SessionStream) error {
+ sessID := uuid.New().String()
+ s := &Session{
+ Logger: p.opts.Logger.With(slog.F("session_id", sessID)),
+ stream: stream,
+ server: p.server,
+ }
+ sessDir := fmt.Sprintf("Session%s", sessID)
+ s.WorkDirectory = filepath.Join(p.opts.WorkDirectory, sessDir)
+ err := os.MkdirAll(s.WorkDirectory, 0o700)
+ if err != nil {
+ return xerrors.Errorf("create work directory %q: %w", s.WorkDirectory, err)
+ }
+ defer func() {
+ var err error
+ // Cleanup the work directory after execution.
+ for attempt := 0; attempt < 5; attempt++ {
+ err = os.RemoveAll(s.WorkDirectory)
+ if err != nil {
+ // On Windows, open files cannot be removed.
+ // When the provisioner daemon is shutting down,
+ // it may take a few milliseconds for processes to exit.
+ // See: https://github.com/golang/go/issues/50510
+ s.Logger.Debug(s.Context(), "failed to clean work directory; trying again", slog.Error(err))
+ time.Sleep(250 * time.Millisecond)
+ continue
+ }
+ s.Logger.Debug(s.Context(), "cleaned up work directory")
+ return
+ }
+ s.Logger.Error(s.Context(), "failed to clean up work directory after multiple attempts",
+ slog.F("path", s.WorkDirectory), slog.Error(err))
+ }()
+ req, err := stream.Recv()
+ if err != nil {
+ return xerrors.Errorf("receive config: %w", err)
+ }
+ config := req.GetConfig()
+ if config == nil {
+ return xerrors.New("first request must be Config")
+ }
+ s.Config = config
+ if s.Config.ProvisionerLogLevel != "" {
+ s.logLevel = proto.LogLevel_value[strings.ToUpper(s.Config.ProvisionerLogLevel)]
+ }
+
+ err = s.extractArchive()
+ if err != nil {
+ return xerrors.Errorf("extract archive: %w", err)
+ }
+ return s.handleRequests()
+}
+
+func (s *Session) requestReader(done <-chan struct{}) <-chan *proto.Request {
+ ch := make(chan *proto.Request)
+ go func() {
+ defer close(ch)
+ for {
+ req, err := s.stream.Recv()
+ if err != nil {
+ s.Logger.Info(s.Context(), "recv done on Session", slog.Error(err))
+ return
+ }
+ select {
+ case ch <- req:
+ continue
+ case <-done:
+ return
+ }
+ }
+ }()
+ return ch
+}
+
+func (s *Session) handleRequests() error {
+ done := make(chan struct{})
+ defer close(done)
+ requests := s.requestReader(done)
+ planned := false
+ for req := range requests {
+ if req.GetCancel() != nil {
+ s.Logger.Warn(s.Context(), "ignoring cancel before request or after complete")
+ continue
+ }
+ resp := &proto.Response{}
+ if parse := req.GetParse(); parse != nil {
+ r := &request[*proto.ParseRequest, *proto.ParseComplete]{
+ req: parse,
+ session: s,
+ serverFn: s.server.Parse,
+ cancels: requests,
+ }
+ complete, err := r.do()
+ if err != nil {
+ return err
+ }
+ // Handle README centrally, so that individual provisioners don't need to mess with it.
+ readme, err := os.ReadFile(filepath.Join(s.WorkDirectory, ReadmeFile))
+ if err == nil {
+ complete.Readme = readme
+ } else {
+ s.Logger.Debug(s.Context(), "failed to parse readme (missing ok)", slog.Error(err))
+ }
+ resp.Type = &proto.Response_Parse{Parse: complete}
+ }
+ if plan := req.GetPlan(); plan != nil {
+ r := &request[*proto.PlanRequest, *proto.PlanComplete]{
+ req: plan,
+ session: s,
+ serverFn: s.server.Plan,
+ cancels: requests,
+ }
+ complete, err := r.do()
+ if err != nil {
+ return err
+ }
+ resp.Type = &proto.Response_Plan{Plan: complete}
+ if complete.Error == "" {
+ planned = true
+ }
+ }
+ if apply := req.GetApply(); apply != nil {
+ if !planned {
+ return xerrors.New("cannot apply before successful plan")
+ }
+ r := &request[*proto.ApplyRequest, *proto.ApplyComplete]{
+ req: apply,
+ session: s,
+ serverFn: s.server.Apply,
+ cancels: requests,
+ }
+ complete, err := r.do()
+ if err != nil {
+ return err
+ }
+ resp.Type = &proto.Response_Apply{Apply: complete}
+ }
+ err := s.stream.Send(resp)
+ if err != nil {
+ return xerrors.Errorf("send response: %w", err)
+ }
+ }
+ return nil
+}
+
+type Session struct {
+ Logger slog.Logger
+ WorkDirectory string
+ Config *proto.Config
+
+ server Server
+ stream proto.DRPCProvisioner_SessionStream
+ logLevel int32
+}
+
+func (s *Session) Context() context.Context {
+ return s.stream.Context()
+}
+
+func (s *Session) extractArchive() error {
+ ctx := s.Context()
+
+ s.Logger.Info(ctx, "unpacking template source archive",
+ slog.F("size_bytes", len(s.Config.TemplateSourceArchive)),
+ )
+
+ reader := tar.NewReader(bytes.NewBuffer(s.Config.TemplateSourceArchive))
+ // for safety, nil out the reference on Config, since the reader now owns it.
+ s.Config.TemplateSourceArchive = nil
+ for {
+ header, err := reader.Next()
+ if err != nil {
+ if xerrors.Is(err, io.EOF) {
+ break
+ }
+ return xerrors.Errorf("read template source archive: %w", err)
+ }
+ // Security: don't untar absolute or relative paths, as this can allow a malicious tar to overwrite
+ // files outside the workdir.
+ if !filepath.IsLocal(header.Name) {
+ return xerrors.Errorf("refusing to extract to non-local path")
+ }
+ // nolint: gosec
+ headerPath := filepath.Join(s.WorkDirectory, header.Name)
+ if !strings.HasPrefix(headerPath, filepath.Clean(s.WorkDirectory)) {
+ return xerrors.New("tar attempts to target relative upper directory")
+ }
+ mode := header.FileInfo().Mode()
+ if mode == 0 {
+ mode = 0o600
+ }
+ switch header.Typeflag {
+ case tar.TypeDir:
+ err = os.MkdirAll(headerPath, mode)
+ if err != nil {
+ return xerrors.Errorf("mkdir %q: %w", headerPath, err)
+ }
+ s.Logger.Debug(context.Background(), "extracted directory",
+ slog.F("path", headerPath),
+ slog.F("mode", fmt.Sprintf("%O", mode)))
+ case tar.TypeReg:
+ file, err := os.OpenFile(headerPath, os.O_CREATE|os.O_RDWR, mode)
+ if err != nil {
+ return xerrors.Errorf("create file %q (mode %s): %w", headerPath, mode, err)
+ }
+ // Max file size of 10MiB.
+ size, err := io.CopyN(file, reader, 10<<20)
+ if xerrors.Is(err, io.EOF) {
+ err = nil
+ }
+ if err != nil {
+ _ = file.Close()
+ return xerrors.Errorf("copy file %q: %w", headerPath, err)
+ }
+ err = file.Close()
+ if err != nil {
+ return xerrors.Errorf("close file %q: %s", headerPath, err)
+ }
+ s.Logger.Debug(context.Background(), "extracted file",
+ slog.F("size_bytes", size),
+ slog.F("path", headerPath),
+ slog.F("mode", mode),
+ )
+ }
+ }
+ return nil
+}
+
+func (s *Session) ProvisionLog(level proto.LogLevel, output string) {
+ if int32(level) < s.logLevel {
+ return
+ }
+
+ err := s.stream.Send(&proto.Response{Type: &proto.Response_Log{Log: &proto.Log{
+ Level: level,
+ Output: output,
+ }}})
+ if err != nil {
+ s.Logger.Error(s.Context(), "failed to transmit log",
+ slog.F("level", level), slog.F("output", output))
+ }
+}
+
+type pRequest interface {
+ *proto.ParseRequest | *proto.PlanRequest | *proto.ApplyRequest
+}
+
+type pComplete interface {
+ *proto.ParseComplete | *proto.PlanComplete | *proto.ApplyComplete
+}
+
+// request processes a single request call to the Server and returns its complete result, while also processing cancel
+// requests from the daemon. Provisioner implementations read from canceledOrComplete to be asynchronously informed
+// of cancel.
+type request[R pRequest, C pComplete] struct {
+ req R
+ session *Session
+ cancels <-chan *proto.Request
+ serverFn func(*Session, R, <-chan struct{}) C
+}
+
+func (r *request[R, C]) do() (C, error) {
+ canceledOrComplete := make(chan struct{})
+ result := make(chan C)
+ go func() {
+ c := r.serverFn(r.session, r.req, canceledOrComplete)
+ result <- c
+ }()
+ select {
+ case req := <-r.cancels:
+ close(canceledOrComplete)
+ // wait for server to complete the request, even though we have canceled,
+ // so that we can't start a new request, and so that if the job was close
+ // to completion and the cancel was ignored, we return to complete.
+ c := <-result
+ // verify we got a cancel instead of another request or closed channel --- which is an error!
+ if req.GetCancel() != nil {
+ return c, nil
+ }
+ if req == nil {
+ return c, xerrors.New("got nil while old request still processing")
+ }
+ return c, xerrors.Errorf("got new request %T while old request still processing", req.Type)
+ case c := <-result:
+ close(canceledOrComplete)
+ return c, nil
+ }
+}
diff --git a/provisionersdk/transport.go b/provisionersdk/transport.go
index aa54efb0f36ba..f5df895d64eaa 100644
--- a/provisionersdk/transport.go
+++ b/provisionersdk/transport.go
@@ -10,7 +10,7 @@ import (
"storj.io/drpc"
"storj.io/drpc/drpcconn"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/tracing"
)
const (
@@ -19,7 +19,7 @@ const (
MaxMessageSize = 4 << 20
)
-// MultiplexedConn returns a multiplexed dRPC connection from a yamux session.
+// MultiplexedConn returns a multiplexed dRPC connection from a yamux Session.
func MultiplexedConn(session *yamux.Session) drpc.Conn {
return &multiplexedDRPC{session}
}
diff --git a/pty/ptytest/ptytest.go b/pty/ptytest/ptytest.go
index a319fbdc9c168..544adc242990e 100644
--- a/pty/ptytest/ptytest.go
+++ b/pty/ptytest/ptytest.go
@@ -18,9 +18,9 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/pty"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/pty"
+ "github.com/coder/coder/v2/testutil"
)
func New(t *testing.T, opts ...pty.Option) *PTY {
diff --git a/pty/ptytest/ptytest_test.go b/pty/ptytest/ptytest_test.go
index 1935c7b5162ff..5a2f11ba728d7 100644
--- a/pty/ptytest/ptytest_test.go
+++ b/pty/ptytest/ptytest_test.go
@@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/pty/ptytest"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
)
func TestPtytest(t *testing.T) {
diff --git a/pty/start_other_test.go b/pty/start_other_test.go
index e7a2a3d69e327..57fc9e4847832 100644
--- a/pty/start_other_test.go
+++ b/pty/start_other_test.go
@@ -12,8 +12,8 @@ import (
"go.uber.org/goleak"
"golang.org/x/xerrors"
- "github.com/coder/coder/pty"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/pty"
+ "github.com/coder/coder/v2/pty/ptytest"
)
func TestMain(m *testing.M) {
diff --git a/pty/start_test.go b/pty/start_test.go
index 5f273428d2ea6..4c077650e3814 100644
--- a/pty/start_test.go
+++ b/pty/start_test.go
@@ -5,16 +5,14 @@ import (
"context"
"fmt"
"io"
- "strings"
"testing"
"time"
- "github.com/hinshun/vt10x"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/pty"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/pty"
+ "github.com/coder/coder/v2/testutil"
)
// Test_Start_copy tests that we can use io.Copy() on command output
@@ -73,7 +71,7 @@ func Test_Start_truncation(t *testing.T) {
n := 1
for n <= countEnd {
want := fmt.Sprintf("%d", n)
- err := readUntil(ctx, t, want, pc.OutputReader())
+ err := testutil.ReadUntilString(ctx, t, want, pc.OutputReader())
assert.NoError(t, err, "want: %s", want)
if err != nil {
return
@@ -141,36 +139,3 @@ func Test_Start_cancel_context(t *testing.T) {
t.Error("cmd.Wait() timed out")
}
}
-
-// readUntil reads one byte at a time until we either see the string we want, or the context expires
-func readUntil(ctx context.Context, t *testing.T, want string, r io.Reader) error {
- // output can contain virtual terminal sequences, so we need to parse these
- // to correctly interpret getting what we want.
- term := vt10x.New(vt10x.WithSize(80, 80))
- readErrs := make(chan error, 1)
- for {
- b := make([]byte, 1)
- go func() {
- _, err := r.Read(b)
- readErrs <- err
- }()
- select {
- case err := <-readErrs:
- if err != nil {
- t.Logf("err: %v\ngot: %v", err, term)
- return err
- }
- term.Write(b)
- case <-ctx.Done():
- return ctx.Err()
- }
- got := term.String()
- lines := strings.Split(got, "\n")
- for _, line := range lines {
- if strings.TrimSpace(line) == want {
- t.Logf("want: %v\n got:%v", want, line)
- return nil
- }
- }
- }
-}
diff --git a/pty/start_windows_test.go b/pty/start_windows_test.go
index b0f862ea15caf..280639cafe3fc 100644
--- a/pty/start_windows_test.go
+++ b/pty/start_windows_test.go
@@ -8,8 +8,8 @@ import (
"os/exec"
"testing"
- "github.com/coder/coder/pty"
- "github.com/coder/coder/pty/ptytest"
+ "github.com/coder/coder/v2/pty"
+ "github.com/coder/coder/v2/pty/ptytest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
diff --git a/scaletest/agentconn/config.go b/scaletest/agentconn/config.go
index 1d5de5cf7a6e0..be40626248be6 100644
--- a/scaletest/agentconn/config.go
+++ b/scaletest/agentconn/config.go
@@ -6,7 +6,7 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
type ConnectionMode string
diff --git a/scaletest/agentconn/config_test.go b/scaletest/agentconn/config_test.go
index 29ccfbf9739c2..5f5cdf7c53da7 100644
--- a/scaletest/agentconn/config_test.go
+++ b/scaletest/agentconn/config_test.go
@@ -7,8 +7,8 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/scaletest/agentconn"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/scaletest/agentconn"
)
func Test_Config(t *testing.T) {
diff --git a/scaletest/agentconn/run.go b/scaletest/agentconn/run.go
index 6593e7c79b52c..cc942448ff6d4 100644
--- a/scaletest/agentconn/run.go
+++ b/scaletest/agentconn/run.go
@@ -15,10 +15,10 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/scaletest/harness"
- "github.com/coder/coder/scaletest/loadtestutil"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/loadtestutil"
)
const defaultRequestTimeout = 5 * time.Second
diff --git a/scaletest/agentconn/run_test.go b/scaletest/agentconn/run_test.go
index 537244af76dcc..4d3ffb8d0da2d 100644
--- a/scaletest/agentconn/run_test.go
+++ b/scaletest/agentconn/run_test.go
@@ -14,15 +14,15 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/scaletest/agentconn"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/scaletest/agentconn"
+ "github.com/coder/coder/v2/testutil"
)
func Test_Runner(t *testing.T) {
@@ -231,10 +231,10 @@ func setupRunnerTest(t *testing.T) (client *codersdk.Client, agentID uuid.UUID)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
diff --git a/scaletest/createworkspaces/config.go b/scaletest/createworkspaces/config.go
index e1e92fb6e86c0..579d9b5288418 100644
--- a/scaletest/createworkspaces/config.go
+++ b/scaletest/createworkspaces/config.go
@@ -4,10 +4,10 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/scaletest/agentconn"
- "github.com/coder/coder/scaletest/reconnectingpty"
- "github.com/coder/coder/scaletest/workspacebuild"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/scaletest/agentconn"
+ "github.com/coder/coder/v2/scaletest/reconnectingpty"
+ "github.com/coder/coder/v2/scaletest/workspacebuild"
)
type UserConfig struct {
diff --git a/scaletest/createworkspaces/config_test.go b/scaletest/createworkspaces/config_test.go
index 298c5efab79d4..6a3d9e8104624 100644
--- a/scaletest/createworkspaces/config_test.go
+++ b/scaletest/createworkspaces/config_test.go
@@ -7,12 +7,12 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/scaletest/agentconn"
- "github.com/coder/coder/scaletest/createworkspaces"
- "github.com/coder/coder/scaletest/reconnectingpty"
- "github.com/coder/coder/scaletest/workspacebuild"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/scaletest/agentconn"
+ "github.com/coder/coder/v2/scaletest/createworkspaces"
+ "github.com/coder/coder/v2/scaletest/reconnectingpty"
+ "github.com/coder/coder/v2/scaletest/workspacebuild"
)
func Test_UserConfig(t *testing.T) {
diff --git a/scaletest/createworkspaces/run.go b/scaletest/createworkspaces/run.go
index dd5465041062f..d1c1713e3d1c3 100644
--- a/scaletest/createworkspaces/run.go
+++ b/scaletest/createworkspaces/run.go
@@ -12,14 +12,14 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/scaletest/agentconn"
- "github.com/coder/coder/scaletest/harness"
- "github.com/coder/coder/scaletest/loadtestutil"
- "github.com/coder/coder/scaletest/reconnectingpty"
- "github.com/coder/coder/scaletest/workspacebuild"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/scaletest/agentconn"
+ "github.com/coder/coder/v2/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/loadtestutil"
+ "github.com/coder/coder/v2/scaletest/reconnectingpty"
+ "github.com/coder/coder/v2/scaletest/workspacebuild"
)
type Runner struct {
diff --git a/scaletest/createworkspaces/run_test.go b/scaletest/createworkspaces/run_test.go
index ac9812dccf3ce..d5e96e22fcc83 100644
--- a/scaletest/createworkspaces/run_test.go
+++ b/scaletest/createworkspaces/run_test.go
@@ -13,18 +13,19 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/scaletest/agentconn"
- "github.com/coder/coder/scaletest/createworkspaces"
- "github.com/coder/coder/scaletest/reconnectingpty"
- "github.com/coder/coder/scaletest/workspacebuild"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/scaletest/agentconn"
+ "github.com/coder/coder/v2/scaletest/createworkspaces"
+ "github.com/coder/coder/v2/scaletest/reconnectingpty"
+ "github.com/coder/coder/v2/scaletest/workspacebuild"
+ "github.com/coder/coder/v2/testutil"
)
func Test_Runner(t *testing.T) {
@@ -47,10 +48,10 @@ func Test_Runner(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{
{
- Type: &proto.Provision_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "hello from logs",
@@ -58,8 +59,8 @@ func Test_Runner(t *testing.T) {
},
},
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Name: "example",
@@ -156,6 +157,124 @@ func Test_Runner(t *testing.T) {
require.Len(t, workspaces.Workspaces, 0)
})
+ t.Run("CleanupPendingBuild", func(t *testing.T) {
+ t.Parallel()
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ // need to include our own logger because the provisioner (rightly) drops error logs when we shut down the
+ // test with a build in progress.
+ logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
+ client := coderdtest.New(t, &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ Logger: &logger,
+ })
+ user := coderdtest.CreateFirstUser(t, client)
+
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
+ Parse: echo.ParseComplete,
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{
+ {
+ Type: &proto.Response_Log{Log: &proto.Log{}},
+ },
+ },
+ })
+
+ version = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
+ request.AllowUserCancelWorkspaceJobs = ptr.Ref(true)
+ })
+
+ const (
+ username = "scaletest-user"
+ email = "scaletest@test.coder.com"
+ )
+ runner := createworkspaces.NewRunner(client, createworkspaces.Config{
+ User: createworkspaces.UserConfig{
+ OrganizationID: user.OrganizationID,
+ Username: username,
+ Email: email,
+ },
+ Workspace: workspacebuild.Config{
+ OrganizationID: user.OrganizationID,
+ Request: codersdk.CreateWorkspaceRequest{
+ TemplateID: template.ID,
+ },
+ },
+ })
+
+ cancelCtx, cancelFunc := context.WithCancel(ctx)
+ done := make(chan struct{})
+ logs := bytes.NewBuffer(nil)
+ go func() {
+ err := runner.Run(cancelCtx, "1", logs)
+ logsStr := logs.String()
+ t.Log("Runner logs:\n\n" + logsStr)
+ require.ErrorIs(t, err, context.Canceled)
+ close(done)
+ }()
+
+ require.Eventually(t, func() bool {
+ workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
+ if err != nil {
+ return false
+ }
+
+ return len(workspaces.Workspaces) > 0
+ }, testutil.WaitShort, testutil.IntervalFast)
+
+ cancelFunc()
+ <-done
+
+ // When we run the cleanup, it should be canceled
+ cancelCtx, cancelFunc = context.WithCancel(ctx)
+ done = make(chan struct{})
+ go func() {
+ // This will return an error as the "delete" operation will never complete.
+ _ = runner.Cleanup(cancelCtx, "1")
+ close(done)
+ }()
+
+ // Ensure the job has been marked as deleted
+ require.Eventually(t, func() bool {
+ workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
+ if err != nil {
+ return false
+ }
+
+ if len(workspaces.Workspaces) == 0 {
+ return false
+ }
+
+ // There should be two builds
+ builds, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{
+ WorkspaceID: workspaces.Workspaces[0].ID,
+ })
+ if err != nil {
+ return false
+ }
+ for i, build := range builds {
+ t.Logf("checking build #%d: %s | %s", i, build.Transition, build.Job.Status)
+ // One of the builds should be for creating the workspace,
+ if build.Transition != codersdk.WorkspaceTransitionStart {
+ continue
+ }
+
+ // And it should be either failed (Echo returns an error when job is canceled), canceling, or canceled.
+ if build.Job.Status == codersdk.ProvisionerJobFailed ||
+ build.Job.Status == codersdk.ProvisionerJobCanceling ||
+ build.Job.Status == codersdk.ProvisionerJobCanceled {
+ return true
+ }
+ }
+ return false
+ }, testutil.WaitShort, testutil.IntervalFast)
+ cancelFunc()
+ <-done
+ })
+
t.Run("NoCleanup", func(t *testing.T) {
t.Parallel()
@@ -170,10 +289,10 @@ func Test_Runner(t *testing.T) {
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{
{
- Type: &proto.Provision_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "hello from logs",
@@ -181,8 +300,8 @@ func Test_Runner(t *testing.T) {
},
},
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Name: "example",
@@ -295,11 +414,11 @@ func Test_Runner(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Error: "test error",
},
},
diff --git a/scaletest/dashboard/cache.go b/scaletest/dashboard/cache.go
index 2ac31ff22a525..3aa25cc46530d 100644
--- a/scaletest/dashboard/cache.go
+++ b/scaletest/dashboard/cache.go
@@ -5,7 +5,7 @@ import (
"math/rand"
"sync"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
type cache struct {
diff --git a/scaletest/dashboard/rolltable.go b/scaletest/dashboard/rolltable.go
index e237cf6983878..725c53913187b 100644
--- a/scaletest/dashboard/rolltable.go
+++ b/scaletest/dashboard/rolltable.go
@@ -5,7 +5,7 @@ import (
"github.com/google/uuid"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
// DefaultActions is a table of actions to perform.
diff --git a/scaletest/dashboard/run.go b/scaletest/dashboard/run.go
index 64c904177565e..a6db21086d658 100644
--- a/scaletest/dashboard/run.go
+++ b/scaletest/dashboard/run.go
@@ -10,8 +10,8 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/scaletest/harness"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/scaletest/harness"
)
type Runner struct {
diff --git a/scaletest/dashboard/run_test.go b/scaletest/dashboard/run_test.go
index d522ba1a6ec88..88c4aecb9b8b6 100644
--- a/scaletest/dashboard/run_test.go
+++ b/scaletest/dashboard/run_test.go
@@ -12,9 +12,9 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/scaletest/dashboard"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/scaletest/dashboard"
+ "github.com/coder/coder/v2/testutil"
)
func Test_Run(t *testing.T) {
diff --git a/scaletest/harness/harness.go b/scaletest/harness/harness.go
index bd99f5cfb9d80..493e12dac598b 100644
--- a/scaletest/harness/harness.go
+++ b/scaletest/harness/harness.go
@@ -8,7 +8,7 @@ import (
"github.com/hashicorp/go-multierror"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/tracing"
+ "github.com/coder/coder/v2/coderd/tracing"
)
// TestHarness runs a bunch of registered test runs using the given execution
diff --git a/scaletest/harness/harness_test.go b/scaletest/harness/harness_test.go
index 5b0a9ba556fc7..11fb8d8bfee75 100644
--- a/scaletest/harness/harness_test.go
+++ b/scaletest/harness/harness_test.go
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/harness"
)
const testPanicMessage = "expected test panic"
diff --git a/scaletest/harness/results.go b/scaletest/harness/results.go
index 3bb1c02596688..a8715e3465e2e 100644
--- a/scaletest/harness/results.go
+++ b/scaletest/harness/results.go
@@ -7,7 +7,7 @@ import (
"strings"
"time"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
// Results is the full compiled results for a set of test runs.
diff --git a/scaletest/harness/results_test.go b/scaletest/harness/results_test.go
index e52d5f2838ae0..d16bb694b7889 100644
--- a/scaletest/harness/results_test.go
+++ b/scaletest/harness/results_test.go
@@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/scaletest/harness"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/scaletest/harness"
)
func Test_Results(t *testing.T) {
diff --git a/scaletest/harness/run_test.go b/scaletest/harness/run_test.go
index a8e0932b6e979..e339849061edf 100644
--- a/scaletest/harness/run_test.go
+++ b/scaletest/harness/run_test.go
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/harness"
)
// testFns implements Runnable and Cleanable.
diff --git a/scaletest/harness/strategies_test.go b/scaletest/harness/strategies_test.go
index 909a3a8f39e5e..0858b5bf71da1 100644
--- a/scaletest/harness/strategies_test.go
+++ b/scaletest/harness/strategies_test.go
@@ -13,7 +13,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
- "github.com/coder/coder/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/harness"
)
//nolint:paralleltest // this tests uses timings to determine if it's working
diff --git a/scaletest/placebo/config.go b/scaletest/placebo/config.go
index 501afb3961572..7d142b1d6b849 100644
--- a/scaletest/placebo/config.go
+++ b/scaletest/placebo/config.go
@@ -3,7 +3,7 @@ package placebo
import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpapi"
)
type Config struct {
diff --git a/scaletest/placebo/config_test.go b/scaletest/placebo/config_test.go
index b814e251523e2..8e3a40000a02e 100644
--- a/scaletest/placebo/config_test.go
+++ b/scaletest/placebo/config_test.go
@@ -6,8 +6,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/scaletest/placebo"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/scaletest/placebo"
)
func Test_Config(t *testing.T) {
diff --git a/scaletest/placebo/run.go b/scaletest/placebo/run.go
index 8692d8feb30b0..71cf4209fb1e1 100644
--- a/scaletest/placebo/run.go
+++ b/scaletest/placebo/run.go
@@ -9,7 +9,7 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/harness"
)
type Runner struct {
diff --git a/scaletest/placebo/run_test.go b/scaletest/placebo/run_test.go
index 3b6a0b382a617..86aaf634846d3 100644
--- a/scaletest/placebo/run_test.go
+++ b/scaletest/placebo/run_test.go
@@ -9,8 +9,8 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/scaletest/placebo"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/scaletest/placebo"
)
func Test_Runner(t *testing.T) {
diff --git a/scaletest/reconnectingpty/config.go b/scaletest/reconnectingpty/config.go
index 924a3e885e635..c226bcc39ca45 100644
--- a/scaletest/reconnectingpty/config.go
+++ b/scaletest/reconnectingpty/config.go
@@ -6,8 +6,8 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
)
const (
diff --git a/scaletest/reconnectingpty/config_test.go b/scaletest/reconnectingpty/config_test.go
index 0c5200bfd7fe6..c6944e3268076 100644
--- a/scaletest/reconnectingpty/config_test.go
+++ b/scaletest/reconnectingpty/config_test.go
@@ -7,9 +7,9 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/scaletest/reconnectingpty"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/scaletest/reconnectingpty"
)
func Test_Config(t *testing.T) {
diff --git a/scaletest/reconnectingpty/run.go b/scaletest/reconnectingpty/run.go
index afded88eb0412..d9b01c8a4d82a 100644
--- a/scaletest/reconnectingpty/run.go
+++ b/scaletest/reconnectingpty/run.go
@@ -13,10 +13,10 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/scaletest/harness"
- "github.com/coder/coder/scaletest/loadtestutil"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/loadtestutil"
)
type Runner struct {
diff --git a/scaletest/reconnectingpty/run_test.go b/scaletest/reconnectingpty/run_test.go
index 382a3718436f9..81de3dcfb9da8 100644
--- a/scaletest/reconnectingpty/run_test.go
+++ b/scaletest/reconnectingpty/run_test.go
@@ -11,15 +11,15 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/scaletest/reconnectingpty"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/scaletest/reconnectingpty"
+ "github.com/coder/coder/v2/testutil"
)
func Test_Runner(t *testing.T) {
@@ -252,10 +252,10 @@ func setupRunnerTest(t *testing.T) (client *codersdk.Client, agentID uuid.UUID)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
diff --git a/scaletest/terraform/gcp_vpc.tf b/scaletest/terraform/gcp_vpc.tf
index 7ed76a00235e9..eb965354c3917 100644
--- a/scaletest/terraform/gcp_vpc.tf
+++ b/scaletest/terraform/gcp_vpc.tf
@@ -12,7 +12,7 @@ resource "google_compute_subnetwork" "subnet" {
project = var.project_id
region = var.region
network = google_compute_network.vpc.name
- ip_cidr_range = "10.10.0.0/24"
+ ip_cidr_range = "10.200.0.0/24"
}
resource "google_compute_global_address" "sql_peering" {
diff --git a/scaletest/workspacebuild/config.go b/scaletest/workspacebuild/config.go
index e2c361d45bcb3..99211010168f1 100644
--- a/scaletest/workspacebuild/config.go
+++ b/scaletest/workspacebuild/config.go
@@ -4,7 +4,7 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
)
type Config struct {
diff --git a/scaletest/workspacebuild/config_test.go b/scaletest/workspacebuild/config_test.go
index 4efbddfe1f364..b9c427f104f3d 100644
--- a/scaletest/workspacebuild/config_test.go
+++ b/scaletest/workspacebuild/config_test.go
@@ -6,8 +6,8 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/scaletest/workspacebuild"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/scaletest/workspacebuild"
)
func Test_Config(t *testing.T) {
diff --git a/scaletest/workspacebuild/run.go b/scaletest/workspacebuild/run.go
index 166aaf1f1aa79..49b2ba6041117 100644
--- a/scaletest/workspacebuild/run.go
+++ b/scaletest/workspacebuild/run.go
@@ -12,11 +12,11 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/scaletest/harness"
- "github.com/coder/coder/scaletest/loadtestutil"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/loadtestutil"
)
type Runner struct {
@@ -112,7 +112,30 @@ func (r *CleanupRunner) Run(ctx context.Context, _ string, logs io.Writer) error
ctx, span := tracing.StartSpan(ctx)
defer span.End()
- build, err := r.client.CreateWorkspaceBuild(ctx, r.workspaceID, codersdk.CreateWorkspaceBuildRequest{
+ logs = loadtestutil.NewSyncWriter(logs)
+ logger := slog.Make(sloghuman.Sink(logs)).Leveled(slog.LevelDebug)
+ r.client.SetLogger(logger)
+ r.client.SetLogBodies(true)
+
+ ws, err := r.client.Workspace(ctx, r.workspaceID)
+ if err != nil {
+ return err
+ }
+
+ build, err := r.client.WorkspaceBuild(ctx, ws.LatestBuild.ID)
+ if err == nil && build.Job.Status.Active() {
+ // mark the build as canceled
+ if err = r.client.CancelWorkspaceBuild(ctx, build.ID); err == nil {
+ // Wait for the job to cancel before we delete it
+ _ = waitForBuild(ctx, logs, r.client, build.ID) // it will return a "build canceled" error
+ } else {
+ logger.Warn(ctx, "failed to cancel workspace build, attempting to delete anyway", slog.Error(err))
+ }
+ } else {
+ logger.Warn(ctx, "unable to lookup latest workspace build, attempting to delete anyway", slog.Error(err))
+ }
+
+ build, err = r.client.CreateWorkspaceBuild(ctx, r.workspaceID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionDelete,
})
if err != nil {
diff --git a/scaletest/workspacebuild/run_test.go b/scaletest/workspacebuild/run_test.go
index 9a4fa9fdc4122..c07b10f8095b9 100644
--- a/scaletest/workspacebuild/run_test.go
+++ b/scaletest/workspacebuild/run_test.go
@@ -13,14 +13,14 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/scaletest/workspacebuild"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/scaletest/workspacebuild"
+ "github.com/coder/coder/v2/testutil"
)
func Test_Runner(t *testing.T) {
@@ -45,10 +45,10 @@ func Test_Runner(t *testing.T) {
authToken3 := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{
{
- Type: &proto.Provision_Response_Log{
+ Type: &proto.Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "hello from logs",
@@ -56,8 +56,8 @@ func Test_Runner(t *testing.T) {
},
},
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Name: "example1",
@@ -199,11 +199,11 @@ func Test_Runner(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{
{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Error: "test error",
},
},
diff --git a/scaletest/workspacetraffic/conn.go b/scaletest/workspacetraffic/conn.go
index 167164c5ef33f..2248ebc4786f8 100644
--- a/scaletest/workspacetraffic/conn.go
+++ b/scaletest/workspacetraffic/conn.go
@@ -5,7 +5,7 @@ import (
"io"
"sync"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/codersdk"
"github.com/google/uuid"
"github.com/hashicorp/go-multierror"
@@ -19,7 +19,7 @@ func connectPTY(ctx context.Context, client *codersdk.Client, agentID, reconnect
Reconnect: reconnect,
Height: 25,
Width: 80,
- Command: "/bin/sh",
+ Command: "sh",
})
if err != nil {
return nil, xerrors.Errorf("connect pty: %w", err)
diff --git a/scaletest/workspacetraffic/run.go b/scaletest/workspacetraffic/run.go
index db263ac90911e..aea4345c4752c 100644
--- a/scaletest/workspacetraffic/run.go
+++ b/scaletest/workspacetraffic/run.go
@@ -13,11 +13,11 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/coderd/tracing"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/cryptorand"
- "github.com/coder/coder/scaletest/harness"
- "github.com/coder/coder/scaletest/loadtestutil"
+ "github.com/coder/coder/v2/coderd/tracing"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/cryptorand"
+ "github.com/coder/coder/v2/scaletest/harness"
+ "github.com/coder/coder/v2/scaletest/loadtestutil"
)
type Runner struct {
diff --git a/scaletest/workspacetraffic/run_test.go b/scaletest/workspacetraffic/run_test.go
index acea009ceacce..308630910427d 100644
--- a/scaletest/workspacetraffic/run_test.go
+++ b/scaletest/workspacetraffic/run_test.go
@@ -8,14 +8,14 @@ import (
"testing"
"time"
- "github.com/coder/coder/agent"
- "github.com/coder/coder/coderd/coderdtest"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/codersdk/agentsdk"
- "github.com/coder/coder/provisioner/echo"
- "github.com/coder/coder/provisionersdk/proto"
- "github.com/coder/coder/scaletest/workspacetraffic"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/agent"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/codersdk/agentsdk"
+ "github.com/coder/coder/v2/provisioner/echo"
+ "github.com/coder/coder/v2/provisionersdk/proto"
+ "github.com/coder/coder/v2/scaletest/workspacetraffic"
+ "github.com/coder/coder/v2/testutil"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
@@ -41,10 +41,10 @@ func TestRun(t *testing.T) {
agentName = "agent"
version = coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
@@ -154,10 +154,10 @@ func TestRun(t *testing.T) {
agentName = "agent"
version = coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
- ProvisionPlan: echo.ProvisionComplete,
- ProvisionApply: []*proto.Provision_Response{{
- Type: &proto.Provision_Response_Complete{
- Complete: &proto.Provision_Complete{
+ ProvisionPlan: echo.PlanComplete,
+ ProvisionApply: []*proto.Response{{
+ Type: &proto.Response_Apply{
+ Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
diff --git a/scripts/Dockerfile.base b/scripts/Dockerfile.base
index 738b66f01090e..2681ef9cafec2 100644
--- a/scripts/Dockerfile.base
+++ b/scripts/Dockerfile.base
@@ -1,7 +1,7 @@
# This is the base image used for Coder images. It's a multi-arch image that is
# built in depot.dev for all supported architectures. Since it's built on real
# hardware and not cross-compiled, it can have "RUN" commands.
-FROM alpine:3.18.2
+FROM alpine:3.18.3
# We use a single RUN command to reduce the number of layers in the image.
# NOTE: Keep the Terraform version in sync with minTerraformVersion and
@@ -10,10 +10,7 @@ RUN apk add --no-cache \
curl \
wget \
bash \
- jq \
git \
- # Fixes CVE-2023-3446 and CVE-2023-2975. Only necessary until Alpine 3.18.3.
- openssl \
openssh-client && \
# Use the edge repo, since Terraform doesn't seem to be backported to 3.18.
apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
diff --git a/scripts/apidocgen/markdown-template/security.def b/scripts/apidocgen/markdown-template/security.def
index aadaf424b97fd..e45d06abe9343 100644
--- a/scripts/apidocgen/markdown-template/security.def
+++ b/scripts/apidocgen/markdown-template/security.def
@@ -3,7 +3,7 @@
Long-lived tokens can be generated to perform actions on behalf of your user account:
-```console
+```shell
coder tokens create
```
diff --git a/scripts/apidocgen/postprocess/main.go b/scripts/apidocgen/postprocess/main.go
index 8441507fada5f..b1f7d43fa2ce5 100644
--- a/scripts/apidocgen/postprocess/main.go
+++ b/scripts/apidocgen/postprocess/main.go
@@ -24,13 +24,13 @@ const (
Generate a token on your Coder deployment by visiting:
-` + "````sh" + `
+` + "````shell" + `
https://coder.example.com/settings/tokens
` + "````" + `
List your workspaces
-` + "````sh" + `
+` + "````shell" + `
# CLI
curl https://coder.example.com/api/v2/workspaces?q=owner:me \
-H "Coder-Session-Token: "
diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go
index 3cf4b1fe79c11..9eeaf5664911e 100644
--- a/scripts/apitypings/main.go
+++ b/scripts/apitypings/main.go
@@ -22,7 +22,7 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
- "github.com/coder/coder/coderd/util/slice"
+ "github.com/coder/coder/v2/coderd/util/slice"
)
var (
@@ -655,8 +655,6 @@ type TypescriptType struct {
// Eg:
//
// []byte returns "string"
-//
-//nolint:gocyclo
func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
switch ty := ty.(type) {
case *types.Basic:
@@ -754,15 +752,15 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
// These are external named types that we handle uniquely.
switch n.String() {
- case "github.com/coder/coder/cli/clibase.String":
+ case "github.com/coder/coder/v2/cli/clibase.String":
return TypescriptType{ValueType: "string"}, nil
- case "github.com/coder/coder/cli/clibase.Strings":
+ case "github.com/coder/coder/v2/cli/clibase.Strings":
return TypescriptType{ValueType: "string[]"}, nil
- case "github.com/coder/coder/cli/clibase.Int64":
+ case "github.com/coder/coder/v2/cli/clibase.Int64":
return TypescriptType{ValueType: "number"}, nil
- case "github.com/coder/coder/cli/clibase.Bool":
+ case "github.com/coder/coder/v2/cli/clibase.Bool":
return TypescriptType{ValueType: "boolean"}, nil
- case "github.com/coder/coder/cli/clibase.Duration":
+ case "github.com/coder/coder/v2/cli/clibase.Duration":
return TypescriptType{ValueType: "number"}, nil
case "net/url.URL":
return TypescriptType{ValueType: "string"}, nil
@@ -773,7 +771,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
return TypescriptType{ValueType: "number"}, nil
case "database/sql.NullTime":
return TypescriptType{ValueType: "string", Optional: true}, nil
- case "github.com/coder/coder/codersdk.NullTime":
+ case "github.com/coder/coder/v2/codersdk.NullTime":
return TypescriptType{ValueType: "string", Optional: true}, nil
case "github.com/google/uuid.NullUUID":
return TypescriptType{ValueType: "string", Optional: true}, nil
@@ -781,7 +779,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
return TypescriptType{ValueType: "string"}, nil
case "encoding/json.RawMessage":
return TypescriptType{ValueType: "Record"}, nil
- case "github.com/coder/coder/cli/clibase.URL":
+ case "github.com/coder/coder/v2/cli/clibase.URL":
return TypescriptType{ValueType: "string"}, nil
}
diff --git a/scripts/auditdocgen/main.go b/scripts/auditdocgen/main.go
index 761658048629b..694fdfc5329b8 100644
--- a/scripts/auditdocgen/main.go
+++ b/scripts/auditdocgen/main.go
@@ -11,7 +11,7 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/enterprise/audit"
+ "github.com/coder/coder/v2/enterprise/audit"
)
var (
diff --git a/scripts/build_go.sh b/scripts/build_go.sh
index 905738786149a..366abb08d95c9 100755
--- a/scripts/build_go.sh
+++ b/scripts/build_go.sh
@@ -96,7 +96,7 @@ fi
ldflags=(
-s
-w
- -X "'github.com/coder/coder/buildinfo.tag=$version'"
+ -X "'github.com/coder/coder/v2/buildinfo.tag=$version'"
)
if [[ "$slim" == 0 ]]; then
@@ -107,7 +107,7 @@ fi
if [[ "$agpl" == 1 ]]; then
# We don't use a tag to control AGPL because we don't want code to depend on
# a flag to control AGPL vs. enterprise behavior.
- ldflags+=(-X "'github.com/coder/coder/buildinfo.agpl=true'")
+ ldflags+=(-X "'github.com/coder/coder/v2/buildinfo.agpl=true'")
fi
build_args+=(-ldflags "${ldflags[*]}")
diff --git a/scripts/check_enterprise_imports.sh b/scripts/check_enterprise_imports.sh
index 321a48ab25f94..340453a62d239 100755
--- a/scripts/check_enterprise_imports.sh
+++ b/scripts/check_enterprise_imports.sh
@@ -13,7 +13,7 @@ find . -regex ".*\.go" |
grep -v "./enterprise" |
grep -v ./scripts/auditdocgen/ --include="*.go" |
grep -v ./scripts/clidocgen/ --include="*.go" |
- xargs grep -n "github.com/coder/coder/enterprise"
+ xargs grep -n "github.com/coder/coder/v2/enterprise"
# reverse the exit code because we want this script to fail if grep finds anything.
status=$?
set -e
diff --git a/scripts/chocolatey/coder.nuspec b/scripts/chocolatey/coder.nuspec
new file mode 100644
index 0000000000000..65751f257e436
--- /dev/null
+++ b/scripts/chocolatey/coder.nuspec
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ coder
+ $version$
+ https://github.com/coder/coder/blob/main/scripts/chocolatey
+
+ Coder Technologies\, Inc.
+
+
+
+
+ Coder (Install)
+ Coder Technologies\, Inc.
+ https://coder.com
+ https://github.com/coder/presskit/raw/main/logos/coder%20logo%20black%20square.png?raw=true
+ Coder Technologies, Inc.
+ https://coder.com/legal/terms-of-service
+ true
+ https://github.com/coder/coder.git
+ https://coder.com/docs/v2/latest
+ https://github.com/coder/coder/issues
+ coder remote-dev terraform development
+ Remote development environments on your infrastructure provisioned with Terraform
+ Remote development environments on your infrastructure provisioned with Terraform
+
+
+
+
+
diff --git a/scripts/ci-report/main.go b/scripts/ci-report/main.go
index 9e3ae7e39d6bb..9637af813e512 100644
--- a/scripts/ci-report/main.go
+++ b/scripts/ci-report/main.go
@@ -12,6 +12,8 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
+
+ "github.com/coder/coder/v2/coderd/util/slice"
)
func main() {
@@ -161,9 +163,8 @@ func parseCIReport(report GotestsumReport) (CIReport, error) {
}
timeouts = timeoutsNorm
- sortAZ := func(a, b string) bool { return a < b }
- slices.SortFunc(packagesSortedByName, sortAZ)
- slices.SortFunc(testSortedByName, sortAZ)
+ slices.SortFunc(packagesSortedByName, slice.Ascending[string])
+ slices.SortFunc(testSortedByName, slice.Ascending[string])
var rep CIReport
diff --git a/scripts/ci-report/testdata/ci-report_gotests-timeout.json.sample.golden b/scripts/ci-report/testdata/ci-report_gotests-timeout.json.sample.golden
index 5f7a8ce57e843..e5df1c70df6a4 100644
--- a/scripts/ci-report/testdata/ci-report_gotests-timeout.json.sample.golden
+++ b/scripts/ci-report/testdata/ci-report_gotests-timeout.json.sample.golden
@@ -1,38 +1,38 @@
{
"packages": [
{
- "name": "agent",
+ "name": "v2/agent",
"time": 2.045,
"fail": true,
"num_failed": 1,
"timeout": true,
- "output": "panic: test timed out after 2s\nrunning tests:\n\tTestAgent_Session_TTY_Hushlogin (0s)\n\ngoroutine 411 [running]:\ntesting.(*M).startAlarm.func1()\n\t/usr/local/go/src/testing/testing.go:2241 +0x3b9\ncreated by time.goFunc\n\t/usr/local/go/src/time/sleep.go:176 +0x32\n\ngoroutine 1 [chan receive]:\ntesting.(*T).Run(0xc0004e1040, {0x16a5e92?, 0x535fa5?}, 0x17462c0)\n\t/usr/local/go/src/testing/testing.go:1630 +0x405\ntesting.runTests.func1(0x236db60?)\n\t/usr/local/go/src/testing/testing.go:2036 +0x45\ntesting.tRunner(0xc0004e1040, 0xc000589bb8)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ntesting.runTests(0xc000341a40?, {0x235c580, 0x21, 0x21}, {0x4182d0?, 0xc000589c78?, 0x236cb40?})\n\t/usr/local/go/src/testing/testing.go:2034 +0x489\ntesting.(*M).Run(0xc000341a40)\n\t/usr/local/go/src/testing/testing.go:1906 +0x63a\ngo.uber.org/goleak.VerifyTestMain({0x18f5540?, 0xc000341a40?}, {0x0, 0x0, 0x0})\n\t/home/mafredri/.local/go/pkg/mod/go.uber.org/goleak@v1.2.1/testmain.go:53 +0x6b\ngithub.com/coder/coder/agent_test.TestMain(...)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:53\nmain.main()\n\t_testmain.go:115 +0x1e5\n\ngoroutine 9 [chan receive]:\ntesting.(*T).Parallel(0xc0004e11e0)\n\t/usr/local/go/src/testing/testing.go:1384 +0x225\ngithub.com/coder/coder/agent_test.TestAgent_SessionExec(0x0?)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:188 +0x33\ntesting.tRunner(0xc0004e11e0, 0x1746298)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ncreated by testing.(*T).Run\n\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n\ngoroutine 10 [chan receive]:\ntesting.(*T).Parallel(0xc0004e1520)\n\t/usr/local/go/src/testing/testing.go:1384 +0x225\ngithub.com/coder/coder/agent_test.TestAgent_SessionTTYShell(0xc0004e1520)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:213 +0x36\ntesting.tRunner(0xc0004e1520, 0x17462a8)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ncreated by testing.(*T).Run\n\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n\ngoroutine 11 [chan receive]:\ntesting.(*T).Parallel(0xc0004e1860)\n\t/usr/local/go/src/testing/testing.go:1384 +0x225\ngithub.com/coder/coder/agent_test.TestAgent_SessionTTYExitCode(0xc0004e1520?)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:244 +0x36\ntesting.tRunner(0xc0004e1860, 0x17462a0)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ncreated by testing.(*T).Run\n\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n\ngoroutine 408 [runnable]:\nmath/big.nat.montgomery({0xc004aa4500?, 0x10?, 0x26?}, {0xc004aa4280?, 0x10?, 0x26?}, {0xc004aa4280?, 0x10?, 0x26?}, {0xc000732820, ...}, ...)\n\t/usr/local/go/src/math/big/nat.go:216 +0x565\nmath/big.nat.expNNMontgomery({0xc004aa4280, 0xc0003c2e70?, 0x26}, {0xc004a9adc0?, 0x21?, 0x24?}, {0xc004a9ac80, 0x10, 0x24?}, {0xc000732820, ...})\n\t/usr/local/go/src/math/big/nat.go:1271 +0xb1c\nmath/big.nat.expNN({0xc004aa4280?, 0x14?, 0x22c2900?}, {0xc004a9adc0?, 0x10, 0x14}, {0xc004a9ac80?, 0x10, 0x14?}, {0xc000732820, ...}, ...)\n\t/usr/local/go/src/math/big/nat.go:996 +0x3b1\nmath/big.nat.probablyPrimeMillerRabin({0xc000732820?, 0x10, 0x14}, 0x15, 0x1)\n\t/usr/local/go/src/math/big/prime.go:106 +0x5b8\nmath/big.(*Int).ProbablyPrime(0xc0047208c0, 0x14)\n\t/usr/local/go/src/math/big/prime.go:78 +0x225\ncrypto/rand.Prime({0x18f04c0, 0xc00007e020}, 0x400)\n\t/usr/local/go/src/crypto/rand/util.go:55 +0x1e5\ncrypto/rsa.GenerateMultiPrimeKey({0x18f04c0, 0xc00007e020}, 0x2, 0x800)\n\t/usr/local/go/src/crypto/rsa/rsa.go:369 +0x745\ncrypto/rsa.GenerateKey(...)\n\t/usr/local/go/src/crypto/rsa/rsa.go:264\ngithub.com/coder/coder/agent.(*agent).init(0xc00485eea0, {0x1902c20?, 0xc00485d770})\n\t/home/mafredri/src/coder/coder/agent/agent.go:810 +0x6c\ngithub.com/coder/coder/agent.New({{0x190cbc0, 0xc0005b7710}, {0x166d829, 0x4}, {0x166d829, 0x4}, 0x17461d8, {0x1907c90, 0xc000278280}, 0x45d964b800, ...})\n\t/home/mafredri/src/coder/coder/agent/agent.go:134 +0x549\ngithub.com/coder/coder/agent_test.setupAgent(0xc00485eb60, {0x0, {0x0, 0x0}, {0x0, 0x0, 0x0}, 0xc0005b8da0, 0x0, {0x0, ...}, ...}, ...)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:1568 +0x63e\ngithub.com/coder/coder/agent_test.setupSSHSession(0xc00485eb60, {0x0, {0x0, 0x0}, {0x0, 0x0, 0x0}, 0x0, 0x0, {0x0, ...}, ...})\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:1524 +0xc5\ngithub.com/coder/coder/agent_test.TestAgent_Session_TTY_Hushlogin(0xc00485eb60)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:330 +0x2fa\ntesting.tRunner(0xc00485eb60, 0x17462c0)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ncreated by testing.(*T).Run\n\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n\ngoroutine 409 [IO wait]:\ninternal/poll.runtime_pollWait(0x7f5230766628, 0x72)\n\t/usr/local/go/src/runtime/netpoll.go:306 +0x89\ninternal/poll.(*pollDesc).wait(0xc00475bf80?, 0xc0005ec5e2?, 0x0)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x32\ninternal/poll.(*pollDesc).waitRead(...)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\ninternal/poll.(*FD).Accept(0xc00475bf80)\n\t/usr/local/go/src/internal/poll/fd_unix.go:614 +0x2bd\nnet.(*netFD).accept(0xc00475bf80)\n\t/usr/local/go/src/net/fd_unix.go:172 +0x35\nnet.(*TCPListener).accept(0xc00486ecd8)\n\t/usr/local/go/src/net/tcpsock_posix.go:148 +0x25\nnet.(*TCPListener).Accept(0xc00486ecd8)\n\t/usr/local/go/src/net/tcpsock.go:297 +0x3d\ncrypto/tls.(*listener).Accept(0xc00486ef18)\n\t/usr/local/go/src/crypto/tls/tls.go:66 +0x2d\nnet/http.(*Server).Serve(0xc00029da40, {0x18fefa0, 0xc00486ef18})\n\t/usr/local/go/src/net/http/server.go:3059 +0x385\nnet/http/httptest.(*Server).goServe.func1()\n\t/usr/local/go/src/net/http/httptest/server.go:310 +0x6a\ncreated by net/http/httptest.(*Server).goServe\n\t/usr/local/go/src/net/http/httptest/server.go:308 +0x6a\n\ngoroutine 410 [IO wait]:\ninternal/poll.runtime_pollWait(0x7f5230765908, 0x72)\n\t/usr/local/go/src/runtime/netpoll.go:306 +0x89\ninternal/poll.(*pollDesc).wait(0xc00043a300?, 0xc004880000?, 0x0)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x32\ninternal/poll.(*pollDesc).waitRead(...)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\ninternal/poll.(*FD).ReadFromInet4(0xc00043a300, {0xc004880000, 0x10000, 0x10000}, 0x0?)\n\t/usr/local/go/src/internal/poll/fd_unix.go:250 +0x24f\nnet.(*netFD).readFromInet4(0xc00043a300, {0xc004880000?, 0x0?, 0x0?}, 0x0?)\n\t/usr/local/go/src/net/fd_posix.go:66 +0x29\nnet.(*UDPConn).readFrom(0x30?, {0xc004880000?, 0xc0005b7770?, 0x0?}, 0xc0005b7770)\n\t/usr/local/go/src/net/udpsock_posix.go:52 +0x1b8\nnet.(*UDPConn).readFromUDP(0xc000015a08, {0xc004880000?, 0x4102c7?, 0x10000?}, 0x13e45e0?)\n\t/usr/local/go/src/net/udpsock.go:149 +0x31\nnet.(*UDPConn).ReadFrom(0x59a?, {0xc004880000, 0x10000, 0x10000})\n\t/usr/local/go/src/net/udpsock.go:158 +0x50\ntailscale.com/net/stun/stuntest.runSTUN({0x1911ec0, 0xc00485eb60}, {0x1907f60, 0xc000015a08}, 0xc00481baa0, 0x17462c0?)\n\t/home/mafredri/.local/go/pkg/mod/github.com/coder/tailscale@v1.1.1-0.20230327205451-058fa46a3723/net/stun/stuntest/stuntest.go:59 +0xc6\ncreated by tailscale.com/net/stun/stuntest.ServeWithPacketListener\n\t/home/mafredri/.local/go/pkg/mod/github.com/coder/tailscale@v1.1.1-0.20230327205451-058fa46a3723/net/stun/stuntest/stuntest.go:47 +0x26a\n"
+ "output": "panic: test timed out after 2s\nrunning tests:\n\tTestAgent_Session_TTY_Hushlogin (0s)\n\ngoroutine 411 [running]:\ntesting.(*M).startAlarm.func1()\n\t/usr/local/go/src/testing/testing.go:2241 +0x3b9\ncreated by time.goFunc\n\t/usr/local/go/src/time/sleep.go:176 +0x32\n\ngoroutine 1 [chan receive]:\ntesting.(*T).Run(0xc0004e1040, {0x16a5e92?, 0x535fa5?}, 0x17462c0)\n\t/usr/local/go/src/testing/testing.go:1630 +0x405\ntesting.runTests.func1(0x236db60?)\n\t/usr/local/go/src/testing/testing.go:2036 +0x45\ntesting.tRunner(0xc0004e1040, 0xc000589bb8)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ntesting.runTests(0xc000341a40?, {0x235c580, 0x21, 0x21}, {0x4182d0?, 0xc000589c78?, 0x236cb40?})\n\t/usr/local/go/src/testing/testing.go:2034 +0x489\ntesting.(*M).Run(0xc000341a40)\n\t/usr/local/go/src/testing/testing.go:1906 +0x63a\ngo.uber.org/goleak.VerifyTestMain({0x18f5540?, 0xc000341a40?}, {0x0, 0x0, 0x0})\n\t/home/mafredri/.local/go/pkg/mod/go.uber.org/goleak@v1.2.1/testmain.go:53 +0x6b\ngithub.com/coder/coder/v2/agent_test.TestMain(...)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:53\nmain.main()\n\t_testmain.go:115 +0x1e5\n\ngoroutine 9 [chan receive]:\ntesting.(*T).Parallel(0xc0004e11e0)\n\t/usr/local/go/src/testing/testing.go:1384 +0x225\ngithub.com/coder/coder/v2/agent_test.TestAgent_SessionExec(0x0?)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:188 +0x33\ntesting.tRunner(0xc0004e11e0, 0x1746298)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ncreated by testing.(*T).Run\n\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n\ngoroutine 10 [chan receive]:\ntesting.(*T).Parallel(0xc0004e1520)\n\t/usr/local/go/src/testing/testing.go:1384 +0x225\ngithub.com/coder/coder/v2/agent_test.TestAgent_SessionTTYShell(0xc0004e1520)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:213 +0x36\ntesting.tRunner(0xc0004e1520, 0x17462a8)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ncreated by testing.(*T).Run\n\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n\ngoroutine 11 [chan receive]:\ntesting.(*T).Parallel(0xc0004e1860)\n\t/usr/local/go/src/testing/testing.go:1384 +0x225\ngithub.com/coder/coder/v2/agent_test.TestAgent_SessionTTYExitCode(0xc0004e1520?)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:244 +0x36\ntesting.tRunner(0xc0004e1860, 0x17462a0)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ncreated by testing.(*T).Run\n\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n\ngoroutine 408 [runnable]:\nmath/big.nat.montgomery({0xc004aa4500?, 0x10?, 0x26?}, {0xc004aa4280?, 0x10?, 0x26?}, {0xc004aa4280?, 0x10?, 0x26?}, {0xc000732820, ...}, ...)\n\t/usr/local/go/src/math/big/nat.go:216 +0x565\nmath/big.nat.expNNMontgomery({0xc004aa4280, 0xc0003c2e70?, 0x26}, {0xc004a9adc0?, 0x21?, 0x24?}, {0xc004a9ac80, 0x10, 0x24?}, {0xc000732820, ...})\n\t/usr/local/go/src/math/big/nat.go:1271 +0xb1c\nmath/big.nat.expNN({0xc004aa4280?, 0x14?, 0x22c2900?}, {0xc004a9adc0?, 0x10, 0x14}, {0xc004a9ac80?, 0x10, 0x14?}, {0xc000732820, ...}, ...)\n\t/usr/local/go/src/math/big/nat.go:996 +0x3b1\nmath/big.nat.probablyPrimeMillerRabin({0xc000732820?, 0x10, 0x14}, 0x15, 0x1)\n\t/usr/local/go/src/math/big/prime.go:106 +0x5b8\nmath/big.(*Int).ProbablyPrime(0xc0047208c0, 0x14)\n\t/usr/local/go/src/math/big/prime.go:78 +0x225\ncrypto/rand.Prime({0x18f04c0, 0xc00007e020}, 0x400)\n\t/usr/local/go/src/crypto/rand/util.go:55 +0x1e5\ncrypto/rsa.GenerateMultiPrimeKey({0x18f04c0, 0xc00007e020}, 0x2, 0x800)\n\t/usr/local/go/src/crypto/rsa/rsa.go:369 +0x745\ncrypto/rsa.GenerateKey(...)\n\t/usr/local/go/src/crypto/rsa/rsa.go:264\ngithub.com/coder/coder/v2/agent.(*agent).init(0xc00485eea0, {0x1902c20?, 0xc00485d770})\n\t/home/mafredri/src/coder/coder/agent/agent.go:810 +0x6c\ngithub.com/coder/coder/v2/agent.New({{0x190cbc0, 0xc0005b7710}, {0x166d829, 0x4}, {0x166d829, 0x4}, 0x17461d8, {0x1907c90, 0xc000278280}, 0x45d964b800, ...})\n\t/home/mafredri/src/coder/coder/agent/agent.go:134 +0x549\ngithub.com/coder/coder/v2/agent_test.setupAgent(0xc00485eb60, {0x0, {0x0, 0x0}, {0x0, 0x0, 0x0}, 0xc0005b8da0, 0x0, {0x0, ...}, ...}, ...)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:1568 +0x63e\ngithub.com/coder/coder/v2/agent_test.setupSSHSession(0xc00485eb60, {0x0, {0x0, 0x0}, {0x0, 0x0, 0x0}, 0x0, 0x0, {0x0, ...}, ...})\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:1524 +0xc5\ngithub.com/coder/coder/v2/agent_test.TestAgent_Session_TTY_Hushlogin(0xc00485eb60)\n\t/home/mafredri/src/coder/coder/agent/agent_test.go:330 +0x2fa\ntesting.tRunner(0xc00485eb60, 0x17462c0)\n\t/usr/local/go/src/testing/testing.go:1576 +0x10b\ncreated by testing.(*T).Run\n\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n\ngoroutine 409 [IO wait]:\ninternal/poll.runtime_pollWait(0x7f5230766628, 0x72)\n\t/usr/local/go/src/runtime/netpoll.go:306 +0x89\ninternal/poll.(*pollDesc).wait(0xc00475bf80?, 0xc0005ec5e2?, 0x0)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x32\ninternal/poll.(*pollDesc).waitRead(...)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\ninternal/poll.(*FD).Accept(0xc00475bf80)\n\t/usr/local/go/src/internal/poll/fd_unix.go:614 +0x2bd\nnet.(*netFD).accept(0xc00475bf80)\n\t/usr/local/go/src/net/fd_unix.go:172 +0x35\nnet.(*TCPListener).accept(0xc00486ecd8)\n\t/usr/local/go/src/net/tcpsock_posix.go:148 +0x25\nnet.(*TCPListener).Accept(0xc00486ecd8)\n\t/usr/local/go/src/net/tcpsock.go:297 +0x3d\ncrypto/tls.(*listener).Accept(0xc00486ef18)\n\t/usr/local/go/src/crypto/tls/tls.go:66 +0x2d\nnet/http.(*Server).Serve(0xc00029da40, {0x18fefa0, 0xc00486ef18})\n\t/usr/local/go/src/net/http/server.go:3059 +0x385\nnet/http/httptest.(*Server).goServe.func1()\n\t/usr/local/go/src/net/http/httptest/server.go:310 +0x6a\ncreated by net/http/httptest.(*Server).goServe\n\t/usr/local/go/src/net/http/httptest/server.go:308 +0x6a\n\ngoroutine 410 [IO wait]:\ninternal/poll.runtime_pollWait(0x7f5230765908, 0x72)\n\t/usr/local/go/src/runtime/netpoll.go:306 +0x89\ninternal/poll.(*pollDesc).wait(0xc00043a300?, 0xc004880000?, 0x0)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x32\ninternal/poll.(*pollDesc).waitRead(...)\n\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\ninternal/poll.(*FD).ReadFromInet4(0xc00043a300, {0xc004880000, 0x10000, 0x10000}, 0x0?)\n\t/usr/local/go/src/internal/poll/fd_unix.go:250 +0x24f\nnet.(*netFD).readFromInet4(0xc00043a300, {0xc004880000?, 0x0?, 0x0?}, 0x0?)\n\t/usr/local/go/src/net/fd_posix.go:66 +0x29\nnet.(*UDPConn).readFrom(0x30?, {0xc004880000?, 0xc0005b7770?, 0x0?}, 0xc0005b7770)\n\t/usr/local/go/src/net/udpsock_posix.go:52 +0x1b8\nnet.(*UDPConn).readFromUDP(0xc000015a08, {0xc004880000?, 0x4102c7?, 0x10000?}, 0x13e45e0?)\n\t/usr/local/go/src/net/udpsock.go:149 +0x31\nnet.(*UDPConn).ReadFrom(0x59a?, {0xc004880000, 0x10000, 0x10000})\n\t/usr/local/go/src/net/udpsock.go:158 +0x50\ntailscale.com/net/stun/stuntest.runSTUN({0x1911ec0, 0xc00485eb60}, {0x1907f60, 0xc000015a08}, 0xc00481baa0, 0x17462c0?)\n\t/home/mafredri/.local/go/pkg/mod/github.com/coder/tailscale@v1.1.1-0.20230327205451-058fa46a3723/net/stun/stuntest/stuntest.go:59 +0xc6\ncreated by tailscale.com/net/stun/stuntest.ServeWithPacketListener\n\t/home/mafredri/.local/go/pkg/mod/github.com/coder/tailscale@v1.1.1-0.20230327205451-058fa46a3723/net/stun/stuntest/stuntest.go:47 +0x26a\n"
}
],
"tests": [
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_SessionExec",
"time": 0,
"fail": true,
"output": "=== RUN TestAgent_SessionExec\n=== PAUSE TestAgent_SessionExec\n"
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_SessionTTYExitCode",
"time": 0,
"fail": true,
"output": "=== RUN TestAgent_SessionTTYExitCode\n=== PAUSE TestAgent_SessionTTYExitCode\n"
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_SessionTTYShell",
"time": 0,
"fail": true,
"output": "=== RUN TestAgent_SessionTTYShell\n=== PAUSE TestAgent_SessionTTYShell\n"
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_Session_TTY_Hushlogin",
"time": 0,
"fail": true,
@@ -40,7 +40,7 @@
"output": "=== RUN TestAgent_Session_TTY_Hushlogin\n"
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_Session_TTY_MOTD",
"time": 1.84
}
diff --git a/scripts/ci-report/testdata/ci-report_gotests.json.sample.golden b/scripts/ci-report/testdata/ci-report_gotests.json.sample.golden
index e5c46507cc6cf..d71b4be63d67e 100644
--- a/scripts/ci-report/testdata/ci-report_gotests.json.sample.golden
+++ b/scripts/ci-report/testdata/ci-report_gotests.json.sample.golden
@@ -1,372 +1,372 @@
{
"packages": [
{
- "name": "agent",
+ "name": "v2/agent",
"time": 4.341,
"fail": true,
"num_failed": 1
},
{
- "name": "cli",
+ "name": "v2/cli",
"time": 26.514,
"fail": true,
"num_failed": 7
},
{
- "name": "cli/cliui",
+ "name": "v2/cli/cliui",
"time": 0.037
}
],
"tests": [
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_SessionExec",
"time": 0.86
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_SessionTTYExitCode",
"time": 0.88
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_SessionTTYShell",
"time": 0.94
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_Session_TTY_FastCommandHasOutput",
"time": 0.95,
"fail": true,
- "output": "=== RUN TestAgent_Session_TTY_FastCommandHasOutput\n=== PAUSE TestAgent_Session_TTY_FastCommandHasOutput\n=== CONT TestAgent_Session_TTY_FastCommandHasOutput\n t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:049e454260a62aa1\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n t.go:81: 2023-03-29 13:37:27.356 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n t.go:81: 2023-03-29 13:37:27.356 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 34688, \"DERPPort\": 43117, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:34ff526bdd502e84\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n t.go:81: 2023-03-29 13:37:27.358 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.358191Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n t.go:81: 2023-03-29 13:37:27.363 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n t.go:81: 2023-03-29 13:37:27.368 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n t.go:81: 2023-03-29 13:37:27.373 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n t.go:81: 2023-03-29 13:37:27.386 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n t.go:81: 2023-03-29 13:37:27.387 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n t.go:81: 2023-03-29 13:37:27.387 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.358191Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.379813Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": []}}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.379813Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": []}}\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n t.go:81: 2023-03-29 13:37:27.441 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:58992 derp=1 derpdist=1v4:62ms\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:58992 (stun), 172.20.0.2:58992 (local)\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.514434952 +0000 UTC m=+4.154042177 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:0}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.514515Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.514515Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.515008199 +0000 UTC m=+4.154615430 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:1}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.062405899}}}\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.515155Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.062405899}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.515155Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.062405899}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.516 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:34848 derp=1 derpdist=1v4:65ms\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:34848 (stun), 172.20.0.2:34848 (local)\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.525526859 +0000 UTC m=+4.165134074 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:0}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.525972278 +0000 UTC m=+4.165579492 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:1}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.065420657}}}\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.5256Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.5256Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.526766Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.065420657}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.526766Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.065420657}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n t.go:81: 2023-03-29 13:37:27.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:58992 derp=1 derpdist=1v4:5ms\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:34848 derp=1 derpdist=1v4:7ms\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.005291379}}}\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.578687Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.005291379}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.578687Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.005291379}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.00675754}}}\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.579606Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.00675754}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.579606Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.00675754}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.624 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n t.go:81: 2023-03-29 13:37:27.625 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [5WitN] set to derp-1 (shared home)\n t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [5EOvJ] set to derp-1 (shared home)\n t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5WitN] d:34ff526bdd502e84 now using 172.20.0.2:34848\n t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n t.go:81: 2023-03-29 13:37:27.627 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Created\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Updating endpoint\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Removing all allowedips\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Adding allowedip\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Updating persistent keepalive interval\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Starting\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Sending handshake initiation\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [5EOvJ] now active, reconfiguring WireGuard\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Created\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Updating endpoint\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Removing all allowedips\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Adding allowedip\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Updating persistent keepalive interval\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Starting\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Received handshake initiation\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Sending handshake response\n t.go:81: 2023-03-29 13:37:27.630 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.630405833 +0000 UTC m=+4.270013057 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c}] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:1}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Received handshake response\n t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5EOvJ] d:049e454260a62aa1 now using 172.20.0.2:58992\n t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.631481797 +0000 UTC m=+4.271089021 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.631305354 +0000 UTC NodeKey:nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f}] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:1}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.632 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5WitN] d:34ff526bdd502e84 now using 127.0.0.1:34848\n agent_test.go:400: \n \tError Trace:\t/home/mafredri/src/coder/coder/agent/agent_test.go:400\n \t \t\t\t\t/home/mafredri/src/coder/coder/agent/agent_test.go:401\n \tError: \t\"\" does not contain \"wazzup\"\n \tTest: \tTestAgent_Session_TTY_FastCommandHasOutput\n \tMessages: \tshould output greeting\n ptytest.go:83: 2023-03-29 13:37:27.648: cmd: closing tpty: close\n ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing pty\n ptytest.go:110: 2023-03-29 13:37:27.648: cmd: copy done: read /dev/ptmx: file already closed\n ptytest.go:111: 2023-03-29 13:37:27.648: cmd: closing out\n ptytest.go:113: 2023-03-29 13:37:27.648: cmd: closed out: read /dev/ptmx: file already closed\n ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed pty: \u003cnil\u003e\n ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing logw\n ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed logw: \u003cnil\u003e\n ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing logr\n ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed logr: \u003cnil\u003e\n ptytest.go:102: 2023-03-29 13:37:27.648: cmd: closed tpty\n t.go:81: 2023-03-29 13:37:27.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n t.go:81: 2023-03-29 13:37:27.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Stopping\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n t.go:81: 2023-03-29 13:37:27.649 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d): sending disco ping to [5EOvJ] ...\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Stopping\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n stuntest.go:63: STUN server shutdown\n--- FAIL: TestAgent_Session_TTY_FastCommandHasOutput (0.95s)\n"
+ "output": "=== RUN TestAgent_Session_TTY_FastCommandHasOutput\n=== PAUSE TestAgent_Session_TTY_FastCommandHasOutput\n=== CONT TestAgent_Session_TTY_FastCommandHasOutput\n t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:049e454260a62aa1\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n t.go:81: 2023-03-29 13:37:27.356 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n t.go:81: 2023-03-29 13:37:27.356 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 34688, \"DERPPort\": 43117, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:34ff526bdd502e84\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n t.go:81: 2023-03-29 13:37:27.358 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.358191Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n t.go:81: 2023-03-29 13:37:27.363 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n t.go:81: 2023-03-29 13:37:27.368 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n t.go:81: 2023-03-29 13:37:27.373 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n t.go:81: 2023-03-29 13:37:27.386 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n t.go:81: 2023-03-29 13:37:27.387 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n t.go:81: 2023-03-29 13:37:27.387 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.358191Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.379813Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": []}}\n t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.379813Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": []}}\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n t.go:81: 2023-03-29 13:37:27.441 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:58992 derp=1 derpdist=1v4:62ms\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:58992 (stun), 172.20.0.2:58992 (local)\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.514434952 +0000 UTC m=+4.154042177 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:0}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.514515Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.514515Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.515008199 +0000 UTC m=+4.154615430 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:1}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.062405899}}}\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.515155Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.062405899}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.515155Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.062405899}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.516 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:34848 derp=1 derpdist=1v4:65ms\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:34848 (stun), 172.20.0.2:34848 (local)\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.525526859 +0000 UTC m=+4.165134074 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:0}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.525972278 +0000 UTC m=+4.165579492 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:1}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.065420657}}}\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.5256Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.5256Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.526766Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.065420657}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.526766Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.065420657}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n t.go:81: 2023-03-29 13:37:27.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:58992 derp=1 derpdist=1v4:5ms\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:34848 derp=1 derpdist=1v4:7ms\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.005291379}}}\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.578687Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.005291379}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.578687Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.005291379}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.00675754}}}\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.579606Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.00675754}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.579606Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.00675754}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n t.go:81: 2023-03-29 13:37:27.624 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n t.go:81: 2023-03-29 13:37:27.625 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [5WitN] set to derp-1 (shared home)\n t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [5EOvJ] set to derp-1 (shared home)\n t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5WitN] d:34ff526bdd502e84 now using 172.20.0.2:34848\n t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n t.go:81: 2023-03-29 13:37:27.627 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Created\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Updating endpoint\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Removing all allowedips\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Adding allowedip\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Updating persistent keepalive interval\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Starting\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Sending handshake initiation\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [5EOvJ] now active, reconfiguring WireGuard\n t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Created\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Updating endpoint\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Removing all allowedips\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Adding allowedip\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Updating persistent keepalive interval\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Starting\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Received handshake initiation\n t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Sending handshake response\n t.go:81: 2023-03-29 13:37:27.630 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.630405833 +0000 UTC m=+4.270013057 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c}] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:1}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Received handshake response\n t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5EOvJ] d:049e454260a62aa1 now using 172.20.0.2:58992\n t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.631481797 +0000 UTC m=+4.271089021 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.631305354 +0000 UTC NodeKey:nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f}] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:1}\", \"err\": null}\n t.go:81: 2023-03-29 13:37:27.632 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5WitN] d:34ff526bdd502e84 now using 127.0.0.1:34848\n agent_test.go:400: \n \tError Trace:\t/home/mafredri/src/coder/coder/agent/agent_test.go:400\n \t \t\t\t\t/home/mafredri/src/coder/coder/agent/agent_test.go:401\n \tError: \t\"\" does not contain \"wazzup\"\n \tTest: \tTestAgent_Session_TTY_FastCommandHasOutput\n \tMessages: \tshould output greeting\n ptytest.go:83: 2023-03-29 13:37:27.648: cmd: closing tpty: close\n ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing pty\n ptytest.go:110: 2023-03-29 13:37:27.648: cmd: copy done: read /dev/ptmx: file already closed\n ptytest.go:111: 2023-03-29 13:37:27.648: cmd: closing out\n ptytest.go:113: 2023-03-29 13:37:27.648: cmd: closed out: read /dev/ptmx: file already closed\n ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed pty: \u003cnil\u003e\n ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing logw\n ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed logw: \u003cnil\u003e\n ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing logr\n ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed logr: \u003cnil\u003e\n ptytest.go:102: 2023-03-29 13:37:27.648: cmd: closed tpty\n t.go:81: 2023-03-29 13:37:27.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n t.go:81: 2023-03-29 13:37:27.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Stopping\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n t.go:81: 2023-03-29 13:37:27.649 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d): sending disco ping to [5EOvJ] ...\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Stopping\n t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n stuntest.go:63: STUN server shutdown\n--- FAIL: TestAgent_Session_TTY_FastCommandHasOutput (0.95s)\n"
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_Session_TTY_HugeOutputIsNotLost",
"time": 0,
"skip": true
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_Session_TTY_Hushlogin",
"time": 1.69
},
{
- "package": "agent",
+ "package": "v2/agent",
"name": "TestAgent_Session_TTY_MOTD",
"time": 1.62
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer",
"time": 0.05,
"fail": true,
"output": "=== RUN TestServer\n--- FAIL: TestServer (0.05s)\n"
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/BuiltinPostgres",
"time": 5.17
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/BuiltinPostgresURL",
"time": 0.11
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/BuiltinPostgresURLRaw",
"time": 0.11
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/CanListenUnspecifiedv4",
"time": 0.6
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/CanListenUnspecifiedv6",
"time": 0.63
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/DeprecatedAddress",
"time": 0.06
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/DeprecatedAddress/HTTP",
"time": 1.01
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/DeprecatedAddress/TLS",
"time": 0.5
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/GitHubOAuth",
"time": 1.01
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/LocalAccessURL",
"time": 0.71
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Logging",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Logging/CreatesFile",
"time": 0.69
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Logging/Human",
"time": 0.7
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Logging/JSON",
"time": 0.67
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Logging/Multiple",
"time": 13.24
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Logging/Stackdriver",
"time": 26.23
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/NoAddress",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/NoSchemeAccessURL",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/NoTLSAddress",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/NoWarningWithRemoteAccessURL",
"time": 0.72
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Production",
"time": 0,
"fail": true,
- "output": "=== RUN TestServer/Production\n server_test.go:109: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_test.go:109\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/cli_test.TestServer.func1\n \t \t \t/home/mafredri/src/coder/coder/cli/server_test.go:108\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServer/Production\n--- FAIL: TestServer/Production (0.00s)\n"
+ "output": "=== RUN TestServer/Production\n server_test.go:109: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_test.go:109\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/v2/cli_test.TestServer.func1\n \t \t \t/home/mafredri/src/coder/coder/cli/server_test.go:108\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServer/Production\n--- FAIL: TestServer/Production (0.00s)\n"
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Prometheus",
"time": 1
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/RateLimit",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/RateLimit/Changed",
"time": 1.07
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/RateLimit/Default",
"time": 0.85
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/RateLimit/Disabled",
"time": 1.02
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/RemoteAccessURL",
"time": 0.92
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Shutdown",
"time": 0.05
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSAndHTTP",
"time": 1.24
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSBadClientAuth",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSBadVersion",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSInvalid",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSInvalid/MismatchedCertAndKey",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSInvalid/MismatchedCount",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSInvalid/NoCert",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSInvalid/NoKey",
"time": 0
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSRedirect",
"time": 0.05
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSRedirect/NoHTTPListener",
"time": 0.62
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSRedirect/NoRedirect",
"time": 0.77
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSRedirect/NoRedirectWithWildcard",
"time": 0.47
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSRedirect/NoTLSListener",
"time": 0.59
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSRedirect/OK",
"time": 1.06
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSValid",
"time": 1.09
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TLSValidMultiple",
"time": 1.23
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/Telemetry",
"time": 1.09
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServer/TracerNoLeak",
"time": 0.75
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServerCreateAdminUser",
"time": 0,
"fail": true,
"output": "=== RUN TestServerCreateAdminUser\n--- FAIL: TestServerCreateAdminUser (0.00s)\n"
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServerCreateAdminUser/Env",
"time": 0,
"fail": true,
- "output": "=== RUN TestServerCreateAdminUser/Env\n=== PAUSE TestServerCreateAdminUser/Env\n=== CONT TestServerCreateAdminUser/Env\n server_createadminuser_test.go:153: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:153\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/cli_test.TestServerCreateAdminUser.func3\n \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:152\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServerCreateAdminUser/Env\n--- FAIL: TestServerCreateAdminUser/Env (0.00s)\n"
+ "output": "=== RUN TestServerCreateAdminUser/Env\n=== PAUSE TestServerCreateAdminUser/Env\n=== CONT TestServerCreateAdminUser/Env\n server_createadminuser_test.go:153: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:153\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/v2/cli_test.TestServerCreateAdminUser.func3\n \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:152\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServerCreateAdminUser/Env\n--- FAIL: TestServerCreateAdminUser/Env (0.00s)\n"
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServerCreateAdminUser/OK",
"time": 0,
"fail": true,
- "output": "=== RUN TestServerCreateAdminUser/OK\n=== PAUSE TestServerCreateAdminUser/OK\n=== CONT TestServerCreateAdminUser/OK\n server_createadminuser_test.go:87: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:87\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/cli_test.TestServerCreateAdminUser.func2\n \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:86\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServerCreateAdminUser/OK\n--- FAIL: TestServerCreateAdminUser/OK (0.00s)\n"
+ "output": "=== RUN TestServerCreateAdminUser/OK\n=== PAUSE TestServerCreateAdminUser/OK\n=== CONT TestServerCreateAdminUser/OK\n server_createadminuser_test.go:87: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:87\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/v2/cli_test.TestServerCreateAdminUser.func2\n \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:86\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServerCreateAdminUser/OK\n--- FAIL: TestServerCreateAdminUser/OK (0.00s)\n"
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServerCreateAdminUser/Stdin",
"time": 0,
"fail": true,
- "output": "=== RUN TestServerCreateAdminUser/Stdin\n=== PAUSE TestServerCreateAdminUser/Stdin\n=== CONT TestServerCreateAdminUser/Stdin\n server_createadminuser_test.go:187: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:187\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/cli_test.TestServerCreateAdminUser.func4\n \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:186\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServerCreateAdminUser/Stdin\n--- FAIL: TestServerCreateAdminUser/Stdin (0.00s)\n"
+ "output": "=== RUN TestServerCreateAdminUser/Stdin\n=== PAUSE TestServerCreateAdminUser/Stdin\n=== CONT TestServerCreateAdminUser/Stdin\n server_createadminuser_test.go:187: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:187\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/v2/cli_test.TestServerCreateAdminUser.func4\n \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:186\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServerCreateAdminUser/Stdin\n--- FAIL: TestServerCreateAdminUser/Stdin (0.00s)\n"
},
{
- "package": "cli",
+ "package": "v2/cli",
"name": "TestServerCreateAdminUser/Validates",
"time": 0,
"fail": true,
- "output": "=== RUN TestServerCreateAdminUser/Validates\n=== PAUSE TestServerCreateAdminUser/Validates\n=== CONT TestServerCreateAdminUser/Validates\n server_createadminuser_test.go:227: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:227\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/cli_test.TestServerCreateAdminUser.func5\n \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:226\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServerCreateAdminUser/Validates\n--- FAIL: TestServerCreateAdminUser/Validates (0.00s)\n"
+ "output": "=== RUN TestServerCreateAdminUser/Validates\n=== PAUSE TestServerCreateAdminUser/Validates\n=== CONT TestServerCreateAdminUser/Validates\n server_createadminuser_test.go:227: \n \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:227\n \tError: \tReceived unexpected error:\n \t \tcould not start resource:\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n \t \t \n \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n \t \t github.com/coder/coder/v2/cli_test.TestServerCreateAdminUser.func5\n \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:226\n \t \t testing.tRunner\n \t \t \t/usr/local/go/src/testing/testing.go:1576\n \t \t runtime.goexit\n \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n \tTest: \tTestServerCreateAdminUser/Validates\n--- FAIL: TestServerCreateAdminUser/Validates (0.00s)\n"
},
{
- "package": "cli/cliui",
+ "package": "v2/cli/cliui",
"name": "TestGitAuth",
"time": 0
},
{
- "package": "cli/cliui",
+ "package": "v2/cli/cliui",
"name": "TestPrompt",
"time": 0
},
{
- "package": "cli/cliui",
+ "package": "v2/cli/cliui",
"name": "TestPrompt/BadJSON",
"time": 0
},
{
- "package": "cli/cliui",
+ "package": "v2/cli/cliui",
"name": "TestPrompt/Confirm",
"time": 0
},
{
- "package": "cli/cliui",
+ "package": "v2/cli/cliui",
"name": "TestPrompt/JSON",
"time": 0
},
{
- "package": "cli/cliui",
+ "package": "v2/cli/cliui",
"name": "TestPrompt/MultilineJSON",
"time": 0
},
{
- "package": "cli/cliui",
+ "package": "v2/cli/cliui",
"name": "TestPrompt/Skip",
"time": 0
},
{
- "package": "cli/cliui",
+ "package": "v2/cli/cliui",
"name": "TestPrompt/Success",
"time": 0
}
diff --git a/scripts/ci-report/testdata/gotests-timeout.json.sample b/scripts/ci-report/testdata/gotests-timeout.json.sample
index 95e809612434d..d13c9dc37199c 100644
--- a/scripts/ci-report/testdata/gotests-timeout.json.sample
+++ b/scripts/ci-report/testdata/gotests-timeout.json.sample
@@ -1,382 +1,382 @@
-{"Time":"2023-03-29T13:59:30.419140864Z","Action":"start","Package":"github.com/coder/coder/agent"}
-{"Time":"2023-03-29T13:59:30.440137227Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec"}
-{"Time":"2023-03-29T13:59:30.440225617Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":"=== RUN TestAgent_SessionExec\n"}
-{"Time":"2023-03-29T13:59:30.440252351Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":"=== PAUSE TestAgent_SessionExec\n"}
-{"Time":"2023-03-29T13:59:30.440264139Z","Action":"pause","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec"}
-{"Time":"2023-03-29T13:59:30.44029211Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell"}
-{"Time":"2023-03-29T13:59:30.440307898Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":"=== RUN TestAgent_SessionTTYShell\n"}
-{"Time":"2023-03-29T13:59:30.440330948Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":"=== PAUSE TestAgent_SessionTTYShell\n"}
-{"Time":"2023-03-29T13:59:30.440340646Z","Action":"pause","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell"}
-{"Time":"2023-03-29T13:59:30.440351592Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode"}
-{"Time":"2023-03-29T13:59:30.440360503Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== RUN TestAgent_SessionTTYExitCode\n"}
-{"Time":"2023-03-29T13:59:30.440373253Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== PAUSE TestAgent_SessionTTYExitCode\n"}
-{"Time":"2023-03-29T13:59:30.440389091Z","Action":"pause","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode"}
-{"Time":"2023-03-29T13:59:30.440406592Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD"}
-{"Time":"2023-03-29T13:59:30.440417518Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":"=== RUN TestAgent_Session_TTY_MOTD\n"}
-{"Time":"2023-03-29T13:59:30.68885571Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:59:30.688902548Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:59:30.688936919Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:59:30.688952573Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:59:30.688978288Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:59:30.689138933Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:59:30.689169612Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:59:30.689278237Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:59:30.689311927Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:59:30.689422904Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:59:30.689462324Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:59:30.689635363Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:1ad81e5115245108\n"}
-{"Time":"2023-03-29T13:59:30.689668719Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:59:30.689762323Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:59:30.689824046Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:59:30.689876569Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:59:30.689906309Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:59:30.689964141Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:59:30.690006177Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:59:30.690054052Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:59:30.690100827Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:59:30.690166644Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:59:30.690333879Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:59:30.69067189Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.690716053Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:59:30.690874768Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:59:30.690920653Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
-{"Time":"2023-03-29T13:59:30.691236077Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 55526, \"DERPPort\": 33325, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"/tmp/TestAgent_Session_TTY_MOTD2921078/001/motd\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
-{"Time":"2023-03-29T13:59:30.691266926Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
-{"Time":"2023-03-29T13:59:30.691645376Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:59:30.691681569Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:59:30.691697309Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:59:30.691834882Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:59:30.691894444Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:59:30.691990111Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:59:30.692037682Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:59:30.692117014Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:59:30.69217036Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:59:30.692223588Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:59:30.692269654Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:59:30.692436067Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:19af0f91ddbc2673\n"}
-{"Time":"2023-03-29T13:59:30.692486153Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:59:30.692600638Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:59:30.692643998Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:59:30.692706838Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:59:30.692750609Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:59:30.692799088Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:59:30.692845724Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:59:30.692892868Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:59:30.692943768Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:59:30.692994617Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:59:30.693130711Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:59:30.693421144Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.693467015Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:59:30.693571784Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
-{"Time":"2023-03-29T13:59:30.693599238Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
-{"Time":"2023-03-29T13:59:30.693798141Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.693596Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:59:30.699379222Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.699 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:59:30.702070777Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.701 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:59:30.702521256Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.702 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:59:30.705825444Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.705 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.690745Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:59:30.705969348Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.705 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:59:30.706047452Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.705 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:59:30.706117981Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.706 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:59:30.706167259Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.706 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:59:30.70672858Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.706 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:59:30.707193213Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.707 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:59:30.707713648Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.707 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:59:30.711327936Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.693596Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:59:30.711351236Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.711484437Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\n"}
-{"Time":"2023-03-29T13:59:30.711757724Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:59:30.711895059Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:59:30.712001206Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:59:30.712012787Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:59:30.712044402Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:59:30.712080254Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:59:30.712196969Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:59:30.712224408Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:59:30.712251498Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:59:30.713321731Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.690745Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:59:30.713345626Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.713438727Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
-{"Time":"2023-03-29T13:59:30.713542134Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:59:30.713653049Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:59:30.713794466Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:59:30.713831747Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:59:30.713873796Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:59:30.71391978Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:59:30.713976575Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:59:30.71401037Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:59:30.714059091Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.714 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:59:30.762911536Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.762 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:40423 derp=1 derpdist=1v4:2ms\n"}
-{"Time":"2023-03-29T13:59:30.763010472Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.762 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:59:30.763604178Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.763 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:40423 (stun), 172.20.0.2:40423 (local)\n"}
-{"Time":"2023-03-29T13:59:30.764002966Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.763 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.76358648 +0000 UTC m=+0.341643633 Peers:[] LocalAddrs:[{Addr:127.0.0.1:40423 Type:stun} {Addr:172.20.0.2:40423 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:59:30.764243833Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.764 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:51077 derp=1 derpdist=1v4:1ms\n"}
-{"Time":"2023-03-29T13:59:30.764368314Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.764 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:59:30.764813395Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.764 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:51077 (stun), 172.20.0.2:51077 (local)\n"}
-{"Time":"2023-03-29T13:59:30.765032404Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.764 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.764800711 +0000 UTC m=+0.342857833 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51077 Type:stun} {Addr:172.20.0.2:51077 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:59:30.766066288Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.765 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:59:30.766178166Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.766 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:59:30.766459711Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.766 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.766177132 +0000 UTC m=+0.344234254 Peers:[] LocalAddrs:[{Addr:127.0.0.1:40423 Type:stun} {Addr:172.20.0.2:40423 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:59:30.766803271Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.766 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.001851767}}}\n"}
-{"Time":"2023-03-29T13:59:30.767031778Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.766 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.763989Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.767745656Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.767 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.763989Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.767803589Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.767 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.7681905Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.767 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:59:30.769269486Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:59:30.769423531Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:59:30.76964587Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:59:30.76985461Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.001439212}}}\n"}
-{"Time":"2023-03-29T13:59:30.770057617Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.765018Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.770543054Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.770 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.765018Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.770576926Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.770 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.770805865Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.770 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:59:30.771214469Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.771 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:59:30.771672983Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.771 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.77151Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001851767}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.77196162Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.771 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.77151Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001851767}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.772249568Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.772286673Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:59:30.772417712Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:59:30.772485685Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:59:30.772763889Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:59:30.772990304Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.772846286 +0000 UTC m=+0.350903351 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51077 Type:stun} {Addr:172.20.0.2:51077 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:59:30.77305219Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.772974Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001439212}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.773283722Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.772974Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001439212}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.773486653Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.773528203Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:59:30.773629095Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:59:30.773696573Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:59:30.781245292Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.781 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:59:30.781769525Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.781 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:59:30.821304727Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.821 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:51077 derp=1 derpdist=1v4:4ms\n"}
-{"Time":"2023-03-29T13:59:30.823014931Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.822 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:40423 derp=1 derpdist=1v4:1ms\n"}
-{"Time":"2023-03-29T13:59:30.842118323Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [tUb7c] ...\n"}
-{"Time":"2023-03-29T13:59:30.842325421Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [tUb7c] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:59:30.842743216Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [s78jr] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:59:30.842835058Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [tUb7c] d:19af0f91ddbc2673 now using 172.20.0.2:51077\n"}
-{"Time":"2023-03-29T13:59:30.842945452Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [tUb7c] ...\n"}
-{"Time":"2023-03-29T13:59:30.843017307Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [tUb7c] ...\n"}
-{"Time":"2023-03-29T13:59:30.843455848Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:59:30.843724102Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:59:30.843761613Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:59:30.843824129Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:59:30.843866119Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:59:30.843967657Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:59:30.843978436Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Starting\n"}
-{"Time":"2023-03-29T13:59:30.844024904Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Sending handshake initiation\n"}
-{"Time":"2023-03-29T13:59:30.844361886Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [s78jr] now active, reconfiguring WireGuard\n"}
-{"Time":"2023-03-29T13:59:30.84441357Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:59:30.844612597Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:59:30.844652368Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:59:30.844692139Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:59:30.844742198Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:59:30.844790446Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:59:30.844839046Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Starting\n"}
-{"Time":"2023-03-29T13:59:30.845054904Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Received handshake initiation\n"}
-{"Time":"2023-03-29T13:59:30.845091562Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.845 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Sending handshake response\n"}
-{"Time":"2023-03-29T13:59:30.845575288Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.845 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.845411293 +0000 UTC m=+0.423468375 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359}] LocalAddrs:[{Addr:127.0.0.1:51077 Type:stun} {Addr:172.20.0.2:51077 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:59:30.846073274Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.846 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Received handshake response\n"}
-{"Time":"2023-03-29T13:59:30.846152172Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.846 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [s78jr] d:1ad81e5115245108 now using 172.20.0.2:40423\n"}
-{"Time":"2023-03-29T13:59:30.846323335Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.846 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.846216257 +0000 UTC m=+0.424273332 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:59:30.846070709 +0000 UTC NodeKey:nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b}] LocalAddrs:[{Addr:127.0.0.1:40423 Type:stun} {Addr:172.20.0.2:40423 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:59:30.846820565Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.846 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [tUb7c] d:19af0f91ddbc2673 now using 127.0.0.1:51077\n"}
-{"Time":"2023-03-29T13:59:30.872225511Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" agent_test.go:298: 2023-03-29 13:59:30.872: cmd: stdin: \"exit 0\\r\"\n"}
-{"Time":"2023-03-29T13:59:30.872911108Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.872 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:51077 derp=1 derpdist=1v4:1ms\n"}
-{"Time":"2023-03-29T13:59:30.873619058Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.873 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000537347}}}\n"}
-{"Time":"2023-03-29T13:59:30.873901772Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.873 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.873622Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000537347}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.874560964Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.874 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:40423 derp=1 derpdist=1v4:0s\n"}
-{"Time":"2023-03-29T13:59:30.875202519Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.874 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.873622Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000537347}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.875651396Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.875 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.0001832}}}\n"}
-{"Time":"2023-03-29T13:59:30.875737105Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.875 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.875642Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.0001832}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.876040926Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.875 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.875642Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.0001832}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
-{"Time":"2023-03-29T13:59:30.876278217Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.8763513Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:59:30.87651371Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:59:30.876622881Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:59:30.876654135Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Sending keepalive packet\n"}
-{"Time":"2023-03-29T13:59:30.876695566Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:59:30.876878296Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:59:30.876944605Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:59:30.877050858Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:59:30.877194231Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:59:30.877218701Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Sending keepalive packet\n"}
-{"Time":"2023-03-29T13:59:30.877269173Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:59:30.877353148Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:121: 2023-03-29 13:59:30.877: cmd: \"exit 0\"\n"}
-{"Time":"2023-03-29T13:59:30.877482007Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Receiving keepalive packet\n"}
-{"Time":"2023-03-29T13:59:30.877522599Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Receiving keepalive packet\n"}
-{"Time":"2023-03-29T13:59:31.212200169Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:31.212 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8): sending disco ping to [s78jr] ...\n"}
-{"Time":"2023-03-29T13:59:31.711519305Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:31.711 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8): sending disco ping to [s78jr] ...\n"}
-{"Time":"2023-03-29T13:59:32.2114982Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.211 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8): sending disco ping to [s78jr] ...\n"}
-{"Time":"2023-03-29T13:59:32.277108499Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:83: 2023-03-29 13:59:32.277: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:59:32.277144197Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:59:32.277: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:59:32.277157734Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:110: 2023-03-29 13:59:32.277: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:59:32.277164721Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:111: 2023-03-29 13:59:32.277: cmd: closing out\n"}
-{"Time":"2023-03-29T13:59:32.2771729Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:113: 2023-03-29 13:59:32.277: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:59:32.277306699Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:59:32.277: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:59:32.277350733Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:59:32.277: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:59:32.277374575Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:59:32.277: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:59:32.277394938Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:59:32.277: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:59:32.277415507Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:59:32.277: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:59:32.277434743Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:102: 2023-03-29 13:59:32.277: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:59:32.277579778Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
-{"Time":"2023-03-29T13:59:32.277611563Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 2s\n"}
-{"Time":"2023-03-29T13:59:32.277638668Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:59:32.277693383Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:59:32.277732009Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:59:32.277947928Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:59:32.277973674Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:59:32.278017899Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:59:32.278057207Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Stopping\n"}
-{"Time":"2023-03-29T13:59:32.278134475Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:59:32.278241902Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:59:32.278284628Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
-{"Time":"2023-03-29T13:59:32.278337186Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
-{"Time":"2023-03-29T13:59:32.27868397Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 2s\n"}
-{"Time":"2023-03-29T13:59:32.278698095Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:59:32.278769886Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:59:32.278808018Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:59:32.278879735Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:59:32.278965521Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8): sending disco ping to [s78jr] ...\n"}
-{"Time":"2023-03-29T13:59:32.279089367Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.279 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:59:32.279122263Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.279 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:59:32.279168949Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.279 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Stopping\n"}
-{"Time":"2023-03-29T13:59:32.279275932Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.279 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:59:32.279696417Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" stuntest.go:63: STUN server shutdown\n"}
-{"Time":"2023-03-29T13:59:32.279872392Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":"--- PASS: TestAgent_Session_TTY_MOTD (1.84s)\n"}
-{"Time":"2023-03-29T13:59:32.279886475Z","Action":"pass","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Elapsed":1.84}
-{"Time":"2023-03-29T13:59:32.27989796Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin"}
-{"Time":"2023-03-29T13:59:32.279902938Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"=== RUN TestAgent_Session_TTY_Hushlogin\n"}
-{"Time":"2023-03-29T13:59:32.457943185Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"panic: test timed out after 2s\n"}
-{"Time":"2023-03-29T13:59:32.45796911Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"running tests:\n"}
-{"Time":"2023-03-29T13:59:32.457977422Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\tTestAgent_Session_TTY_Hushlogin (0s)\n"}
-{"Time":"2023-03-29T13:59:32.457982677Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
-{"Time":"2023-03-29T13:59:32.457987323Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 411 [running]:\n"}
-{"Time":"2023-03-29T13:59:32.457991361Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*M).startAlarm.func1()\n"}
-{"Time":"2023-03-29T13:59:32.457995716Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:2241 +0x3b9\n"}
-{"Time":"2023-03-29T13:59:32.4580029Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by time.goFunc\n"}
-{"Time":"2023-03-29T13:59:32.458007365Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/time/sleep.go:176 +0x32\n"}
-{"Time":"2023-03-29T13:59:32.45801203Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
-{"Time":"2023-03-29T13:59:32.45801607Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 1 [chan receive]:\n"}
-{"Time":"2023-03-29T13:59:32.458020121Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*T).Run(0xc0004e1040, {0x16a5e92?, 0x535fa5?}, 0x17462c0)\n"}
-{"Time":"2023-03-29T13:59:32.458024235Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1630 +0x405\n"}
-{"Time":"2023-03-29T13:59:32.458028654Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.runTests.func1(0x236db60?)\n"}
-{"Time":"2023-03-29T13:59:32.45803313Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:2036 +0x45\n"}
-{"Time":"2023-03-29T13:59:32.458037122Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc0004e1040, 0xc000589bb8)\n"}
-{"Time":"2023-03-29T13:59:32.458041079Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
-{"Time":"2023-03-29T13:59:32.45804729Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.runTests(0xc000341a40?, {0x235c580, 0x21, 0x21}, {0x4182d0?, 0xc000589c78?, 0x236cb40?})\n"}
-{"Time":"2023-03-29T13:59:32.458051787Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:2034 +0x489\n"}
-{"Time":"2023-03-29T13:59:32.458055836Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*M).Run(0xc000341a40)\n"}
-{"Time":"2023-03-29T13:59:32.458059703Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1906 +0x63a\n"}
-{"Time":"2023-03-29T13:59:32.458066931Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"go.uber.org/goleak.VerifyTestMain({0x18f5540?, 0xc000341a40?}, {0x0, 0x0, 0x0})\n"}
-{"Time":"2023-03-29T13:59:32.458071333Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/.local/go/pkg/mod/go.uber.org/goleak@v1.2.1/testmain.go:53 +0x6b\n"}
-{"Time":"2023-03-29T13:59:32.458075235Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/agent_test.TestMain(...)\n"}
-{"Time":"2023-03-29T13:59:32.458081052Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:53\n"}
-{"Time":"2023-03-29T13:59:32.45808506Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"main.main()\n"}
-{"Time":"2023-03-29T13:59:32.458088903Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t_testmain.go:115 +0x1e5\n"}
-{"Time":"2023-03-29T13:59:32.458094468Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
-{"Time":"2023-03-29T13:59:32.458102106Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 9 [chan receive]:\n"}
-{"Time":"2023-03-29T13:59:32.45810621Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*T).Parallel(0xc0004e11e0)\n"}
-{"Time":"2023-03-29T13:59:32.45811231Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1384 +0x225\n"}
-{"Time":"2023-03-29T13:59:32.458116485Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/agent_test.TestAgent_SessionExec(0x0?)\n"}
-{"Time":"2023-03-29T13:59:32.458120426Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:188 +0x33\n"}
-{"Time":"2023-03-29T13:59:32.458126141Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc0004e11e0, 0x1746298)\n"}
-{"Time":"2023-03-29T13:59:32.458130428Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
-{"Time":"2023-03-29T13:59:32.458135189Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by testing.(*T).Run\n"}
-{"Time":"2023-03-29T13:59:32.458144082Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n"}
-{"Time":"2023-03-29T13:59:32.458150718Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
-{"Time":"2023-03-29T13:59:32.458156738Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 10 [chan receive]:\n"}
-{"Time":"2023-03-29T13:59:32.458165162Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*T).Parallel(0xc0004e1520)\n"}
-{"Time":"2023-03-29T13:59:32.458171886Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1384 +0x225\n"}
-{"Time":"2023-03-29T13:59:32.458178556Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/agent_test.TestAgent_SessionTTYShell(0xc0004e1520)\n"}
-{"Time":"2023-03-29T13:59:32.458182782Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:213 +0x36\n"}
-{"Time":"2023-03-29T13:59:32.458188646Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc0004e1520, 0x17462a8)\n"}
-{"Time":"2023-03-29T13:59:32.458192664Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
-{"Time":"2023-03-29T13:59:32.458196426Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by testing.(*T).Run\n"}
-{"Time":"2023-03-29T13:59:32.458200247Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n"}
-{"Time":"2023-03-29T13:59:32.458204698Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
-{"Time":"2023-03-29T13:59:32.458208395Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 11 [chan receive]:\n"}
-{"Time":"2023-03-29T13:59:32.458215562Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*T).Parallel(0xc0004e1860)\n"}
-{"Time":"2023-03-29T13:59:32.458219487Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1384 +0x225\n"}
-{"Time":"2023-03-29T13:59:32.458223369Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/agent_test.TestAgent_SessionTTYExitCode(0xc0004e1520?)\n"}
-{"Time":"2023-03-29T13:59:32.458228633Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:244 +0x36\n"}
-{"Time":"2023-03-29T13:59:32.458234377Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc0004e1860, 0x17462a0)\n"}
-{"Time":"2023-03-29T13:59:32.458238258Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
-{"Time":"2023-03-29T13:59:32.458242179Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by testing.(*T).Run\n"}
-{"Time":"2023-03-29T13:59:32.458246008Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n"}
-{"Time":"2023-03-29T13:59:32.458249635Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
-{"Time":"2023-03-29T13:59:32.45825516Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 408 [runnable]:\n"}
-{"Time":"2023-03-29T13:59:32.458282465Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.nat.montgomery({0xc004aa4500?, 0x10?, 0x26?}, {0xc004aa4280?, 0x10?, 0x26?}, {0xc004aa4280?, 0x10?, 0x26?}, {0xc000732820, ...}, ...)\n"}
-{"Time":"2023-03-29T13:59:32.458291741Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/nat.go:216 +0x565\n"}
-{"Time":"2023-03-29T13:59:32.458330298Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.nat.expNNMontgomery({0xc004aa4280, 0xc0003c2e70?, 0x26}, {0xc004a9adc0?, 0x21?, 0x24?}, {0xc004a9ac80, 0x10, 0x24?}, {0xc000732820, ...})\n"}
-{"Time":"2023-03-29T13:59:32.458336764Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/nat.go:1271 +0xb1c\n"}
-{"Time":"2023-03-29T13:59:32.458384114Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.nat.expNN({0xc004aa4280?, 0x14?, 0x22c2900?}, {0xc004a9adc0?, 0x10, 0x14}, {0xc004a9ac80?, 0x10, 0x14?}, {0xc000732820, ...}, ...)\n"}
-{"Time":"2023-03-29T13:59:32.458393699Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/nat.go:996 +0x3b1\n"}
-{"Time":"2023-03-29T13:59:32.458403406Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.nat.probablyPrimeMillerRabin({0xc000732820?, 0x10, 0x14}, 0x15, 0x1)\n"}
-{"Time":"2023-03-29T13:59:32.458413208Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/prime.go:106 +0x5b8\n"}
-{"Time":"2023-03-29T13:59:32.458420936Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.(*Int).ProbablyPrime(0xc0047208c0, 0x14)\n"}
-{"Time":"2023-03-29T13:59:32.458424869Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/prime.go:78 +0x225\n"}
-{"Time":"2023-03-29T13:59:32.458430384Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"crypto/rand.Prime({0x18f04c0, 0xc00007e020}, 0x400)\n"}
-{"Time":"2023-03-29T13:59:32.45843919Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/crypto/rand/util.go:55 +0x1e5\n"}
-{"Time":"2023-03-29T13:59:32.45845888Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"crypto/rsa.GenerateMultiPrimeKey({0x18f04c0, 0xc00007e020}, 0x2, 0x800)\n"}
-{"Time":"2023-03-29T13:59:32.458469074Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/crypto/rsa/rsa.go:369 +0x745\n"}
-{"Time":"2023-03-29T13:59:32.458474102Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"crypto/rsa.GenerateKey(...)\n"}
-{"Time":"2023-03-29T13:59:32.45847985Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/crypto/rsa/rsa.go:264\n"}
-{"Time":"2023-03-29T13:59:32.458483767Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/agent.(*agent).init(0xc00485eea0, {0x1902c20?, 0xc00485d770})\n"}
-{"Time":"2023-03-29T13:59:32.458489397Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent.go:810 +0x6c\n"}
-{"Time":"2023-03-29T13:59:32.458522193Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/agent.New({{0x190cbc0, 0xc0005b7710}, {0x166d829, 0x4}, {0x166d829, 0x4}, 0x17461d8, {0x1907c90, 0xc000278280}, 0x45d964b800, ...})\n"}
-{"Time":"2023-03-29T13:59:32.458531042Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent.go:134 +0x549\n"}
-{"Time":"2023-03-29T13:59:32.458565925Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/agent_test.setupAgent(0xc00485eb60, {0x0, {0x0, 0x0}, {0x0, 0x0, 0x0}, 0xc0005b8da0, 0x0, {0x0, ...}, ...}, ...)\n"}
-{"Time":"2023-03-29T13:59:32.458576217Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:1568 +0x63e\n"}
-{"Time":"2023-03-29T13:59:32.458605192Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/agent_test.setupSSHSession(0xc00485eb60, {0x0, {0x0, 0x0}, {0x0, 0x0, 0x0}, 0x0, 0x0, {0x0, ...}, ...})\n"}
-{"Time":"2023-03-29T13:59:32.458614801Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:1524 +0xc5\n"}
-{"Time":"2023-03-29T13:59:32.458625161Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/agent_test.TestAgent_Session_TTY_Hushlogin(0xc00485eb60)\n"}
-{"Time":"2023-03-29T13:59:32.458630015Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:330 +0x2fa\n"}
-{"Time":"2023-03-29T13:59:32.458635887Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc00485eb60, 0x17462c0)\n"}
-{"Time":"2023-03-29T13:59:32.458639744Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
-{"Time":"2023-03-29T13:59:32.458643595Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by testing.(*T).Run\n"}
-{"Time":"2023-03-29T13:59:32.458649393Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n"}
-{"Time":"2023-03-29T13:59:32.458653156Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
-{"Time":"2023-03-29T13:59:32.458657314Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 409 [IO wait]:\n"}
-{"Time":"2023-03-29T13:59:32.458662763Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.runtime_pollWait(0x7f5230766628, 0x72)\n"}
-{"Time":"2023-03-29T13:59:32.458668522Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/runtime/netpoll.go:306 +0x89\n"}
-{"Time":"2023-03-29T13:59:32.45867585Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*pollDesc).wait(0xc00475bf80?, 0xc0005ec5e2?, 0x0)\n"}
-{"Time":"2023-03-29T13:59:32.458681918Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x32\n"}
-{"Time":"2023-03-29T13:59:32.458688307Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*pollDesc).waitRead(...)\n"}
-{"Time":"2023-03-29T13:59:32.458708219Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\n"}
-{"Time":"2023-03-29T13:59:32.458712856Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*FD).Accept(0xc00475bf80)\n"}
-{"Time":"2023-03-29T13:59:32.458717364Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_unix.go:614 +0x2bd\n"}
-{"Time":"2023-03-29T13:59:32.458721204Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*netFD).accept(0xc00475bf80)\n"}
-{"Time":"2023-03-29T13:59:32.458727021Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/fd_unix.go:172 +0x35\n"}
-{"Time":"2023-03-29T13:59:32.458731155Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*TCPListener).accept(0xc00486ecd8)\n"}
-{"Time":"2023-03-29T13:59:32.458737943Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/tcpsock_posix.go:148 +0x25\n"}
-{"Time":"2023-03-29T13:59:32.458744974Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*TCPListener).Accept(0xc00486ecd8)\n"}
-{"Time":"2023-03-29T13:59:32.458752245Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/tcpsock.go:297 +0x3d\n"}
-{"Time":"2023-03-29T13:59:32.458758039Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"crypto/tls.(*listener).Accept(0xc00486ef18)\n"}
-{"Time":"2023-03-29T13:59:32.458763674Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/crypto/tls/tls.go:66 +0x2d\n"}
-{"Time":"2023-03-29T13:59:32.458770104Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net/http.(*Server).Serve(0xc00029da40, {0x18fefa0, 0xc00486ef18})\n"}
-{"Time":"2023-03-29T13:59:32.458778229Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/http/server.go:3059 +0x385\n"}
-{"Time":"2023-03-29T13:59:32.45878539Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net/http/httptest.(*Server).goServe.func1()\n"}
-{"Time":"2023-03-29T13:59:32.458793477Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/http/httptest/server.go:310 +0x6a\n"}
-{"Time":"2023-03-29T13:59:32.458797511Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by net/http/httptest.(*Server).goServe\n"}
-{"Time":"2023-03-29T13:59:32.458801374Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/http/httptest/server.go:308 +0x6a\n"}
-{"Time":"2023-03-29T13:59:32.458805093Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
-{"Time":"2023-03-29T13:59:32.458810472Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 410 [IO wait]:\n"}
-{"Time":"2023-03-29T13:59:32.458814744Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.runtime_pollWait(0x7f5230765908, 0x72)\n"}
-{"Time":"2023-03-29T13:59:32.458819038Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/runtime/netpoll.go:306 +0x89\n"}
-{"Time":"2023-03-29T13:59:32.458824589Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*pollDesc).wait(0xc00043a300?, 0xc004880000?, 0x0)\n"}
-{"Time":"2023-03-29T13:59:32.458830327Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x32\n"}
-{"Time":"2023-03-29T13:59:32.458835463Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*pollDesc).waitRead(...)\n"}
-{"Time":"2023-03-29T13:59:32.458840944Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\n"}
-{"Time":"2023-03-29T13:59:32.458850643Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*FD).ReadFromInet4(0xc00043a300, {0xc004880000, 0x10000, 0x10000}, 0x0?)\n"}
-{"Time":"2023-03-29T13:59:32.458859626Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_unix.go:250 +0x24f\n"}
-{"Time":"2023-03-29T13:59:32.458872383Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*netFD).readFromInet4(0xc00043a300, {0xc004880000?, 0x0?, 0x0?}, 0x0?)\n"}
-{"Time":"2023-03-29T13:59:32.458879419Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/fd_posix.go:66 +0x29\n"}
-{"Time":"2023-03-29T13:59:32.458901901Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*UDPConn).readFrom(0x30?, {0xc004880000?, 0xc0005b7770?, 0x0?}, 0xc0005b7770)\n"}
-{"Time":"2023-03-29T13:59:32.458908829Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/udpsock_posix.go:52 +0x1b8\n"}
-{"Time":"2023-03-29T13:59:32.458922969Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*UDPConn).readFromUDP(0xc000015a08, {0xc004880000?, 0x4102c7?, 0x10000?}, 0x13e45e0?)\n"}
-{"Time":"2023-03-29T13:59:32.458928418Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/udpsock.go:149 +0x31\n"}
-{"Time":"2023-03-29T13:59:32.458942888Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*UDPConn).ReadFrom(0x59a?, {0xc004880000, 0x10000, 0x10000})\n"}
-{"Time":"2023-03-29T13:59:32.45894865Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/udpsock.go:158 +0x50\n"}
-{"Time":"2023-03-29T13:59:32.458972992Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"tailscale.com/net/stun/stuntest.runSTUN({0x1911ec0, 0xc00485eb60}, {0x1907f60, 0xc000015a08}, 0xc00481baa0, 0x17462c0?)\n"}
-{"Time":"2023-03-29T13:59:32.458979652Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/.local/go/pkg/mod/github.com/coder/tailscale@v1.1.1-0.20230327205451-058fa46a3723/net/stun/stuntest/stuntest.go:59 +0xc6\n"}
-{"Time":"2023-03-29T13:59:32.458988938Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by tailscale.com/net/stun/stuntest.ServeWithPacketListener\n"}
-{"Time":"2023-03-29T13:59:32.458996325Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/.local/go/pkg/mod/github.com/coder/tailscale@v1.1.1-0.20230327205451-058fa46a3723/net/stun/stuntest/stuntest.go:47 +0x26a\n"}
-{"Time":"2023-03-29T13:59:32.464073774Z","Action":"output","Package":"github.com/coder/coder/agent","Output":"FAIL\tgithub.com/coder/coder/agent\t2.045s\n"}
-{"Time":"2023-03-29T13:59:32.464093085Z","Action":"fail","Package":"github.com/coder/coder/agent","Elapsed":2.045}
+{"Time":"2023-03-29T13:59:30.419140864Z","Action":"start","Package":"github.com/coder/coder/v2/agent"}
+{"Time":"2023-03-29T13:59:30.440137227Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec"}
+{"Time":"2023-03-29T13:59:30.440225617Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":"=== RUN TestAgent_SessionExec\n"}
+{"Time":"2023-03-29T13:59:30.440252351Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":"=== PAUSE TestAgent_SessionExec\n"}
+{"Time":"2023-03-29T13:59:30.440264139Z","Action":"pause","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec"}
+{"Time":"2023-03-29T13:59:30.44029211Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell"}
+{"Time":"2023-03-29T13:59:30.440307898Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":"=== RUN TestAgent_SessionTTYShell\n"}
+{"Time":"2023-03-29T13:59:30.440330948Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":"=== PAUSE TestAgent_SessionTTYShell\n"}
+{"Time":"2023-03-29T13:59:30.440340646Z","Action":"pause","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell"}
+{"Time":"2023-03-29T13:59:30.440351592Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode"}
+{"Time":"2023-03-29T13:59:30.440360503Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== RUN TestAgent_SessionTTYExitCode\n"}
+{"Time":"2023-03-29T13:59:30.440373253Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== PAUSE TestAgent_SessionTTYExitCode\n"}
+{"Time":"2023-03-29T13:59:30.440389091Z","Action":"pause","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode"}
+{"Time":"2023-03-29T13:59:30.440406592Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD"}
+{"Time":"2023-03-29T13:59:30.440417518Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":"=== RUN TestAgent_Session_TTY_MOTD\n"}
+{"Time":"2023-03-29T13:59:30.68885571Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:59:30.688902548Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:59:30.688936919Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:59:30.688952573Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:59:30.688978288Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.688 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:59:30.689138933Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:59:30.689169612Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:59:30.689278237Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:59:30.689311927Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:59:30.689422904Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:59:30.689462324Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:59:30.689635363Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:1ad81e5115245108\n"}
+{"Time":"2023-03-29T13:59:30.689668719Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:59:30.689762323Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:59:30.689824046Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:59:30.689876569Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:59:30.689906309Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:59:30.689964141Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:59:30.690006177Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:59:30.690054052Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.689 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:59:30.690100827Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:59:30.690166644Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:59:30.690333879Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:59:30.69067189Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.690716053Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:59:30.690874768Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:59:30.690920653Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
+{"Time":"2023-03-29T13:59:30.691236077Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.690 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 55526, \"DERPPort\": 33325, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"/tmp/TestAgent_Session_TTY_MOTD2921078/001/motd\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
+{"Time":"2023-03-29T13:59:30.691266926Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
+{"Time":"2023-03-29T13:59:30.691645376Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:59:30.691681569Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:59:30.691697309Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:59:30.691834882Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:59:30.691894444Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:59:30.691990111Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:59:30.692037682Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.691 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:59:30.692117014Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:59:30.69217036Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:59:30.692223588Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:59:30.692269654Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:59:30.692436067Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:19af0f91ddbc2673\n"}
+{"Time":"2023-03-29T13:59:30.692486153Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:59:30.692600638Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:59:30.692643998Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:59:30.692706838Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:59:30.692750609Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:59:30.692799088Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:59:30.692845724Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:59:30.692892868Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:59:30.692943768Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:59:30.692994617Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.692 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:59:30.693130711Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:59:30.693421144Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.693467015Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:59:30.693571784Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
+{"Time":"2023-03-29T13:59:30.693599238Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
+{"Time":"2023-03-29T13:59:30.693798141Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.693 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.693596Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:59:30.699379222Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.699 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:59:30.702070777Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.701 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:59:30.702521256Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.702 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:59:30.705825444Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.705 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.690745Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:59:30.705969348Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.705 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:59:30.706047452Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.705 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:59:30.706117981Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.706 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:59:30.706167259Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.706 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:59:30.70672858Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.706 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:59:30.707193213Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.707 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:59:30.707713648Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.707 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:59:30.711327936Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.693596Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:59:30.711351236Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.711484437Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\n"}
+{"Time":"2023-03-29T13:59:30.711757724Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:59:30.711895059Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:59:30.712001206Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:59:30.712012787Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.711 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:59:30.712044402Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:59:30.712080254Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:59:30.712196969Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:59:30.712224408Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:59:30.712251498Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.712 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:59:30.713321731Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.690745Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:59:30.713345626Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.713438727Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
+{"Time":"2023-03-29T13:59:30.713542134Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:59:30.713653049Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:59:30.713794466Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:59:30.713831747Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:59:30.713873796Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:59:30.71391978Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:59:30.713976575Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:59:30.71401037Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.713 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:59:30.714059091Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.714 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:59:30.762911536Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.762 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:40423 derp=1 derpdist=1v4:2ms\n"}
+{"Time":"2023-03-29T13:59:30.763010472Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.762 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:59:30.763604178Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.763 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:40423 (stun), 172.20.0.2:40423 (local)\n"}
+{"Time":"2023-03-29T13:59:30.764002966Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.763 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.76358648 +0000 UTC m=+0.341643633 Peers:[] LocalAddrs:[{Addr:127.0.0.1:40423 Type:stun} {Addr:172.20.0.2:40423 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:59:30.764243833Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.764 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:51077 derp=1 derpdist=1v4:1ms\n"}
+{"Time":"2023-03-29T13:59:30.764368314Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.764 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:59:30.764813395Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.764 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:51077 (stun), 172.20.0.2:51077 (local)\n"}
+{"Time":"2023-03-29T13:59:30.765032404Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.764 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.764800711 +0000 UTC m=+0.342857833 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51077 Type:stun} {Addr:172.20.0.2:51077 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:59:30.766066288Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.765 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:59:30.766178166Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.766 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:59:30.766459711Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.766 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.766177132 +0000 UTC m=+0.344234254 Peers:[] LocalAddrs:[{Addr:127.0.0.1:40423 Type:stun} {Addr:172.20.0.2:40423 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:59:30.766803271Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.766 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.001851767}}}\n"}
+{"Time":"2023-03-29T13:59:30.767031778Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.766 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.763989Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.767745656Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.767 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.763989Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.767803589Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.767 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.7681905Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.767 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:59:30.769269486Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:59:30.769423531Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:59:30.76964587Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:59:30.76985461Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.001439212}}}\n"}
+{"Time":"2023-03-29T13:59:30.770057617Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.769 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.765018Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.770543054Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.770 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.765018Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.770576926Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.770 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.770805865Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.770 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:59:30.771214469Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.771 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:59:30.771672983Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.771 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.77151Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001851767}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.77196162Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.771 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.77151Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001851767}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.772249568Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.772286673Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:59:30.772417712Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:59:30.772485685Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:59:30.772763889Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:59:30.772990304Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.772846286 +0000 UTC m=+0.350903351 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51077 Type:stun} {Addr:172.20.0.2:51077 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:59:30.77305219Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.772 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.772974Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001439212}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.773283722Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.772974Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001439212}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.773486653Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.773528203Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:59:30.773629095Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:59:30.773696573Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.773 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:59:30.781245292Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.781 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:59:30.781769525Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.781 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:59:30.821304727Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.821 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:51077 derp=1 derpdist=1v4:4ms\n"}
+{"Time":"2023-03-29T13:59:30.823014931Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.822 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:40423 derp=1 derpdist=1v4:1ms\n"}
+{"Time":"2023-03-29T13:59:30.842118323Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [tUb7c] ...\n"}
+{"Time":"2023-03-29T13:59:30.842325421Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [tUb7c] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:59:30.842743216Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [s78jr] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:59:30.842835058Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [tUb7c] d:19af0f91ddbc2673 now using 172.20.0.2:51077\n"}
+{"Time":"2023-03-29T13:59:30.842945452Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [tUb7c] ...\n"}
+{"Time":"2023-03-29T13:59:30.843017307Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.842 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [tUb7c] ...\n"}
+{"Time":"2023-03-29T13:59:30.843455848Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:59:30.843724102Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:59:30.843761613Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:59:30.843824129Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:59:30.843866119Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:59:30.843967657Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:59:30.843978436Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Starting\n"}
+{"Time":"2023-03-29T13:59:30.844024904Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.843 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Sending handshake initiation\n"}
+{"Time":"2023-03-29T13:59:30.844361886Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [s78jr] now active, reconfiguring WireGuard\n"}
+{"Time":"2023-03-29T13:59:30.84441357Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:59:30.844612597Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:59:30.844652368Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:59:30.844692139Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:59:30.844742198Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:59:30.844790446Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:59:30.844839046Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Starting\n"}
+{"Time":"2023-03-29T13:59:30.845054904Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.844 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Received handshake initiation\n"}
+{"Time":"2023-03-29T13:59:30.845091562Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.845 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Sending handshake response\n"}
+{"Time":"2023-03-29T13:59:30.845575288Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.845 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.845411293 +0000 UTC m=+0.423468375 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359}] LocalAddrs:[{Addr:127.0.0.1:51077 Type:stun} {Addr:172.20.0.2:51077 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:59:30.846073274Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.846 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Received handshake response\n"}
+{"Time":"2023-03-29T13:59:30.846152172Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.846 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [s78jr] d:1ad81e5115245108 now using 172.20.0.2:40423\n"}
+{"Time":"2023-03-29T13:59:30.846323335Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.846 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:59:30.846216257 +0000 UTC m=+0.424273332 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:59:30.846070709 +0000 UTC NodeKey:nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b}] LocalAddrs:[{Addr:127.0.0.1:40423 Type:stun} {Addr:172.20.0.2:40423 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:59:30.846820565Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.846 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [tUb7c] d:19af0f91ddbc2673 now using 127.0.0.1:51077\n"}
+{"Time":"2023-03-29T13:59:30.872225511Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" agent_test.go:298: 2023-03-29 13:59:30.872: cmd: stdin: \"exit 0\\r\"\n"}
+{"Time":"2023-03-29T13:59:30.872911108Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.872 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:51077 derp=1 derpdist=1v4:1ms\n"}
+{"Time":"2023-03-29T13:59:30.873619058Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.873 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000537347}}}\n"}
+{"Time":"2023-03-29T13:59:30.873901772Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.873 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.873622Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000537347}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.874560964Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.874 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:40423 derp=1 derpdist=1v4:0s\n"}
+{"Time":"2023-03-29T13:59:30.875202519Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.874 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 7662552826698250063, \"as_of\": \"2023-03-29T13:59:30.873622Z\", \"key\": \"nodekey:b546fb7238fa44f6eb2eca16d2f6bc594b0fddda4dec86205f89af643b13b37b\", \"disco\": \"discokey:19af0f91ddbc267311e247d5dcefbf2c73a92431c7efac1902ced549bc2fd71c\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000537347}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:51077\", \"172.20.0.2:51077\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.875651396Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.875 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.0001832}}}\n"}
+{"Time":"2023-03-29T13:59:30.875737105Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.875 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.875642Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.0001832}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.876040926Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.875 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3088035564585519863, \"as_of\": \"2023-03-29T13:59:30.875642Z\", \"key\": \"nodekey:b3bf23adfe2e0f25fd088299b4fedb0da1a938fc36d2c925b85332f4cf681359\", \"disco\": \"discokey:1ad81e511524510849b5f4d4d0c86a4ff083af67002259e793939de53bc1c421\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.0001832}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8/128\"], \"endpoints\": [\"127.0.0.1:40423\", \"172.20.0.2:40423\"]}}\n"}
+{"Time":"2023-03-29T13:59:30.876278217Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.8763513Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:59:30.87651371Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:59:30.876622881Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:59:30.876654135Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Sending keepalive packet\n"}
+{"Time":"2023-03-29T13:59:30.876695566Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:59:30.876878296Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:59:30.876944605Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:59:30.877050858Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.876 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:59:30.877194231Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:59:30.877218701Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Sending keepalive packet\n"}
+{"Time":"2023-03-29T13:59:30.877269173Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:59:30.877353148Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:121: 2023-03-29 13:59:30.877: cmd: \"exit 0\"\n"}
+{"Time":"2023-03-29T13:59:30.877482007Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Receiving keepalive packet\n"}
+{"Time":"2023-03-29T13:59:30.877522599Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:30.877 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Receiving keepalive packet\n"}
+{"Time":"2023-03-29T13:59:31.212200169Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:31.212 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8): sending disco ping to [s78jr] ...\n"}
+{"Time":"2023-03-29T13:59:31.711519305Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:31.711 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8): sending disco ping to [s78jr] ...\n"}
+{"Time":"2023-03-29T13:59:32.2114982Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.211 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8): sending disco ping to [s78jr] ...\n"}
+{"Time":"2023-03-29T13:59:32.277108499Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:83: 2023-03-29 13:59:32.277: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:59:32.277144197Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:59:32.277: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:59:32.277157734Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:110: 2023-03-29 13:59:32.277: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:59:32.277164721Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:111: 2023-03-29 13:59:32.277: cmd: closing out\n"}
+{"Time":"2023-03-29T13:59:32.2771729Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:113: 2023-03-29 13:59:32.277: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:59:32.277306699Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:59:32.277: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:59:32.277350733Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:59:32.277: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:59:32.277374575Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:59:32.277: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:59:32.277394938Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:59:32.277: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:59:32.277415507Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:59:32.277: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:59:32.277434743Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:102: 2023-03-29 13:59:32.277: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:59:32.277579778Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
+{"Time":"2023-03-29T13:59:32.277611563Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 2s\n"}
+{"Time":"2023-03-29T13:59:32.277638668Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:59:32.277693383Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:59:32.277732009Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:59:32.277947928Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:59:32.277973674Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:59:32.278017899Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.277 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:59:32.278057207Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [tUb7c] - Stopping\n"}
+{"Time":"2023-03-29T13:59:32.278134475Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:59:32.278241902Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:59:32.278284628Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
+{"Time":"2023-03-29T13:59:32.278337186Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
+{"Time":"2023-03-29T13:59:32.27868397Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 2s\n"}
+{"Time":"2023-03-29T13:59:32.278698095Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:59:32.278769886Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:59:32.278808018Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:59:32.278879735Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:59:32.278965521Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.278 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4574:9c80:638b:7d8b:1bd8): sending disco ping to [s78jr] ...\n"}
+{"Time":"2023-03-29T13:59:32.279089367Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.279 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:59:32.279122263Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.279 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:59:32.279168949Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.279 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [s78jr] - Stopping\n"}
+{"Time":"2023-03-29T13:59:32.279275932Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:59:32.279 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:59:32.279696417Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" stuntest.go:63: STUN server shutdown\n"}
+{"Time":"2023-03-29T13:59:32.279872392Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":"--- PASS: TestAgent_Session_TTY_MOTD (1.84s)\n"}
+{"Time":"2023-03-29T13:59:32.279886475Z","Action":"pass","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Elapsed":1.84}
+{"Time":"2023-03-29T13:59:32.27989796Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin"}
+{"Time":"2023-03-29T13:59:32.279902938Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"=== RUN TestAgent_Session_TTY_Hushlogin\n"}
+{"Time":"2023-03-29T13:59:32.457943185Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"panic: test timed out after 2s\n"}
+{"Time":"2023-03-29T13:59:32.45796911Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"running tests:\n"}
+{"Time":"2023-03-29T13:59:32.457977422Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\tTestAgent_Session_TTY_Hushlogin (0s)\n"}
+{"Time":"2023-03-29T13:59:32.457982677Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
+{"Time":"2023-03-29T13:59:32.457987323Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 411 [running]:\n"}
+{"Time":"2023-03-29T13:59:32.457991361Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*M).startAlarm.func1()\n"}
+{"Time":"2023-03-29T13:59:32.457995716Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:2241 +0x3b9\n"}
+{"Time":"2023-03-29T13:59:32.4580029Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by time.goFunc\n"}
+{"Time":"2023-03-29T13:59:32.458007365Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/time/sleep.go:176 +0x32\n"}
+{"Time":"2023-03-29T13:59:32.45801203Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
+{"Time":"2023-03-29T13:59:32.45801607Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 1 [chan receive]:\n"}
+{"Time":"2023-03-29T13:59:32.458020121Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*T).Run(0xc0004e1040, {0x16a5e92?, 0x535fa5?}, 0x17462c0)\n"}
+{"Time":"2023-03-29T13:59:32.458024235Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1630 +0x405\n"}
+{"Time":"2023-03-29T13:59:32.458028654Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.runTests.func1(0x236db60?)\n"}
+{"Time":"2023-03-29T13:59:32.45803313Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:2036 +0x45\n"}
+{"Time":"2023-03-29T13:59:32.458037122Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc0004e1040, 0xc000589bb8)\n"}
+{"Time":"2023-03-29T13:59:32.458041079Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
+{"Time":"2023-03-29T13:59:32.45804729Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.runTests(0xc000341a40?, {0x235c580, 0x21, 0x21}, {0x4182d0?, 0xc000589c78?, 0x236cb40?})\n"}
+{"Time":"2023-03-29T13:59:32.458051787Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:2034 +0x489\n"}
+{"Time":"2023-03-29T13:59:32.458055836Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*M).Run(0xc000341a40)\n"}
+{"Time":"2023-03-29T13:59:32.458059703Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1906 +0x63a\n"}
+{"Time":"2023-03-29T13:59:32.458066931Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"go.uber.org/goleak.VerifyTestMain({0x18f5540?, 0xc000341a40?}, {0x0, 0x0, 0x0})\n"}
+{"Time":"2023-03-29T13:59:32.458071333Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/.local/go/pkg/mod/go.uber.org/goleak@v1.2.1/testmain.go:53 +0x6b\n"}
+{"Time":"2023-03-29T13:59:32.458075235Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/v2/agent_test.TestMain(...)\n"}
+{"Time":"2023-03-29T13:59:32.458081052Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:53\n"}
+{"Time":"2023-03-29T13:59:32.45808506Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"main.main()\n"}
+{"Time":"2023-03-29T13:59:32.458088903Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t_testmain.go:115 +0x1e5\n"}
+{"Time":"2023-03-29T13:59:32.458094468Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
+{"Time":"2023-03-29T13:59:32.458102106Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 9 [chan receive]:\n"}
+{"Time":"2023-03-29T13:59:32.45810621Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*T).Parallel(0xc0004e11e0)\n"}
+{"Time":"2023-03-29T13:59:32.45811231Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1384 +0x225\n"}
+{"Time":"2023-03-29T13:59:32.458116485Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/v2/agent_test.TestAgent_SessionExec(0x0?)\n"}
+{"Time":"2023-03-29T13:59:32.458120426Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:188 +0x33\n"}
+{"Time":"2023-03-29T13:59:32.458126141Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc0004e11e0, 0x1746298)\n"}
+{"Time":"2023-03-29T13:59:32.458130428Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
+{"Time":"2023-03-29T13:59:32.458135189Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by testing.(*T).Run\n"}
+{"Time":"2023-03-29T13:59:32.458144082Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n"}
+{"Time":"2023-03-29T13:59:32.458150718Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
+{"Time":"2023-03-29T13:59:32.458156738Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 10 [chan receive]:\n"}
+{"Time":"2023-03-29T13:59:32.458165162Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*T).Parallel(0xc0004e1520)\n"}
+{"Time":"2023-03-29T13:59:32.458171886Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1384 +0x225\n"}
+{"Time":"2023-03-29T13:59:32.458178556Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/v2/agent_test.TestAgent_SessionTTYShell(0xc0004e1520)\n"}
+{"Time":"2023-03-29T13:59:32.458182782Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:213 +0x36\n"}
+{"Time":"2023-03-29T13:59:32.458188646Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc0004e1520, 0x17462a8)\n"}
+{"Time":"2023-03-29T13:59:32.458192664Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
+{"Time":"2023-03-29T13:59:32.458196426Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by testing.(*T).Run\n"}
+{"Time":"2023-03-29T13:59:32.458200247Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n"}
+{"Time":"2023-03-29T13:59:32.458204698Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
+{"Time":"2023-03-29T13:59:32.458208395Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 11 [chan receive]:\n"}
+{"Time":"2023-03-29T13:59:32.458215562Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.(*T).Parallel(0xc0004e1860)\n"}
+{"Time":"2023-03-29T13:59:32.458219487Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1384 +0x225\n"}
+{"Time":"2023-03-29T13:59:32.458223369Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/v2/agent_test.TestAgent_SessionTTYExitCode(0xc0004e1520?)\n"}
+{"Time":"2023-03-29T13:59:32.458228633Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:244 +0x36\n"}
+{"Time":"2023-03-29T13:59:32.458234377Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc0004e1860, 0x17462a0)\n"}
+{"Time":"2023-03-29T13:59:32.458238258Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
+{"Time":"2023-03-29T13:59:32.458242179Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by testing.(*T).Run\n"}
+{"Time":"2023-03-29T13:59:32.458246008Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n"}
+{"Time":"2023-03-29T13:59:32.458249635Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
+{"Time":"2023-03-29T13:59:32.45825516Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 408 [runnable]:\n"}
+{"Time":"2023-03-29T13:59:32.458282465Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.nat.montgomery({0xc004aa4500?, 0x10?, 0x26?}, {0xc004aa4280?, 0x10?, 0x26?}, {0xc004aa4280?, 0x10?, 0x26?}, {0xc000732820, ...}, ...)\n"}
+{"Time":"2023-03-29T13:59:32.458291741Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/nat.go:216 +0x565\n"}
+{"Time":"2023-03-29T13:59:32.458330298Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.nat.expNNMontgomery({0xc004aa4280, 0xc0003c2e70?, 0x26}, {0xc004a9adc0?, 0x21?, 0x24?}, {0xc004a9ac80, 0x10, 0x24?}, {0xc000732820, ...})\n"}
+{"Time":"2023-03-29T13:59:32.458336764Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/nat.go:1271 +0xb1c\n"}
+{"Time":"2023-03-29T13:59:32.458384114Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.nat.expNN({0xc004aa4280?, 0x14?, 0x22c2900?}, {0xc004a9adc0?, 0x10, 0x14}, {0xc004a9ac80?, 0x10, 0x14?}, {0xc000732820, ...}, ...)\n"}
+{"Time":"2023-03-29T13:59:32.458393699Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/nat.go:996 +0x3b1\n"}
+{"Time":"2023-03-29T13:59:32.458403406Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.nat.probablyPrimeMillerRabin({0xc000732820?, 0x10, 0x14}, 0x15, 0x1)\n"}
+{"Time":"2023-03-29T13:59:32.458413208Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/prime.go:106 +0x5b8\n"}
+{"Time":"2023-03-29T13:59:32.458420936Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"math/big.(*Int).ProbablyPrime(0xc0047208c0, 0x14)\n"}
+{"Time":"2023-03-29T13:59:32.458424869Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/math/big/prime.go:78 +0x225\n"}
+{"Time":"2023-03-29T13:59:32.458430384Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"crypto/rand.Prime({0x18f04c0, 0xc00007e020}, 0x400)\n"}
+{"Time":"2023-03-29T13:59:32.45843919Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/crypto/rand/util.go:55 +0x1e5\n"}
+{"Time":"2023-03-29T13:59:32.45845888Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"crypto/rsa.GenerateMultiPrimeKey({0x18f04c0, 0xc00007e020}, 0x2, 0x800)\n"}
+{"Time":"2023-03-29T13:59:32.458469074Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/crypto/rsa/rsa.go:369 +0x745\n"}
+{"Time":"2023-03-29T13:59:32.458474102Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"crypto/rsa.GenerateKey(...)\n"}
+{"Time":"2023-03-29T13:59:32.45847985Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/crypto/rsa/rsa.go:264\n"}
+{"Time":"2023-03-29T13:59:32.458483767Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/v2/agent.(*agent).init(0xc00485eea0, {0x1902c20?, 0xc00485d770})\n"}
+{"Time":"2023-03-29T13:59:32.458489397Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent.go:810 +0x6c\n"}
+{"Time":"2023-03-29T13:59:32.458522193Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/v2/agent.New({{0x190cbc0, 0xc0005b7710}, {0x166d829, 0x4}, {0x166d829, 0x4}, 0x17461d8, {0x1907c90, 0xc000278280}, 0x45d964b800, ...})\n"}
+{"Time":"2023-03-29T13:59:32.458531042Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent.go:134 +0x549\n"}
+{"Time":"2023-03-29T13:59:32.458565925Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/v2/agent_test.setupAgent(0xc00485eb60, {0x0, {0x0, 0x0}, {0x0, 0x0, 0x0}, 0xc0005b8da0, 0x0, {0x0, ...}, ...}, ...)\n"}
+{"Time":"2023-03-29T13:59:32.458576217Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:1568 +0x63e\n"}
+{"Time":"2023-03-29T13:59:32.458605192Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/v2/agent_test.setupSSHSession(0xc00485eb60, {0x0, {0x0, 0x0}, {0x0, 0x0, 0x0}, 0x0, 0x0, {0x0, ...}, ...})\n"}
+{"Time":"2023-03-29T13:59:32.458614801Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:1524 +0xc5\n"}
+{"Time":"2023-03-29T13:59:32.458625161Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"github.com/coder/coder/v2/agent_test.TestAgent_Session_TTY_Hushlogin(0xc00485eb60)\n"}
+{"Time":"2023-03-29T13:59:32.458630015Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/src/coder/coder/agent/agent_test.go:330 +0x2fa\n"}
+{"Time":"2023-03-29T13:59:32.458635887Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"testing.tRunner(0xc00485eb60, 0x17462c0)\n"}
+{"Time":"2023-03-29T13:59:32.458639744Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1576 +0x10b\n"}
+{"Time":"2023-03-29T13:59:32.458643595Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by testing.(*T).Run\n"}
+{"Time":"2023-03-29T13:59:32.458649393Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/testing/testing.go:1629 +0x3ea\n"}
+{"Time":"2023-03-29T13:59:32.458653156Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
+{"Time":"2023-03-29T13:59:32.458657314Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 409 [IO wait]:\n"}
+{"Time":"2023-03-29T13:59:32.458662763Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.runtime_pollWait(0x7f5230766628, 0x72)\n"}
+{"Time":"2023-03-29T13:59:32.458668522Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/runtime/netpoll.go:306 +0x89\n"}
+{"Time":"2023-03-29T13:59:32.45867585Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*pollDesc).wait(0xc00475bf80?, 0xc0005ec5e2?, 0x0)\n"}
+{"Time":"2023-03-29T13:59:32.458681918Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x32\n"}
+{"Time":"2023-03-29T13:59:32.458688307Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*pollDesc).waitRead(...)\n"}
+{"Time":"2023-03-29T13:59:32.458708219Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\n"}
+{"Time":"2023-03-29T13:59:32.458712856Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*FD).Accept(0xc00475bf80)\n"}
+{"Time":"2023-03-29T13:59:32.458717364Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_unix.go:614 +0x2bd\n"}
+{"Time":"2023-03-29T13:59:32.458721204Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*netFD).accept(0xc00475bf80)\n"}
+{"Time":"2023-03-29T13:59:32.458727021Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/fd_unix.go:172 +0x35\n"}
+{"Time":"2023-03-29T13:59:32.458731155Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*TCPListener).accept(0xc00486ecd8)\n"}
+{"Time":"2023-03-29T13:59:32.458737943Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/tcpsock_posix.go:148 +0x25\n"}
+{"Time":"2023-03-29T13:59:32.458744974Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*TCPListener).Accept(0xc00486ecd8)\n"}
+{"Time":"2023-03-29T13:59:32.458752245Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/tcpsock.go:297 +0x3d\n"}
+{"Time":"2023-03-29T13:59:32.458758039Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"crypto/tls.(*listener).Accept(0xc00486ef18)\n"}
+{"Time":"2023-03-29T13:59:32.458763674Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/crypto/tls/tls.go:66 +0x2d\n"}
+{"Time":"2023-03-29T13:59:32.458770104Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net/http.(*Server).Serve(0xc00029da40, {0x18fefa0, 0xc00486ef18})\n"}
+{"Time":"2023-03-29T13:59:32.458778229Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/http/server.go:3059 +0x385\n"}
+{"Time":"2023-03-29T13:59:32.45878539Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net/http/httptest.(*Server).goServe.func1()\n"}
+{"Time":"2023-03-29T13:59:32.458793477Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/http/httptest/server.go:310 +0x6a\n"}
+{"Time":"2023-03-29T13:59:32.458797511Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by net/http/httptest.(*Server).goServe\n"}
+{"Time":"2023-03-29T13:59:32.458801374Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/http/httptest/server.go:308 +0x6a\n"}
+{"Time":"2023-03-29T13:59:32.458805093Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\n"}
+{"Time":"2023-03-29T13:59:32.458810472Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"goroutine 410 [IO wait]:\n"}
+{"Time":"2023-03-29T13:59:32.458814744Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.runtime_pollWait(0x7f5230765908, 0x72)\n"}
+{"Time":"2023-03-29T13:59:32.458819038Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/runtime/netpoll.go:306 +0x89\n"}
+{"Time":"2023-03-29T13:59:32.458824589Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*pollDesc).wait(0xc00043a300?, 0xc004880000?, 0x0)\n"}
+{"Time":"2023-03-29T13:59:32.458830327Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:84 +0x32\n"}
+{"Time":"2023-03-29T13:59:32.458835463Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*pollDesc).waitRead(...)\n"}
+{"Time":"2023-03-29T13:59:32.458840944Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_poll_runtime.go:89\n"}
+{"Time":"2023-03-29T13:59:32.458850643Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"internal/poll.(*FD).ReadFromInet4(0xc00043a300, {0xc004880000, 0x10000, 0x10000}, 0x0?)\n"}
+{"Time":"2023-03-29T13:59:32.458859626Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/internal/poll/fd_unix.go:250 +0x24f\n"}
+{"Time":"2023-03-29T13:59:32.458872383Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*netFD).readFromInet4(0xc00043a300, {0xc004880000?, 0x0?, 0x0?}, 0x0?)\n"}
+{"Time":"2023-03-29T13:59:32.458879419Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/fd_posix.go:66 +0x29\n"}
+{"Time":"2023-03-29T13:59:32.458901901Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*UDPConn).readFrom(0x30?, {0xc004880000?, 0xc0005b7770?, 0x0?}, 0xc0005b7770)\n"}
+{"Time":"2023-03-29T13:59:32.458908829Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/udpsock_posix.go:52 +0x1b8\n"}
+{"Time":"2023-03-29T13:59:32.458922969Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*UDPConn).readFromUDP(0xc000015a08, {0xc004880000?, 0x4102c7?, 0x10000?}, 0x13e45e0?)\n"}
+{"Time":"2023-03-29T13:59:32.458928418Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/udpsock.go:149 +0x31\n"}
+{"Time":"2023-03-29T13:59:32.458942888Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"net.(*UDPConn).ReadFrom(0x59a?, {0xc004880000, 0x10000, 0x10000})\n"}
+{"Time":"2023-03-29T13:59:32.45894865Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/usr/local/go/src/net/udpsock.go:158 +0x50\n"}
+{"Time":"2023-03-29T13:59:32.458972992Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"tailscale.com/net/stun/stuntest.runSTUN({0x1911ec0, 0xc00485eb60}, {0x1907f60, 0xc000015a08}, 0xc00481baa0, 0x17462c0?)\n"}
+{"Time":"2023-03-29T13:59:32.458979652Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/.local/go/pkg/mod/github.com/coder/tailscale@v1.1.1-0.20230327205451-058fa46a3723/net/stun/stuntest/stuntest.go:59 +0xc6\n"}
+{"Time":"2023-03-29T13:59:32.458988938Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"created by tailscale.com/net/stun/stuntest.ServeWithPacketListener\n"}
+{"Time":"2023-03-29T13:59:32.458996325Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"\t/home/mafredri/.local/go/pkg/mod/github.com/coder/tailscale@v1.1.1-0.20230327205451-058fa46a3723/net/stun/stuntest/stuntest.go:47 +0x26a\n"}
+{"Time":"2023-03-29T13:59:32.464073774Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Output":"FAIL\tgithub.com/coder/coder/v2/agent\t2.045s\n"}
+{"Time":"2023-03-29T13:59:32.464093085Z","Action":"fail","Package":"github.com/coder/coder/v2/agent","Elapsed":2.045}
diff --git a/scripts/ci-report/testdata/gotests.json.sample b/scripts/ci-report/testdata/gotests.json.sample
index 245facbfb8c8e..63f19aa1c7ba7 100644
--- a/scripts/ci-report/testdata/gotests.json.sample
+++ b/scripts/ci-report/testdata/gotests.json.sample
@@ -1,2922 +1,2922 @@
-{"Time":"2023-03-29T13:37:23.355347397Z","Action":"start","Package":"github.com/coder/coder/agent"}
-{"Time":"2023-03-29T13:37:23.381695238Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec"}
-{"Time":"2023-03-29T13:37:23.38177342Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":"=== RUN TestAgent_SessionExec\n"}
-{"Time":"2023-03-29T13:37:23.381791755Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":"=== PAUSE TestAgent_SessionExec\n"}
-{"Time":"2023-03-29T13:37:23.381805147Z","Action":"pause","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec"}
-{"Time":"2023-03-29T13:37:23.381827974Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell"}
-{"Time":"2023-03-29T13:37:23.381835977Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":"=== RUN TestAgent_SessionTTYShell\n"}
-{"Time":"2023-03-29T13:37:23.381850018Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":"=== PAUSE TestAgent_SessionTTYShell\n"}
-{"Time":"2023-03-29T13:37:23.381857444Z","Action":"pause","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell"}
-{"Time":"2023-03-29T13:37:23.381868815Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode"}
-{"Time":"2023-03-29T13:37:23.381876252Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== RUN TestAgent_SessionTTYExitCode\n"}
-{"Time":"2023-03-29T13:37:23.381885049Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== PAUSE TestAgent_SessionTTYExitCode\n"}
-{"Time":"2023-03-29T13:37:23.381896641Z","Action":"pause","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode"}
-{"Time":"2023-03-29T13:37:23.381914968Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD"}
-{"Time":"2023-03-29T13:37:23.381930694Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":"=== RUN TestAgent_Session_TTY_MOTD\n"}
-{"Time":"2023-03-29T13:37:23.459584829Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:23.45962803Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:23.459637144Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:23.459709589Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:23.459766441Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:23.459896565Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:23.45992711Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:23.460013936Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:23.460047337Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:23.460151722Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:23.460178571Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:23.460311639Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:07ad6d06cd8b5ff2\n"}
-{"Time":"2023-03-29T13:37:23.460356174Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:23.460498076Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:23.460536017Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:23.460590141Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:23.460620636Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:23.460662149Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:23.460698929Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:23.460733289Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:23.460789117Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:23.460856196Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:23.460986291Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:23.46141926Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.461506329Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:23.461608094Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:23.46164708Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
-{"Time":"2023-03-29T13:37:23.461997997Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 48127, \"DERPPort\": 44839, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"/tmp/TestAgent_Session_TTY_MOTD1157664819/001/motd\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
-{"Time":"2023-03-29T13:37:23.462041275Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
-{"Time":"2023-03-29T13:37:23.462418253Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:23.46244618Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:23.462489007Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:23.462532307Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:23.462584588Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:23.462669431Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:23.462699701Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:23.46277017Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:23.46280348Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:23.46284612Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:23.462890638Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:23.463014252Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:8ac4cc2c7460d56f\n"}
-{"Time":"2023-03-29T13:37:23.463040585Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:23.463167416Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:23.463228086Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:23.463265117Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:23.463307341Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:23.463345133Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:23.463380146Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:23.463437865Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:23.463474458Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:23.463513083Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:23.463651429Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:23.463966826Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.463992242Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:23.464079789Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.464 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
-{"Time":"2023-03-29T13:37:23.464100162Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.464 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
-{"Time":"2023-03-29T13:37:23.464314405Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.464 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.464098Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:23.470436775Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.470 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:23.473142737Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.473 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:23.473905461Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.473 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:23.476012898Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.475 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.4615Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:23.476335333Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.476 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:23.476591432Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.476 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:23.476641778Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.476 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:23.476677236Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.476 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:23.47834267Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.478 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:23.478773607Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.478 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:23.479289417Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.479 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:23.484191154Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.4615Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:23.484233027Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.484426557Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
-{"Time":"2023-03-29T13:37:23.484790305Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:23.484946523Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:23.485230922Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:23.485304276Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:23.485410816Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:23.485506941Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:23.485747079Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:23.485857203Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:23.485942868Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:23.486354495Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.464098Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:23.486406209Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.486580191Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\n"}
-{"Time":"2023-03-29T13:37:23.486731116Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:23.486910536Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:23.48721125Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:23.487271545Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:23.487362767Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:23.487505661Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:23.48757023Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:23.487687075Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:23.487755179Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:23.533579774Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.533 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:41471 derp=1 derpdist=1v4:5ms\n"}
-{"Time":"2023-03-29T13:37:23.533653394Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.533 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:23.534036522Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.533 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:41471 (stun), 172.20.0.2:41471 (local)\n"}
-{"Time":"2023-03-29T13:37:23.534320881Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.534012576 +0000 UTC m=+0.173619877 Peers:[] LocalAddrs:[{Addr:127.0.0.1:41471 Type:stun} {Addr:172.20.0.2:41471 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:23.534485142Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:34768 derp=1 derpdist=1v4:4ms\n"}
-{"Time":"2023-03-29T13:37:23.534597588Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:23.534893919Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:34768 (stun), 172.20.0.2:34768 (local)\n"}
-{"Time":"2023-03-29T13:37:23.535056614Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.534872863 +0000 UTC m=+0.174480149 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34768 Type:stun} {Addr:172.20.0.2:34768 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:23.535722359Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.535 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:23.535840193Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.535 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:23.53601927Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.535 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.535826298 +0000 UTC m=+0.175433549 Peers:[] LocalAddrs:[{Addr:127.0.0.1:41471 Type:stun} {Addr:172.20.0.2:41471 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:23.536284979Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.536 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.004534057}}}\n"}
-{"Time":"2023-03-29T13:37:23.536457311Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.536 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.5343Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.536888005Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.536 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.5343Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.536962253Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.536 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.537168204Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:23.537809136Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.537 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:23.537959175Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:23.538044116Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:23.538236588Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.538043332 +0000 UTC m=+0.177650587 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34768 Type:stun} {Addr:172.20.0.2:34768 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:23.538347057Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.004475683}}}\n"}
-{"Time":"2023-03-29T13:37:23.538488084Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.535038Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.538915728Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.535038Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.538974002Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.539154829Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.539 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:23.539540545Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.539 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:23.539953465Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.539 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.539778Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.004534057}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.540373922Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.540 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.539778Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.004534057}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.540832675Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.540 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.540920246Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.540 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:23.541080962Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.541 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:23.541207198Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.541 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:23.541540025Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.541 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:23.541869031Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.541 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.541715Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.004475683}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.542270097Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.541715Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.004475683}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.542592821Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.542683196Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:23.542867815Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:23.542985478Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:23.55226213Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.552 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:23.552392256Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.552 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:23.589932282Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.589 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:34768 derp=1 derpdist=1v4:4ms\n"}
-{"Time":"2023-03-29T13:37:23.591567427Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.591 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:41471 derp=1 derpdist=1v4:2ms\n"}
-{"Time":"2023-03-29T13:37:23.598751818Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.598 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0x7ra] ...\n"}
-{"Time":"2023-03-29T13:37:23.598954894Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.598 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [0x7ra] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:23.599390218Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.599 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [vT+Vd] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:23.599516651Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.599 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [0x7ra] d:8ac4cc2c7460d56f now using 172.20.0.2:34768\n"}
-{"Time":"2023-03-29T13:37:23.599643596Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.599 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0x7ra] ...\n"}
-{"Time":"2023-03-29T13:37:23.59972717Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.599 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0x7ra] ...\n"}
-{"Time":"2023-03-29T13:37:23.600132385Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:23.600385251Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:23.600423261Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:23.600476824Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:23.600511572Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:23.600547765Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:23.600583838Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Starting\n"}
-{"Time":"2023-03-29T13:37:23.600650591Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Sending handshake initiation\n"}
-{"Time":"2023-03-29T13:37:23.601044073Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [vT+Vd] now active, reconfiguring WireGuard\n"}
-{"Time":"2023-03-29T13:37:23.601107152Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:23.601327676Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:23.601369666Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:23.601396751Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:23.601460927Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:23.601495639Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:23.601526183Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Starting\n"}
-{"Time":"2023-03-29T13:37:23.60175503Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Received handshake initiation\n"}
-{"Time":"2023-03-29T13:37:23.601775856Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Sending handshake response\n"}
-{"Time":"2023-03-29T13:37:23.602280259Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.602 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.602127595 +0000 UTC m=+0.241734812 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33}] LocalAddrs:[{Addr:127.0.0.1:34768 Type:stun} {Addr:172.20.0.2:34768 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:23.602838109Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.602 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Received handshake response\n"}
-{"Time":"2023-03-29T13:37:23.602930775Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.602 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [vT+Vd] d:07ad6d06cd8b5ff2 now using 172.20.0.2:41471\n"}
-{"Time":"2023-03-29T13:37:23.603134941Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.602 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.602980142 +0000 UTC m=+0.242587360 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:23.602828352 +0000 UTC NodeKey:nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229}] LocalAddrs:[{Addr:127.0.0.1:41471 Type:stun} {Addr:172.20.0.2:41471 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:23.603731388Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.603 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [0x7ra] d:8ac4cc2c7460d56f now using 127.0.0.1:34768\n"}
-{"Time":"2023-03-29T13:37:23.629049874Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" agent_test.go:298: 2023-03-29 13:37:23.628: cmd: stdin: \"exit 0\\r\"\n"}
-{"Time":"2023-03-29T13:37:23.62993175Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:121: 2023-03-29 13:37:23.629: cmd: \"exit 0\"\n"}
-{"Time":"2023-03-29T13:37:23.643243989Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.643 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:34768 derp=1 derpdist=1v4:1ms\n"}
-{"Time":"2023-03-29T13:37:23.643873931Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.643 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:41471 derp=1 derpdist=1v4:0s\n"}
-{"Time":"2023-03-29T13:37:23.644469186Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.644 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000547591}}}\n"}
-{"Time":"2023-03-29T13:37:23.644715274Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.644 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.644461Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000547591}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.645390624Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.645 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.644461Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000547591}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.64591875Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.645 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.646145075Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.645 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:23.646400847Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:23.646730863Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:23.646828936Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Sending keepalive packet\n"}
-{"Time":"2023-03-29T13:37:23.646930238Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:23.647158211Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000154898}}}\n"}
-{"Time":"2023-03-29T13:37:23.647376125Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.647 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.64715Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000154898}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.648003118Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.647 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.64715Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000154898}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
-{"Time":"2023-03-29T13:37:23.648256172Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:23.648338509Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:23.648471344Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:23.648609559Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:23.648638814Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Sending keepalive packet\n"}
-{"Time":"2023-03-29T13:37:23.648721589Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:23.648895668Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Receiving keepalive packet\n"}
-{"Time":"2023-03-29T13:37:23.648944175Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Receiving keepalive packet\n"}
-{"Time":"2023-03-29T13:37:23.983374775Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.983 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775): sending disco ping to [vT+Vd] ...\n"}
-{"Time":"2023-03-29T13:37:24.483975661Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.483 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775): sending disco ping to [vT+Vd] ...\n"}
-{"Time":"2023-03-29T13:37:24.983883086Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.983 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775): sending disco ping to [vT+Vd] ...\n"}
-{"Time":"2023-03-29T13:37:24.997284571Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:83: 2023-03-29 13:37:24.997: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:24.997307935Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:37:24.997: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:24.997315409Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:110: 2023-03-29 13:37:24.997: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:24.997319364Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:111: 2023-03-29 13:37:24.997: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:24.997323588Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:113: 2023-03-29 13:37:24.997: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:24.997370156Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:37:24.997: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:24.997381692Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:37:24.997: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:24.997385034Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:37:24.997: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:24.997389205Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:37:24.997: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:24.997393753Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:37:24.997: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:24.997405892Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:102: 2023-03-29 13:37:24.997: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:24.997490606Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
-{"Time":"2023-03-29T13:37:24.997723999Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 1s\n"}
-{"Time":"2023-03-29T13:37:24.997783062Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:24.997839084Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:24.997864344Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:24.997932377Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:24.998046302Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:24.998086112Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:24.998136192Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Stopping\n"}
-{"Time":"2023-03-29T13:37:24.998214902Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:24.998405401Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:24.998453108Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
-{"Time":"2023-03-29T13:37:24.998545966Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
-{"Time":"2023-03-29T13:37:24.998863807Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 1s\n"}
-{"Time":"2023-03-29T13:37:24.998907983Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:24.998974565Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:24.999012856Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:24.999084066Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:24.999163662Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775): sending disco ping to [vT+Vd] ...\n"}
-{"Time":"2023-03-29T13:37:24.999281214Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:24.999322797Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:24.999367964Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Stopping\n"}
-{"Time":"2023-03-29T13:37:24.999477141Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:24.999790842Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" stuntest.go:63: STUN server shutdown\n"}
-{"Time":"2023-03-29T13:37:24.999978482Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Output":"--- PASS: TestAgent_Session_TTY_MOTD (1.62s)\n"}
-{"Time":"2023-03-29T13:37:24.999989636Z","Action":"pass","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_MOTD","Elapsed":1.62}
-{"Time":"2023-03-29T13:37:25.000001861Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin"}
-{"Time":"2023-03-29T13:37:25.000006766Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"=== RUN TestAgent_Session_TTY_Hushlogin\n"}
-{"Time":"2023-03-29T13:37:25.061523057Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:25.061562172Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:25.061580317Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:25.061650184Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:25.061692748Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:25.061779714Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:25.061825894Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:25.0619026Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:25.061948469Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:25.061997351Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:25.062034009Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:25.062159882Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:de63960686d4d969\n"}
-{"Time":"2023-03-29T13:37:25.062211517Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:25.062292207Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:25.062336748Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:25.062378334Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:25.062420493Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:25.06246598Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:25.062509064Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:25.0625467Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:25.062582098Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:25.062621581Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:25.062741251Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:25.063047606Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.063098481Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:25.063228621Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:25.063275399Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
-{"Time":"2023-03-29T13:37:25.063394952Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 48719, \"DERPPort\": 45121, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"/tmp/TestAgent_Session_TTY_Hushlogin1510664063/001/motd\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
-{"Time":"2023-03-29T13:37:25.063449256Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
-{"Time":"2023-03-29T13:37:25.063787886Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:25.063828837Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:25.063873191Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:25.063920593Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:25.063972721Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:25.064056998Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:25.064104109Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:25.064172955Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:25.064220262Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:25.064265054Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:25.064311842Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:25.064424792Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:a2fa54a4398f4b14\n"}
-{"Time":"2023-03-29T13:37:25.064460776Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:25.064519398Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:25.064564764Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:25.064610015Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:25.064649665Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:25.064697586Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:25.064733375Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:25.06476809Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:25.064817752Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:25.064858351Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:25.064968632Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:25.065284782Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.065331529Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:25.065409197Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
-{"Time":"2023-03-29T13:37:25.065446163Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
-{"Time":"2023-03-29T13:37:25.065532991Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.065442Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:25.065984331Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:25.066385592Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.066 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:25.066774463Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.066 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:25.067577596Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.063134Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:25.067723294Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:25.067791082Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:25.067853127Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:25.067910376Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:25.0683366Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.068 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:25.07002731Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.069 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:25.071537156Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.071 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:25.073373256Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.065442Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:25.073396962Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.073480235Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\n"}
-{"Time":"2023-03-29T13:37:25.073564836Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:25.073671864Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:25.07378875Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:25.073820206Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:25.073854357Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:25.073896565Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:25.073931707Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:25.073965714Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:25.074005901Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:25.074880124Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.074 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.063134Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:25.074899766Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.074 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.074982929Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.074 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
-{"Time":"2023-03-29T13:37:25.075048527Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:25.075142259Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:25.075263679Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:25.075295728Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:25.075336116Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:25.075375372Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:25.075421587Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:25.075466864Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:25.075504912Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:25.117000024Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.116 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:25.124882906Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.124 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:60850 derp=1 derpdist=1v4:1ms\n"}
-{"Time":"2023-03-29T13:37:25.125053667Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.124 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:25.12561266Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.125 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:60850 (stun), 172.20.0.2:60850 (local)\n"}
-{"Time":"2023-03-29T13:37:25.125905661Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.125 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.125569748 +0000 UTC m=+1.765177074 Peers:[] LocalAddrs:[{Addr:127.0.0.1:60850 Type:stun} {Addr:172.20.0.2:60850 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:25.126844264Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.126 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:58304 derp=1 derpdist=1v4:1ms\n"}
-{"Time":"2023-03-29T13:37:25.127005238Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.126 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:25.127513222Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.127 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:58304 (stun), 172.20.0.2:58304 (local)\n"}
-{"Time":"2023-03-29T13:37:25.127757541Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.127 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.127481783 +0000 UTC m=+1.767089103 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58304 Type:stun} {Addr:172.20.0.2:58304 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:25.128599916Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.128 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:25.128762101Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.128 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:25.129009957Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.128 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.12872419 +0000 UTC m=+1.768331474 Peers:[] LocalAddrs:[{Addr:127.0.0.1:60850 Type:stun} {Addr:172.20.0.2:60850 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:25.129209925Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.128 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000985407}}}\n"}
-{"Time":"2023-03-29T13:37:25.129460508Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.129 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.125849Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.130127324Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.129 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.125849Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.130232734Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.130 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.13055158Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.130 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:25.131114968Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.130 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:25.131240194Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.131 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:25.131519421Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.131 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.131235659 +0000 UTC m=+1.770842948 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58304 Type:stun} {Addr:172.20.0.2:58304 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:25.131694593Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.131 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.001250656}}}\n"}
-{"Time":"2023-03-29T13:37:25.131999693Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.131 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.127736Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.132733057Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.132 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:25.133220162Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.132 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.132982Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000985407}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.133869838Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.133 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.132982Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000985407}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.134272814Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.134404044Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:25.134672632Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.134517Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001250656}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.134983325Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.127736Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.135024699Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.135198837Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:25.135284701Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:25.135438825Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:25.135566373Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:25.135876201Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.134517Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001250656}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.136046662Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.136 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.136091872Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.136 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:25.136196604Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.136 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:25.136258441Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.136 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:25.143831824Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.143 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:25.143963014Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.143 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:25.181560631Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.181 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:60850 derp=1 derpdist=1v4:10ms\n"}
-{"Time":"2023-03-29T13:37:25.182840653Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.182 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:58304 derp=1 derpdist=1v4:8ms\n"}
-{"Time":"2023-03-29T13:37:25.197904655Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.197 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0LjuK] ...\n"}
-{"Time":"2023-03-29T13:37:25.198311943Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.198 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [0LjuK] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:25.199139623Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.199 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [S1KIY] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:25.199333828Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.199 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [0LjuK] d:a2fa54a4398f4b14 now using 172.20.0.2:60850\n"}
-{"Time":"2023-03-29T13:37:25.199614502Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.199 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0LjuK] ...\n"}
-{"Time":"2023-03-29T13:37:25.199798383Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.199 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0LjuK] ...\n"}
-{"Time":"2023-03-29T13:37:25.200531317Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.200 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:25.20096595Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.200 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:25.201076837Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.200 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:25.201187536Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:25.201294507Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:25.201406637Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:25.20154342Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Starting\n"}
-{"Time":"2023-03-29T13:37:25.201642014Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Sending handshake initiation\n"}
-{"Time":"2023-03-29T13:37:25.202494139Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.202 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [S1KIY] now active, reconfiguring WireGuard\n"}
-{"Time":"2023-03-29T13:37:25.202613281Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.202 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:25.203067239Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.202 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:25.203194112Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:25.203305469Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:25.203432836Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:25.203643635Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:25.203694677Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Starting\n"}
-{"Time":"2023-03-29T13:37:25.204047455Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Received handshake initiation\n"}
-{"Time":"2023-03-29T13:37:25.20407267Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.204 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Sending handshake response\n"}
-{"Time":"2023-03-29T13:37:25.204499058Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.204 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.204364109 +0000 UTC m=+1.843971335 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262}] LocalAddrs:[{Addr:127.0.0.1:60850 Type:stun} {Addr:172.20.0.2:60850 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:25.204985233Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.204 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Received handshake response\n"}
-{"Time":"2023-03-29T13:37:25.205043968Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.204 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [S1KIY] d:de63960686d4d969 now using 172.20.0.2:58304\n"}
-{"Time":"2023-03-29T13:37:25.205186973Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.205 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.205077383 +0000 UTC m=+1.844684599 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:25.204972346 +0000 UTC NodeKey:nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c}] LocalAddrs:[{Addr:127.0.0.1:58304 Type:stun} {Addr:172.20.0.2:58304 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:25.205653441Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.205 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [0LjuK] d:a2fa54a4398f4b14 now using 127.0.0.1:60850\n"}
-{"Time":"2023-03-29T13:37:25.228059809Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" agent_test.go:344: 2023-03-29 13:37:25.227: cmd: stdin: \"exit 0\\r\"\n"}
-{"Time":"2023-03-29T13:37:25.228276231Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:121: 2023-03-29 13:37:25.228: cmd: \"exit 0\"\n"}
-{"Time":"2023-03-29T13:37:25.235085539Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:58304 derp=1 derpdist=1v4:0s\n"}
-{"Time":"2023-03-29T13:37:25.235327678Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:60850 derp=1 derpdist=1v4:0s\n"}
-{"Time":"2023-03-29T13:37:25.235573892Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000235695}}}\n"}
-{"Time":"2023-03-29T13:37:25.235627639Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.235549Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000235695}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.235912714Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.235549Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000235695}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.2361447Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.236185457Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:25.23632231Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:25.2364646Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:25.23648571Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Sending keepalive packet\n"}
-{"Time":"2023-03-29T13:37:25.236535173Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:25.236646389Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000124794}}}\n"}
-{"Time":"2023-03-29T13:37:25.236691145Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.236616Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000124794}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.236953136Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.236616Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000124794}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
-{"Time":"2023-03-29T13:37:25.237148814Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:25.23717419Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:25.23729907Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:25.237442993Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:25.237465362Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Sending keepalive packet\n"}
-{"Time":"2023-03-29T13:37:25.23748091Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:25.237621079Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Receiving keepalive packet\n"}
-{"Time":"2023-03-29T13:37:25.237646187Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Receiving keepalive packet\n"}
-{"Time":"2023-03-29T13:37:25.57358248Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.573 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7): sending disco ping to [S1KIY] ...\n"}
-{"Time":"2023-03-29T13:37:26.073909695Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.073 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7): sending disco ping to [S1KIY] ...\n"}
-{"Time":"2023-03-29T13:37:26.573442313Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.573 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7): sending disco ping to [S1KIY] ...\n"}
-{"Time":"2023-03-29T13:37:26.685974077Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:83: 2023-03-29 13:37:26.685: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:26.686024343Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:74: 2023-03-29 13:37:26.685: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:26.686044434Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:110: 2023-03-29 13:37:26.685: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:26.686057034Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:111: 2023-03-29 13:37:26.685: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:26.686068789Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:113: 2023-03-29 13:37:26.685: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:26.686157867Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:76: 2023-03-29 13:37:26.686: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:26.686173026Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:74: 2023-03-29 13:37:26.686: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:26.686188025Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:76: 2023-03-29 13:37:26.686: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:26.686198978Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:74: 2023-03-29 13:37:26.686: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:26.686213128Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:76: 2023-03-29 13:37:26.686: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:26.686224275Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:102: 2023-03-29 13:37:26.686: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:26.686399517Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
-{"Time":"2023-03-29T13:37:26.686631954Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 2s\n"}
-{"Time":"2023-03-29T13:37:26.686672029Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:26.686752993Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:26.686793215Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:26.686883059Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:26.687010573Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:26.687040046Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:26.687106628Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Stopping\n"}
-{"Time":"2023-03-29T13:37:26.687190751Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:26.68730229Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:26.687319022Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
-{"Time":"2023-03-29T13:37:26.68739086Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
-{"Time":"2023-03-29T13:37:26.687791287Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 2s\n"}
-{"Time":"2023-03-29T13:37:26.687807566Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:26.687907277Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:26.687956258Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:26.68802861Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:26.688112368Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7): sending disco ping to [S1KIY] ...\n"}
-{"Time":"2023-03-29T13:37:26.688253692Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:26.688318345Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:26.688366659Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Stopping\n"}
-{"Time":"2023-03-29T13:37:26.688473063Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:26.688794731Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" stuntest.go:63: STUN server shutdown\n"}
-{"Time":"2023-03-29T13:37:26.688993708Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"--- PASS: TestAgent_Session_TTY_Hushlogin (1.69s)\n"}
-{"Time":"2023-03-29T13:37:26.689005169Z","Action":"pass","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_Hushlogin","Elapsed":1.69}
-{"Time":"2023-03-29T13:37:26.689017486Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput"}
-{"Time":"2023-03-29T13:37:26.689022846Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":"=== RUN TestAgent_Session_TTY_FastCommandHasOutput\n"}
-{"Time":"2023-03-29T13:37:26.689031231Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":"=== PAUSE TestAgent_Session_TTY_FastCommandHasOutput\n"}
-{"Time":"2023-03-29T13:37:26.689049237Z","Action":"pause","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput"}
-{"Time":"2023-03-29T13:37:26.689055783Z","Action":"run","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost"}
-{"Time":"2023-03-29T13:37:26.689060673Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":"=== RUN TestAgent_Session_TTY_HugeOutputIsNotLost\n"}
-{"Time":"2023-03-29T13:37:26.689069084Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":"=== PAUSE TestAgent_Session_TTY_HugeOutputIsNotLost\n"}
-{"Time":"2023-03-29T13:37:26.689074026Z","Action":"pause","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost"}
-{"Time":"2023-03-29T13:37:26.689083532Z","Action":"cont","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec"}
-{"Time":"2023-03-29T13:37:26.689090279Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":"=== CONT TestAgent_SessionExec\n"}
-{"Time":"2023-03-29T13:37:26.703787142Z","Action":"cont","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost"}
-{"Time":"2023-03-29T13:37:26.703825656Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":"=== CONT TestAgent_Session_TTY_HugeOutputIsNotLost\n"}
-{"Time":"2023-03-29T13:37:26.703839698Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":" agent_test.go:413: This test proves we have a bug where parts of large output on a PTY can be lost after the command exits, skipped to avoid test failures.\n"}
-{"Time":"2023-03-29T13:37:26.703868206Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":"--- SKIP: TestAgent_Session_TTY_HugeOutputIsNotLost (0.00s)\n"}
-{"Time":"2023-03-29T13:37:26.70388799Z","Action":"skip","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Elapsed":0}
-{"Time":"2023-03-29T13:37:26.703900175Z","Action":"cont","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput"}
-{"Time":"2023-03-29T13:37:26.703908033Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":"=== CONT TestAgent_Session_TTY_FastCommandHasOutput\n"}
-{"Time":"2023-03-29T13:37:26.723935151Z","Action":"cont","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode"}
-{"Time":"2023-03-29T13:37:26.723957967Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== CONT TestAgent_SessionTTYExitCode\n"}
-{"Time":"2023-03-29T13:37:26.744050676Z","Action":"cont","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell"}
-{"Time":"2023-03-29T13:37:26.744073511Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":"=== CONT TestAgent_SessionTTYShell\n"}
-{"Time":"2023-03-29T13:37:26.975908391Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:26.975935102Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:26.975941463Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:26.975992643Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:26.976027672Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:26.976097148Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:26.976137127Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:26.976202552Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:26.976237559Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:26.976273529Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:26.976300958Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:26.976417909Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:17b5066de479f458\n"}
-{"Time":"2023-03-29T13:37:26.976445133Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:26.976529694Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:26.976575591Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:26.976603915Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:26.976641835Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:26.97666441Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:26.976690087Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:26.97672446Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:26.976749123Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:26.976786013Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:26.976893189Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:26.977219957Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.977 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:26.977264288Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.977 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:26.977393997Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.977 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.015978257Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.015 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
-{"Time":"2023-03-29T13:37:27.016072388Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.015 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 55109, \"DERPPort\": 34655, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
-{"Time":"2023-03-29T13:37:27.016101997Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
-{"Time":"2023-03-29T13:37:27.016454205Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:27.016484809Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:27.016527444Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:27.016579577Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:27.016616504Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:27.01670613Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.016736272Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.016795884Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.01683051Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.016869184Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.016901207Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.017009443Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:e6f05f1260bbd611\n"}
-{"Time":"2023-03-29T13:37:27.017032499Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:27.017087268Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:27.017133219Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:27.017166541Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:27.017198439Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:27.017231784Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.017255027Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:27.017285867Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.017313159Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:27.017341323Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:27.017453136Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:27.017777899Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.01781684Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:27.017877499Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
-{"Time":"2023-03-29T13:37:27.017903073Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
-{"Time":"2023-03-29T13:37:27.017983662Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.017904Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.018477038Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.018 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:27.018878036Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.018 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:27.019251435Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.019 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:27.020053918Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.019 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:26.977307Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.020198673Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.020 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.020242577Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.020 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:27.020285833Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.020 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:27.020317905Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.020 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:27.036521314Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.036 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:27.036924425Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.036 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:27.037332816Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.037 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:27.03841778Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.017904Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.038434547Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.03852175Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\n"}
-{"Time":"2023-03-29T13:37:27.038608926Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:27.038696362Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:27.038810178Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:27.038824686Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.038858134Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:27.038891063Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:27.038924242Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:27.038937309Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:27.038976455Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.039988711Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.039 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:26.977307Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.040010425Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.039 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.040059771Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
-{"Time":"2023-03-29T13:37:27.040126554Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:27.040195696Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:27.040300593Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:27.040315089Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.040342986Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:27.040371935Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:27.040410694Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:27.040423938Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:27.040453889Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.096588631Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.096 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
-{"Time":"2023-03-29T13:37:27.096797479Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.096 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.097032423Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.096 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.09709667Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.097 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
-{"Time":"2023-03-29T13:37:27.12109969Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:27.121135169Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:27.121150972Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:27.121167311Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:27.121216222Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:27.121292057Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.12132273Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.121381219Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.121408684Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.121435744Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.121461695Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.121594822Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:c7f1bea9d6ff269c\n"}
-{"Time":"2023-03-29T13:37:27.121620573Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:27.121655676Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:27.121730239Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:27.121756777Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:27.121781901Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:27.121808996Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.121833529Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:27.121858413Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.121888455Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:27.121913023Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:27.122044906Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:27.12234719Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.122 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.122375547Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.122 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:27.122495597Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.122 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.136791704Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.136 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.136826981Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.136 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
-{"Time":"2023-03-29T13:37:27.13694108Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.136 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 48864, \"DERPPort\": 33963, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
-{"Time":"2023-03-29T13:37:27.136979053Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.136 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
-{"Time":"2023-03-29T13:37:27.137291589Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:27.137308609Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:27.13732063Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:27.137373985Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:27.137407965Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:27.137495219Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.137519229Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.137572028Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.137602411Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.137622846Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.137667976Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.137770053Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:59083cba13956f00\n"}
-{"Time":"2023-03-29T13:37:27.137794544Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:27.137898354Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:27.137934758Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:27.137955272Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:27.138002548Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:27.138024513Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.138043252Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:27.138068121Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.138095784Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:27.138122918Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:27.138231919Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:27.138531438Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.138556077Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:27.138636819Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
-{"Time":"2023-03-29T13:37:27.138658918Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
-{"Time":"2023-03-29T13:37:27.138754966Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.138663Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.139212045Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.139 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:27.139609965Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.139 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:27.141001091Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.140 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:27.143964866Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.143 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.122393Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.144498996Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.144 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.144562824Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.144 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:27.144649797Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.144 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:27.144702964Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.144 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:27.147050678Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.146 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:27.152240491Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.152 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:27.157654353Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.157 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:27.16338877Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.16375523Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.138663Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.163780585Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.163864424Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\n"}
-{"Time":"2023-03-29T13:37:27.163963441Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:27.164079841Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:27.164217744Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:27.164235438Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.16424974Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:27.164316222Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:27.164338403Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:27.164386486Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:27.1644261Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.165498628Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.122393Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.165527893Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.16555513Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
-{"Time":"2023-03-29T13:37:27.165643091Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:27.165737176Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:27.165892091Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:27.165909805Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.165924292Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:27.165977872Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:27.166008015Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:27.166054821Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.166 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:27.166085097Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.166 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.187154465Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.187 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.247520351Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.247 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.247693682Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.247 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
-{"Time":"2023-03-29T13:37:27.247900828Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.247 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
-{"Time":"2023-03-29T13:37:27.248074095Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.248 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.248118748Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.248 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.267591238Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.267 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.327992838Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.327 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
-{"Time":"2023-03-29T13:37:27.328026983Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.327 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:59384 derp=1 derpdist=1v4:83ms\n"}
-{"Time":"2023-03-29T13:37:27.328082537Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.328 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.328318974Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.328 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:59384 (stun), 172.20.0.2:59384 (local)\n"}
-{"Time":"2023-03-29T13:37:27.328415723Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.328 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.328311263 +0000 UTC m=+3.967918487 Peers:[] LocalAddrs:[{Addr:127.0.0.1:59384 Type:stun} {Addr:172.20.0.2:59384 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.331309331Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:27.331346835Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:27.331498355Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.331349704 +0000 UTC m=+3.970956931 Peers:[] LocalAddrs:[{Addr:127.0.0.1:59384 Type:stun} {Addr:172.20.0.2:59384 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.331576885Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.083211738}}}\n"}
-{"Time":"2023-03-29T13:37:27.331699503Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.328409Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.331945606Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.328409Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.33196728Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.332108335Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.332325739Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.332542999Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.332444Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.083211738}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.332790784Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.332444Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.083211738}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.333024686Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.333062782Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.333 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.333182232Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.333 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.333249208Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.333 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.348154607Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.348 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.353941907Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:27.353956013Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:27.353983717Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:27.354104135Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:27.354170721Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:27.354248787Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.354272858Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.35432815Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.354379756Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.354437701Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.354522443Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.35465091Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:049e454260a62aa1\n"}
-{"Time":"2023-03-29T13:37:27.354686785Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:27.354820076Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:27.35484141Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:27.354884035Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:27.354900747Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:27.354943262Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.354962224Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:27.354997734Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.355013636Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:27.355062521Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:27.355172916Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:27.355579603Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
-{"Time":"2023-03-29T13:37:27.355609674Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:45837 derp=1 derpdist=1v4:83ms\n"}
-{"Time":"2023-03-29T13:37:27.35566873Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.355827579Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:45837 (stun), 172.20.0.2:45837 (local)\n"}
-{"Time":"2023-03-29T13:37:27.355913155Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.355809216 +0000 UTC m=+3.995416428 Peers:[] LocalAddrs:[{Addr:127.0.0.1:45837 Type:stun} {Addr:172.20.0.2:45837 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.356261459Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
-{"Time":"2023-03-29T13:37:27.356354227Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 34688, \"DERPPort\": 43117, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
-{"Time":"2023-03-29T13:37:27.35638277Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
-{"Time":"2023-03-29T13:37:27.356686207Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:27.356708299Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:27.356723833Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:27.356833939Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:27.356850125Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:27.356945712Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.356968923Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.357036251Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.357070033Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.35711526Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.357134582Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.357249522Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:34ff526bdd502e84\n"}
-{"Time":"2023-03-29T13:37:27.35727419Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:27.35742285Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:27.357459674Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:27.357483314Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:27.357523236Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:27.357548281Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.357567638Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:27.357589503Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.357626515Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:27.357652913Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:27.357769486Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:27.358036076Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.358075196Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:27.358178009Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
-{"Time":"2023-03-29T13:37:27.358202627Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.358 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
-{"Time":"2023-03-29T13:37:27.358298603Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.358191Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.363703025Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.363 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:27.368878553Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.368 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:27.374094674Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.373 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:27.379724421Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.379782057Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:27.379915271Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.37996992Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:27.380020183Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:27.380128212Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.380012238 +0000 UTC m=+4.019619457 Peers:[] LocalAddrs:[{Addr:127.0.0.1:45837 Type:stun} {Addr:172.20.0.2:45837 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.380193848Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.082795986}}}\n"}
-{"Time":"2023-03-29T13:37:27.380276172Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.355895Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.38052742Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.355895Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.380550207Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.380665436Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.380879187Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:27.380925379Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:27.380952567Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:27.386338613Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.386 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:27.387267029Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.387 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:27.387774697Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.387 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:27.389160607Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.358191Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.389186972Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.389275389Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\n"}
-{"Time":"2023-03-29T13:37:27.389397112Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:27.38946191Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:27.389582352Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:27.389605243Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.389634845Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:27.389680241Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:27.389711656Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:27.389725148Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:27.389767299Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.389833677Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.379813Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.390055453Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.379813Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.390076169Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.390126429Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
-{"Time":"2023-03-29T13:37:27.390188645Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:27.39025637Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:27.390371704Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:27.39038785Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.390406815Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:27.390435981Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:27.390450102Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:27.39048039Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:27.390509523Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.390585223Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.390644997Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.39085229Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.390753Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.082795986}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.391046642Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.390753Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.082795986}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.391253711Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.391 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.391294825Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.391 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.391387255Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.391 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.391491088Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.391 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.408305611Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:27.408340875Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:27.408352182Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:27.408381704Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:27.41114267Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.41133032Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
-{"Time":"2023-03-29T13:37:27.411379291Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:35595 derp=1 derpdist=1v4:84ms\n"}
-{"Time":"2023-03-29T13:37:27.411463359Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.411622433Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:35595 (stun), 172.20.0.2:35595 (local)\n"}
-{"Time":"2023-03-29T13:37:27.411704928Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.411617207 +0000 UTC m=+4.051224425 Peers:[] LocalAddrs:[{Addr:127.0.0.1:35595 Type:stun} {Addr:172.20.0.2:35595 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.412030111Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.411 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
-{"Time":"2023-03-29T13:37:27.412110894Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 51906, \"DERPPort\": 41275, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
-{"Time":"2023-03-29T13:37:27.41214998Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
-{"Time":"2023-03-29T13:37:27.412464057Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
-{"Time":"2023-03-29T13:37:27.412485841Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
-{"Time":"2023-03-29T13:37:27.412515514Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
-{"Time":"2023-03-29T13:37:27.412564627Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
-{"Time":"2023-03-29T13:37:27.412620272Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:27.412695891Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.412734645Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.412796459Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.41282973Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.412882896Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.412921148Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.413034159Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:cc502d2065d3910d\n"}
-{"Time":"2023-03-29T13:37:27.413066135Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:27.413142789Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:27.413196003Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:27.413234614Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:27.413259313Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:27.413291621Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.413317957Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:27.413346325Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.413369117Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:27.413402783Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:27.413518268Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:27.413809664Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.413845445Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:27.413910962Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
-{"Time":"2023-03-29T13:37:27.413937365Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
-{"Time":"2023-03-29T13:37:27.414017708Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.413933Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.414064655Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:27.414111814Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:27.414189184Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.414107708 +0000 UTC m=+4.053714921 Peers:[] LocalAddrs:[{Addr:127.0.0.1:35595 Type:stun} {Addr:172.20.0.2:35595 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.414245907Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.083765437}}}\n"}
-{"Time":"2023-03-29T13:37:27.414312155Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.4117Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.414517803Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.4117Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.414535126Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.414623995Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.414849045Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:27.414905554Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
-{"Time":"2023-03-29T13:37:27.414936416Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:27.415525692Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.415 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:27.416019712Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.415 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:27.416475957Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.416 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:27.419029132Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.418 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.419447399Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.419 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.419321Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.083765437}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.42011877Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.419321Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.083765437}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.420376019Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.420736563Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.42087992Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.420979406Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.422951198Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.422 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
-{"Time":"2023-03-29T13:37:27.42298967Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.422 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:51685 derp=1 derpdist=1v4:84ms\n"}
-{"Time":"2023-03-29T13:37:27.423073917Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.423 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.423298046Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.423 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:51685 (stun), 172.20.0.2:51685 (local)\n"}
-{"Time":"2023-03-29T13:37:27.423385945Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.423 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.423272543 +0000 UTC m=+4.062879776 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51685 Type:stun} {Addr:172.20.0.2:51685 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.424393184Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:27.424445676Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:27.424603665Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.424442129 +0000 UTC m=+4.064049348 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51685 Type:stun} {Addr:172.20.0.2:51685 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.424655158Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.084146852}}}\n"}
-{"Time":"2023-03-29T13:37:27.424762683Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.423371Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.425045855Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.423371Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.425065382Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.425204191Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.425429938Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.425622509Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.425514Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.084146852}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.425845165Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.425514Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.084146852}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.426046304Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.426095567Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.426 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.426917109Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.426 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.426975294Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.426 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.427132044Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:59384 derp=1 derpdist=1v4:95ms\n"}
-{"Time":"2023-03-29T13:37:27.427810726Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
-{"Time":"2023-03-29T13:37:27.427890723Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.427915145Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.42800586Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.428030074Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.428084359Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.428115313Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
-{"Time":"2023-03-29T13:37:27.428260646Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:9c9ea8075f682592\n"}
-{"Time":"2023-03-29T13:37:27.428283953Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
-{"Time":"2023-03-29T13:37:27.428422249Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
-{"Time":"2023-03-29T13:37:27.428458364Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
-{"Time":"2023-03-29T13:37:27.428479111Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
-{"Time":"2023-03-29T13:37:27.428530647Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
-{"Time":"2023-03-29T13:37:27.428549605Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.428596203Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
-{"Time":"2023-03-29T13:37:27.428629468Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.42866001Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
-{"Time":"2023-03-29T13:37:27.428692965Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
-{"Time":"2023-03-29T13:37:27.42881653Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
-{"Time":"2023-03-29T13:37:27.430284733Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.094731332}}}\n"}
-{"Time":"2023-03-29T13:37:27.430366553Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.430277Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.094731332}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.430637096Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.430277Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.094731332}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.430871537Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.430971821Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:45837 derp=1 derpdist=1v4:37ms\n"}
-{"Time":"2023-03-29T13:37:27.436731495Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.436 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
-{"Time":"2023-03-29T13:37:27.441948864Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.441 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
-{"Time":"2023-03-29T13:37:27.442143804Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.442 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
-{"Time":"2023-03-29T13:37:27.447325985Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.447 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
-{"Time":"2023-03-29T13:37:27.452941046Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.452 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.452978923Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.452 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
-{"Time":"2023-03-29T13:37:27.453138084Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.453213493Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.453354362Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.45343882Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.037239154}}}\n"}
-{"Time":"2023-03-29T13:37:27.453524949Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.453418Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.037239154}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.453796409Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.453418Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.037239154}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.454022251Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.454053656Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.454164006Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.454402409Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.454452969Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
-{"Time":"2023-03-29T13:37:27.454661334Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.455180586Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.413933Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.45520381Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.455291748Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\n"}
-{"Time":"2023-03-29T13:37:27.455371391Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:27.45558374Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:27.455722626Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:27.455744892Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.455759263Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:27.455811196Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:27.455851745Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:27.455887337Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:27.45593531Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.456026705Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.453Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.456257727Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.453Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": []}}\n"}
-{"Time":"2023-03-29T13:37:27.456275526Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.456348245Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
-{"Time":"2023-03-29T13:37:27.456430865Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
-{"Time":"2023-03-29T13:37:27.456533237Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
-{"Time":"2023-03-29T13:37:27.456666838Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
-{"Time":"2023-03-29T13:37:27.456681257Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.456709567Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
-{"Time":"2023-03-29T13:37:27.456736103Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
-{"Time":"2023-03-29T13:37:27.456785483Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
-{"Time":"2023-03-29T13:37:27.456811479Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
-{"Time":"2023-03-29T13:37:27.456866526Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.456945718Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.457035437Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
-{"Time":"2023-03-29T13:37:27.490404055Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.490 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:35595 derp=1 derpdist=1v4:45ms\n"}
-{"Time":"2023-03-29T13:37:27.490957872Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.490 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:57709 derp=1 derpdist=1v4:31ms\n"}
-{"Time":"2023-03-29T13:37:27.491013794Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.490 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.491185914Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:57709 (stun), 172.20.0.2:57709 (local)\n"}
-{"Time":"2023-03-29T13:37:27.491295468Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.491174325 +0000 UTC m=+4.130781564 Peers:[] LocalAddrs:[{Addr:127.0.0.1:57709 Type:stun} {Addr:172.20.0.2:57709 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.491675303Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.045323829}}}\n"}
-{"Time":"2023-03-29T13:37:27.491767926Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.491649Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.045323829}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.492071167Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.491649Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.045323829}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.492303429Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.492381183Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.492509729Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.492832285Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:27.492955047Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:27.493041705Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.492824353 +0000 UTC m=+4.132431585 Peers:[] LocalAddrs:[{Addr:127.0.0.1:57709 Type:stun} {Addr:172.20.0.2:57709 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.493059245Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.031410894}}}\n"}
-{"Time":"2023-03-29T13:37:27.493174012Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.491286Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.493508739Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.491286Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.493558301Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.493658522Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.493954418Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.49414611Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.494032Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.031410894}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.494394917Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.494032Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.031410894}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.494635514Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.494676622Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.494780288Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.494840889Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.495169992Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.495 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:59384 derp=1 derpdist=1v4:64ms\n"}
-{"Time":"2023-03-29T13:37:27.497129169Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.497 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [XHSZg] ...\n"}
-{"Time":"2023-03-29T13:37:27.497196731Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.497 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:51685 derp=1 derpdist=1v4:71ms\n"}
-{"Time":"2023-03-29T13:37:27.498816504Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.498 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for [XHSZg]\n"}
-{"Time":"2023-03-29T13:37:27.498899237Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.498 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:27.498921801Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.498 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [XHSZg] set to derp-1 (their home)\n"}
-{"Time":"2023-03-29T13:37:27.499047332Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.498 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.498914775 +0000 UTC m=+4.138522011 Peers:[] LocalAddrs:[] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.499167908Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.070836569}}}\n"}
-{"Time":"2023-03-29T13:37:27.499241573Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.499162Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.070836569}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.49953116Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.499162Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.070836569}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.499756959Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.499820527Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.499954778Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.500094938Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.500 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.508017322Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.507 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:51993 derp=1 derpdist=1v4:48ms\n"}
-{"Time":"2023-03-29T13:37:27.50805261Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.507 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.508212683Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.508 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:51993 (stun), 172.20.0.2:51993 (local)\n"}
-{"Time":"2023-03-29T13:37:27.508285157Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.508 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.50819775 +0000 UTC m=+4.147804976 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51993 Type:stun} {Addr:172.20.0.2:51993 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.51148228Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.047681075}}}\n"}
-{"Time":"2023-03-29T13:37:27.511561169Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.50828Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.511646171Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [64qRi] ...\n"}
-{"Time":"2023-03-29T13:37:27.512004191Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.50828Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.512026173Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.512131402Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.512284203Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [64qRi] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:27.512446305Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.512355Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.047681075}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.512694091Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.512355Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.047681075}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.512945734Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.512982501Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.513074208Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.513149912Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.513458516Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
-{"Time":"2023-03-29T13:37:27.513601111Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:27.514167854Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
-{"Time":"2023-03-29T13:37:27.514216037Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:58992 derp=1 derpdist=1v4:62ms\n"}
-{"Time":"2023-03-29T13:37:27.514270704Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.51443958Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:58992 (stun), 172.20.0.2:58992 (local)\n"}
-{"Time":"2023-03-29T13:37:27.514519618Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.514434952 +0000 UTC m=+4.154042177 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.514591409Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.514515Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.514811275Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.514515Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.514831215Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.514927532Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.514969002Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:27.51501Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:27.515094247Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.515008199 +0000 UTC m=+4.154615430 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.515144167Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.062405899}}}\n"}
-{"Time":"2023-03-29T13:37:27.515231182Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.515155Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.062405899}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.515477352Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.515155Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.062405899}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.5157436Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.515810118Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.515944355Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.51603017Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.516114674Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.516 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.520145614Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.520 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [64qRi] d:e6f05f1260bbd611 now using 172.20.0.2:45837\n"}
-{"Time":"2023-03-29T13:37:27.520236787Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.520 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [64qRi] ...\n"}
-{"Time":"2023-03-29T13:37:27.520381164Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.520 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:27.523889233Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.523 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [64qRi] ...\n"}
-{"Time":"2023-03-29T13:37:27.524071718Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.524 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:27.525270748Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
-{"Time":"2023-03-29T13:37:27.525326669Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:34848 derp=1 derpdist=1v4:65ms\n"}
-{"Time":"2023-03-29T13:37:27.525374168Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.525530241Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:34848 (stun), 172.20.0.2:34848 (local)\n"}
-{"Time":"2023-03-29T13:37:27.525639565Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.525526859 +0000 UTC m=+4.165134074 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:0}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.525953664Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
-{"Time":"2023-03-29T13:37:27.525977204Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
-{"Time":"2023-03-29T13:37:27.526067972Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.525972278 +0000 UTC m=+4.165579492 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.526107105Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.065420657}}}\n"}
-{"Time":"2023-03-29T13:37:27.526188976Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.5256Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.526396168Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.5256Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.526422378Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.526517317Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.526687969Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
-{"Time":"2023-03-29T13:37:27.526875114Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.526766Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.065420657}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.527145691Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.526766Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.065420657}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.527368061Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.527462668Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.527579879Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.52765384Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.531819508Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.531 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:27.532053362Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.532237504Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:27.5322919Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:27.532342787Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:27.532383163Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:27.532430032Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:27.532468962Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - Starting\n"}
-{"Time":"2023-03-29T13:37:27.532522407Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - Sending handshake initiation\n"}
-{"Time":"2023-03-29T13:37:27.53317143Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.533 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:27.533306241Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.533 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:27.534331952Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [dv8u3] now active, reconfiguring WireGuard\n"}
-{"Time":"2023-03-29T13:37:27.534386848Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.53460606Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:27.534650003Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:27.53470262Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:27.534744335Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:27.534797239Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:27.534834361Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - Starting\n"}
-{"Time":"2023-03-29T13:37:27.535064975Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - Received handshake initiation\n"}
-{"Time":"2023-03-29T13:37:27.535101365Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - Sending handshake response\n"}
-{"Time":"2023-03-29T13:37:27.535486164Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [dv8u3] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:27.535646633Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.535517538 +0000 UTC m=+4.175124762 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370}] LocalAddrs:[{Addr:127.0.0.1:45837 Type:stun} {Addr:172.20.0.2:45837 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.535973095Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:27.536642888Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.536 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - Received handshake response\n"}
-{"Time":"2023-03-29T13:37:27.536713027Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.536 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [dv8u3] d:17b5066de479f458 now using 172.20.0.2:59384\n"}
-{"Time":"2023-03-29T13:37:27.536917351Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.536 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.536783767 +0000 UTC m=+4.176390982 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.536635319 +0000 UTC NodeKey:nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24}] LocalAddrs:[{Addr:127.0.0.1:59384 Type:stun} {Addr:172.20.0.2:59384 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.53763406Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
-{"Time":"2023-03-29T13:37:27.538314662Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.538 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4781:bb82:1540:3954:6a8): sending disco ping to [dv8u3] ...\n"}
-{"Time":"2023-03-29T13:37:27.542696821Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.542 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:35595 derp=1 derpdist=1v4:9ms\n"}
-{"Time":"2023-03-29T13:37:27.545658947Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.545 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:57709 derp=1 derpdist=1v4:15ms\n"}
-{"Time":"2023-03-29T13:37:27.546526665Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.546 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.014798468}}}\n"}
-{"Time":"2023-03-29T13:37:27.546624395Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.546 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.546513Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.014798468}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.546899956Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.546 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.546513Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.014798468}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.547131568Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.547 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.547206358Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.547 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.547317822Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.547 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.548159459Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
-{"Time":"2023-03-29T13:37:27.548281455Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
-{"Time":"2023-03-29T13:37:27.548330701Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:27.548397336Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.548439957Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:27.548494794Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:27.548600181Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.548645011Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.548694237Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - Stopping\n"}
-{"Time":"2023-03-29T13:37:27.548765909Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:27.548840457Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:27.548886886Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
-{"Time":"2023-03-29T13:37:27.548943901Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
-{"Time":"2023-03-29T13:37:27.549285716Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
-{"Time":"2023-03-29T13:37:27.549325844Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:27.549397271Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.549434623Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:27.549486154Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:27.549569105Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4781:bb82:1540:3954:6a8): sending disco ping to [dv8u3] ...\n"}
-{"Time":"2023-03-29T13:37:27.54967718Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.549720977Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.549763197Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - Stopping\n"}
-{"Time":"2023-03-29T13:37:27.549846472Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:27.55012971Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":" stuntest.go:63: STUN server shutdown\n"}
-{"Time":"2023-03-29T13:37:27.55014656Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Output":"--- PASS: TestAgent_SessionExec (0.86s)\n"}
-{"Time":"2023-03-29T13:37:27.562749894Z","Action":"pass","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionExec","Elapsed":0.86}
-{"Time":"2023-03-29T13:37:27.562803065Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.562 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:51993 derp=1 derpdist=1v4:8ms\n"}
-{"Time":"2023-03-29T13:37:27.563392796Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.563 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [XHSZg] ...\n"}
-{"Time":"2023-03-29T13:37:27.563801502Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.563 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.008279639}}}\n"}
-{"Time":"2023-03-29T13:37:27.56407208Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.563 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.563776Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.008279639}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.564819316Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.564 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.563776Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.008279639}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.565650461Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.565 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.565870529Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.565 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.566173208Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.566 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.567328308Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.567 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [V4gBN] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:27.567693064Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.567 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [XHSZg] d:cc502d2065d3910d now using 127.0.0.1:57709\n"}
-{"Time":"2023-03-29T13:37:27.567994501Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.567 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [XHSZg] ...\n"}
-{"Time":"2023-03-29T13:37:27.568226707Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.568 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [XHSZg] ...\n"}
-{"Time":"2023-03-29T13:37:27.56908417Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.568 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.569629095Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.569 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:27.569764065Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.569 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:27.569899377Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.569 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:27.570050145Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.569 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:27.57018252Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.570 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:27.570302009Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.570 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Starting\n"}
-{"Time":"2023-03-29T13:37:27.570580746Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.570 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Sending keepalive packet\n"}
-{"Time":"2023-03-29T13:37:27.570704044Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.570 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Sending handshake initiation\n"}
-{"Time":"2023-03-29T13:37:27.571723295Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.571 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [V4gBN] now active, reconfiguring WireGuard\n"}
-{"Time":"2023-03-29T13:37:27.571754224Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.571 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.571960017Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.571 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:27.5720063Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.571 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:27.572053253Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:27.572098018Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:27.572143378Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:27.572182651Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Starting\n"}
-{"Time":"2023-03-29T13:37:27.572412769Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Received handshake initiation\n"}
-{"Time":"2023-03-29T13:37:27.57244765Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Sending handshake response\n"}
-{"Time":"2023-03-29T13:37:27.572888396Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.57276895 +0000 UTC m=+4.212376174 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00}] LocalAddrs:[{Addr:127.0.0.1:57709 Type:stun} {Addr:172.20.0.2:57709 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.573422095Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.573 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Received handshake response\n"}
-{"Time":"2023-03-29T13:37:27.573505226Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.573 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [V4gBN] d:9c9ea8075f682592 now using 172.20.0.2:51993\n"}
-{"Time":"2023-03-29T13:37:27.573673421Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.573 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.573556739 +0000 UTC m=+4.213163954 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.573422752 +0000 UTC NodeKey:nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907}] LocalAddrs:[{Addr:127.0.0.1:51993 Type:stun} {Addr:172.20.0.2:51993 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.574036177Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.573 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Receiving keepalive packet\n"}
-{"Time":"2023-03-29T13:37:27.578151189Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:58992 derp=1 derpdist=1v4:5ms\n"}
-{"Time":"2023-03-29T13:37:27.578464111Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:34848 derp=1 derpdist=1v4:7ms\n"}
-{"Time":"2023-03-29T13:37:27.578697845Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.005291379}}}\n"}
-{"Time":"2023-03-29T13:37:27.578788271Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.578687Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.005291379}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.579040964Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.578687Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.005291379}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.579269912Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.579338921Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.579486192Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.579615554Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.00675754}}}\n"}
-{"Time":"2023-03-29T13:37:27.579715409Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.579606Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.00675754}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.580015856Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.579606Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.00675754}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
-{"Time":"2023-03-29T13:37:27.580200069Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
-{"Time":"2023-03-29T13:37:27.580263003Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
-{"Time":"2023-03-29T13:37:27.580357379Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
-{"Time":"2023-03-29T13:37:27.584912838Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.584 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:836\u003e\t(*agent).init.func2\tssh session returned\t{\"error\": \"exit status 127\"}\n"}
-{"Time":"2023-03-29T13:37:27.585085871Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.585 [WARN]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:1119\u003e\t(*agent).handleSSHSession.func2\tfailed to resize tty ...\n"}
-{"Time":"2023-03-29T13:37:27.58510059Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" \"error\": pty: closed:\n"}
-{"Time":"2023-03-29T13:37:27.585107445Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" github.com/coder/coder/pty.(*otherPty).Close\n"}
-{"Time":"2023-03-29T13:37:27.585113814Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" /home/mafredri/src/coder/coder/pty/pty_other.go:134\n"}
-{"Time":"2023-03-29T13:37:27.585512242Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:83: 2023-03-29 13:37:27.585: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:27.585526871Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:74: 2023-03-29 13:37:27.585: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:27.58556302Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:110: 2023-03-29 13:37:27.585: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:27.585579238Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:111: 2023-03-29 13:37:27.585: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:27.585599261Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:113: 2023-03-29 13:37:27.585: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:27.58566446Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:76: 2023-03-29 13:37:27.585: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:27.585677492Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:74: 2023-03-29 13:37:27.585: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:27.585682384Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:76: 2023-03-29 13:37:27.585: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:27.585699939Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:74: 2023-03-29 13:37:27.585: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:27.58570508Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:76: 2023-03-29 13:37:27.585: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:27.585722893Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:102: 2023-03-29 13:37:27.585: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:27.585976831Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.585 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
-{"Time":"2023-03-29T13:37:27.586016625Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.585 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:27.586092293Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.586133054Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:27.586200024Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:27.586329617Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.586371206Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.586499602Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Stopping\n"}
-{"Time":"2023-03-29T13:37:27.586608393Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:27.58672256Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:27.586761915Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
-{"Time":"2023-03-29T13:37:27.586833422Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
-{"Time":"2023-03-29T13:37:27.5869144Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
-{"Time":"2023-03-29T13:37:27.58723746Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
-{"Time":"2023-03-29T13:37:27.58728708Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:27.587374552Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.587490464Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4743:8dab:c855:f633:532): sending disco ping to [V4gBN] ...\n"}
-{"Time":"2023-03-29T13:37:27.587617597Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.598290287Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:57709 derp=1 derpdist=1v4:3ms\n"}
-{"Time":"2023-03-29T13:37:27.598667071Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.598735879Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:27.598824991Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:27.598917706Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Stopping\n"}
-{"Time":"2023-03-29T13:37:27.599077451Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.599 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:27.599268425Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":" stuntest.go:63: STUN server shutdown\n"}
-{"Time":"2023-03-29T13:37:27.599297466Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Output":"--- PASS: TestAgent_SessionTTYExitCode (0.88s)\n"}
-{"Time":"2023-03-29T13:37:27.624884929Z","Action":"pass","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYExitCode","Elapsed":0.88}
-{"Time":"2023-03-29T13:37:27.624923874Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.624 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n"}
-{"Time":"2023-03-29T13:37:27.625357585Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.625 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [5WitN] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:27.626340882Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [5EOvJ] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:27.626577093Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5WitN] d:34ff526bdd502e84 now using 172.20.0.2:34848\n"}
-{"Time":"2023-03-29T13:37:27.626873494Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n"}
-{"Time":"2023-03-29T13:37:27.627094808Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n"}
-{"Time":"2023-03-29T13:37:27.627846604Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.627 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.628133034Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:27.628178673Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:27.628231143Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:27.628273924Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:27.628326843Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:27.628365041Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Starting\n"}
-{"Time":"2023-03-29T13:37:27.628423532Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Sending handshake initiation\n"}
-{"Time":"2023-03-29T13:37:27.62897967Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [5EOvJ] now active, reconfiguring WireGuard\n"}
-{"Time":"2023-03-29T13:37:27.62904036Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.629335987Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:27.629378533Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:27.629437221Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:27.629484031Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:27.629528288Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:27.629564272Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Starting\n"}
-{"Time":"2023-03-29T13:37:27.629899772Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Received handshake initiation\n"}
-{"Time":"2023-03-29T13:37:27.629937538Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Sending handshake response\n"}
-{"Time":"2023-03-29T13:37:27.630550619Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.630 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.630405833 +0000 UTC m=+4.270013057 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c}] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.631298672Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Received handshake response\n"}
-{"Time":"2023-03-29T13:37:27.631395745Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5EOvJ] d:049e454260a62aa1 now using 172.20.0.2:58992\n"}
-{"Time":"2023-03-29T13:37:27.631619667Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.631481797 +0000 UTC m=+4.271089021 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.631305354 +0000 UTC NodeKey:nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f}] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.632260898Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.632 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5WitN] d:34ff526bdd502e84 now using 127.0.0.1:34848\n"}
-{"Time":"2023-03-29T13:37:27.637293337Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.637 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [Xlu3R] ...\n"}
-{"Time":"2023-03-29T13:37:27.637491883Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.637 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [Xlu3R] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:27.638607677Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.638 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [uWfac] set to derp-1 (shared home)\n"}
-{"Time":"2023-03-29T13:37:27.6387189Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.638 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [Xlu3R] d:59083cba13956f00 now using 172.20.0.2:35595\n"}
-{"Time":"2023-03-29T13:37:27.638794177Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.638 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [Xlu3R] ...\n"}
-{"Time":"2023-03-29T13:37:27.638932756Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.638 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [Xlu3R] ...\n"}
-{"Time":"2023-03-29T13:37:27.639446274Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.639629272Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:27.639676449Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:27.639710523Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:27.639738112Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:27.639767671Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:27.639792041Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - Starting\n"}
-{"Time":"2023-03-29T13:37:27.639832214Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - Sending handshake initiation\n"}
-{"Time":"2023-03-29T13:37:27.640359836Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [uWfac] now active, reconfiguring WireGuard\n"}
-{"Time":"2023-03-29T13:37:27.640405214Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
-{"Time":"2023-03-29T13:37:27.640596752Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Created\n"}
-{"Time":"2023-03-29T13:37:27.64062514Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Updating endpoint\n"}
-{"Time":"2023-03-29T13:37:27.640660936Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Removing all allowedips\n"}
-{"Time":"2023-03-29T13:37:27.640688925Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Adding allowedip\n"}
-{"Time":"2023-03-29T13:37:27.640721293Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Updating persistent keepalive interval\n"}
-{"Time":"2023-03-29T13:37:27.640745599Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - Starting\n"}
-{"Time":"2023-03-29T13:37:27.640964666Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - Received handshake initiation\n"}
-{"Time":"2023-03-29T13:37:27.640987155Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - Sending handshake response\n"}
-{"Time":"2023-03-29T13:37:27.641427103Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.641 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.641327519 +0000 UTC m=+4.280934743 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e}] LocalAddrs:[{Addr:127.0.0.1:35595 Type:stun} {Addr:172.20.0.2:35595 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.642337602Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.642 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - Received handshake response\n"}
-{"Time":"2023-03-29T13:37:27.642405776Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.642 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [uWfac] d:c7f1bea9d6ff269c now using 172.20.0.2:51685\n"}
-{"Time":"2023-03-29T13:37:27.642612703Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.642 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.64250087 +0000 UTC m=+4.282108085 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.642336966 +0000 UTC NodeKey:nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56}] LocalAddrs:[{Addr:127.0.0.1:51685 Type:stun} {Addr:172.20.0.2:51685 Type:local}] DERPs:1}\", \"err\": null}\n"}
-{"Time":"2023-03-29T13:37:27.64326778Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.643 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [Xlu3R] d:59083cba13956f00 now using 127.0.0.1:35595\n"}
-{"Time":"2023-03-29T13:37:27.648583243Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" agent_test.go:400: \n"}
-{"Time":"2023-03-29T13:37:27.648599647Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/agent/agent_test.go:400\n"}
-{"Time":"2023-03-29T13:37:27.648605577Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \t \t\t\t\t/home/mafredri/src/coder/coder/agent/agent_test.go:401\n"}
-{"Time":"2023-03-29T13:37:27.648610117Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \tError: \t\"\" does not contain \"wazzup\"\n"}
-{"Time":"2023-03-29T13:37:27.648616547Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \tTest: \tTestAgent_Session_TTY_FastCommandHasOutput\n"}
-{"Time":"2023-03-29T13:37:27.648621257Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \tMessages: \tshould output greeting\n"}
-{"Time":"2023-03-29T13:37:27.648628226Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:83: 2023-03-29 13:37:27.648: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:27.648632598Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:27.648669381Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:110: 2023-03-29 13:37:27.648: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:27.648676209Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:111: 2023-03-29 13:37:27.648: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:27.64868565Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:113: 2023-03-29 13:37:27.648: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:27.648725455Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:27.648735417Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:27.648742887Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:27.648755628Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:27.648763111Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:27.648781723Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:102: 2023-03-29 13:37:27.648: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:27.64898407Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
-{"Time":"2023-03-29T13:37:27.64901264Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:27.649065867Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.649092046Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:27.649138332Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:27.649234264Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.649272903Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.649300503Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Stopping\n"}
-{"Time":"2023-03-29T13:37:27.649378408Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:27.649434677Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
-{"Time":"2023-03-29T13:37:27.649514069Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:27.649551066Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
-{"Time":"2023-03-29T13:37:27.649591609Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
-{"Time":"2023-03-29T13:37:27.649899898Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
-{"Time":"2023-03-29T13:37:27.649921785Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:27.649976578Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.650005597Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:27.650053977Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:27.650112159Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d): sending disco ping to [5EOvJ] ...\n"}
-{"Time":"2023-03-29T13:37:27.650212974Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.650252149Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.650280078Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Stopping\n"}
-{"Time":"2023-03-29T13:37:27.650357784Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:27.650618927Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" stuntest.go:63: STUN server shutdown\n"}
-{"Time":"2023-03-29T13:37:27.650634125Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":"--- FAIL: TestAgent_Session_TTY_FastCommandHasOutput (0.95s)\n"}
-{"Time":"2023-03-29T13:37:27.674793681Z","Action":"fail","Package":"github.com/coder/coder/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Elapsed":0.95}
-{"Time":"2023-03-29T13:37:27.674817479Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.674 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805): sending disco ping to [uWfac] ...\n"}
-{"Time":"2023-03-29T13:37:27.675734168Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" agent_test.go:235: 2023-03-29 13:37:27.675: cmd: peeked 1/1 bytes = \"$\"\n"}
-{"Time":"2023-03-29T13:37:27.675774216Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" agent_test.go:236: 2023-03-29 13:37:27.675: cmd: stdin: \"echo test\\r\"\n"}
-{"Time":"2023-03-29T13:37:27.676191344Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.676: cmd: \"$ echo test\"\n"}
-{"Time":"2023-03-29T13:37:27.676273026Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" agent_test.go:237: 2023-03-29 13:37:27.676: cmd: matched \"test\" = \"$ echo test\"\n"}
-{"Time":"2023-03-29T13:37:27.676308536Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" agent_test.go:238: 2023-03-29 13:37:27.676: cmd: stdin: \"exit\\r\"\n"}
-{"Time":"2023-03-29T13:37:27.676642013Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.676: cmd: \"exit\"\n"}
-{"Time":"2023-03-29T13:37:27.67773278Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.677: cmd: \"echo test\\r\"\n"}
-{"Time":"2023-03-29T13:37:27.677752958Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.677: cmd: \"exit\\r\"\n"}
-{"Time":"2023-03-29T13:37:27.67778936Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.677: cmd: \"test\\r\"\n"}
-{"Time":"2023-03-29T13:37:27.678126711Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:83: 2023-03-29 13:37:27.678: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:27.678143306Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:74: 2023-03-29 13:37:27.678: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:27.678200486Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:110: 2023-03-29 13:37:27.678: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:27.678223246Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:111: 2023-03-29 13:37:27.678: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:27.678238993Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:113: 2023-03-29 13:37:27.678: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:27.678305772Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:76: 2023-03-29 13:37:27.678: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:27.678359399Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:74: 2023-03-29 13:37:27.678: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:27.678373082Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:76: 2023-03-29 13:37:27.678: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:27.678387877Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:74: 2023-03-29 13:37:27.678: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:27.678398091Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:76: 2023-03-29 13:37:27.678: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:27.678444105Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.678: cmd: \"$ \"\n"}
-{"Time":"2023-03-29T13:37:27.67847139Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:102: 2023-03-29 13:37:27.678: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:27.67876363Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.678 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
-{"Time":"2023-03-29T13:37:27.678920913Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.678 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
-{"Time":"2023-03-29T13:37:27.678978615Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.678 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:27.679093854Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.67916374Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:27.679261738Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:27.679498576Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.679558488Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.679635937Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - Stopping\n"}
-{"Time":"2023-03-29T13:37:27.679772893Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:27.679951878Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
-{"Time":"2023-03-29T13:37:27.680029583Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
-{"Time":"2023-03-29T13:37:27.680147794Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
-{"Time":"2023-03-29T13:37:27.680759898Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
-{"Time":"2023-03-29T13:37:27.68081749Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
-{"Time":"2023-03-29T13:37:27.680933631Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
-{"Time":"2023-03-29T13:37:27.681009542Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
-{"Time":"2023-03-29T13:37:27.681106332Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
-{"Time":"2023-03-29T13:37:27.681249372Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805): sending disco ping to [uWfac] ...\n"}
-{"Time":"2023-03-29T13:37:27.681454735Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.681540218Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
-{"Time":"2023-03-29T13:37:27.68161487Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - Stopping\n"}
-{"Time":"2023-03-29T13:37:27.681781085Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
-{"Time":"2023-03-29T13:37:27.682219467Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":" stuntest.go:63: STUN server shutdown\n"}
-{"Time":"2023-03-29T13:37:27.682247508Z","Action":"output","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Output":"--- PASS: TestAgent_SessionTTYShell (0.94s)\n"}
-{"Time":"2023-03-29T13:37:27.682263381Z","Action":"pass","Package":"github.com/coder/coder/agent","Test":"TestAgent_SessionTTYShell","Elapsed":0.94}
-{"Time":"2023-03-29T13:37:27.682278577Z","Action":"output","Package":"github.com/coder/coder/agent","Output":"FAIL\n"}
-{"Time":"2023-03-29T13:37:27.696326667Z","Action":"output","Package":"github.com/coder/coder/agent","Output":"FAIL\tgithub.com/coder/coder/agent\t4.341s\n"}
-{"Time":"2023-03-29T13:37:27.696360103Z","Action":"fail","Package":"github.com/coder/coder/agent","Elapsed":4.341}
-{"Time":"2023-03-29T13:37:32.643934624Z","Action":"start","Package":"github.com/coder/coder/cli"}
-{"Time":"2023-03-29T13:37:32.790842698Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser"}
-{"Time":"2023-03-29T13:37:32.79088125Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser","Output":"=== RUN TestServerCreateAdminUser\n"}
-{"Time":"2023-03-29T13:37:32.792730073Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK"}
-{"Time":"2023-03-29T13:37:32.792739078Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":"=== RUN TestServerCreateAdminUser/OK\n"}
-{"Time":"2023-03-29T13:37:32.792745576Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":"=== PAUSE TestServerCreateAdminUser/OK\n"}
-{"Time":"2023-03-29T13:37:32.79274818Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK"}
-{"Time":"2023-03-29T13:37:32.79275173Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env"}
-{"Time":"2023-03-29T13:37:32.792754236Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":"=== RUN TestServerCreateAdminUser/Env\n"}
-{"Time":"2023-03-29T13:37:32.792759492Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":"=== PAUSE TestServerCreateAdminUser/Env\n"}
-{"Time":"2023-03-29T13:37:32.792763227Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env"}
-{"Time":"2023-03-29T13:37:32.792767605Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin"}
-{"Time":"2023-03-29T13:37:32.792772306Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":"=== RUN TestServerCreateAdminUser/Stdin\n"}
-{"Time":"2023-03-29T13:37:32.792778719Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":"=== PAUSE TestServerCreateAdminUser/Stdin\n"}
-{"Time":"2023-03-29T13:37:32.792781324Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin"}
-{"Time":"2023-03-29T13:37:32.792785642Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates"}
-{"Time":"2023-03-29T13:37:32.792788132Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":"=== RUN TestServerCreateAdminUser/Validates\n"}
-{"Time":"2023-03-29T13:37:32.792833072Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":"=== PAUSE TestServerCreateAdminUser/Validates\n"}
-{"Time":"2023-03-29T13:37:32.792839332Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates"}
-{"Time":"2023-03-29T13:37:32.792849881Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK"}
-{"Time":"2023-03-29T13:37:32.79285277Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":"=== CONT TestServerCreateAdminUser/OK\n"}
-{"Time":"2023-03-29T13:37:32.794003473Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" server_createadminuser_test.go:87: \n"}
-{"Time":"2023-03-29T13:37:32.794008803Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:87\n"}
-{"Time":"2023-03-29T13:37:32.794012119Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \tError: \tReceived unexpected error:\n"}
-{"Time":"2023-03-29T13:37:32.794014928Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \tcould not start resource:\n"}
-{"Time":"2023-03-29T13:37:32.794017745Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.794020968Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
-{"Time":"2023-03-29T13:37:32.794025025Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
-{"Time":"2023-03-29T13:37:32.794028396Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \n"}
-{"Time":"2023-03-29T13:37:32.794031694Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
-{"Time":"2023-03-29T13:37:32.794034996Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
-{"Time":"2023-03-29T13:37:32.794038934Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.794042353Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
-{"Time":"2023-03-29T13:37:32.794045324Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t github.com/coder/coder/cli_test.TestServerCreateAdminUser.func2\n"}
-{"Time":"2023-03-29T13:37:32.794048191Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:86\n"}
-{"Time":"2023-03-29T13:37:32.794051013Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t testing.tRunner\n"}
-{"Time":"2023-03-29T13:37:32.794053771Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
-{"Time":"2023-03-29T13:37:32.794056664Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t runtime.goexit\n"}
-{"Time":"2023-03-29T13:37:32.794060193Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
-{"Time":"2023-03-29T13:37:32.79406303Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":" \tTest: \tTestServerCreateAdminUser/OK\n"}
-{"Time":"2023-03-29T13:37:32.79407275Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Output":"--- FAIL: TestServerCreateAdminUser/OK (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.794075922Z","Action":"fail","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/OK","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.794079842Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates"}
-{"Time":"2023-03-29T13:37:32.794082413Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":"=== CONT TestServerCreateAdminUser/Validates\n"}
-{"Time":"2023-03-29T13:37:32.795185256Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" server_createadminuser_test.go:227: \n"}
-{"Time":"2023-03-29T13:37:32.795189907Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:227\n"}
-{"Time":"2023-03-29T13:37:32.795192879Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \tError: \tReceived unexpected error:\n"}
-{"Time":"2023-03-29T13:37:32.795195707Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \tcould not start resource:\n"}
-{"Time":"2023-03-29T13:37:32.795198569Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.795203724Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
-{"Time":"2023-03-29T13:37:32.795206991Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
-{"Time":"2023-03-29T13:37:32.795209975Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \n"}
-{"Time":"2023-03-29T13:37:32.795212879Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
-{"Time":"2023-03-29T13:37:32.79521788Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
-{"Time":"2023-03-29T13:37:32.795223388Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.795228236Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
-{"Time":"2023-03-29T13:37:32.795231388Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t github.com/coder/coder/cli_test.TestServerCreateAdminUser.func5\n"}
-{"Time":"2023-03-29T13:37:32.795234339Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:226\n"}
-{"Time":"2023-03-29T13:37:32.795237524Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t testing.tRunner\n"}
-{"Time":"2023-03-29T13:37:32.795240439Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
-{"Time":"2023-03-29T13:37:32.795243318Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t runtime.goexit\n"}
-{"Time":"2023-03-29T13:37:32.795246653Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
-{"Time":"2023-03-29T13:37:32.795249486Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \tTest: \tTestServerCreateAdminUser/Validates\n"}
-{"Time":"2023-03-29T13:37:32.795256993Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Output":"--- FAIL: TestServerCreateAdminUser/Validates (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.795260148Z","Action":"fail","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Validates","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.795262834Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin"}
-{"Time":"2023-03-29T13:37:32.795265403Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":"=== CONT TestServerCreateAdminUser/Stdin\n"}
-{"Time":"2023-03-29T13:37:32.795769577Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" server_createadminuser_test.go:187: \n"}
-{"Time":"2023-03-29T13:37:32.795774301Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:187\n"}
-{"Time":"2023-03-29T13:37:32.795777433Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \tError: \tReceived unexpected error:\n"}
-{"Time":"2023-03-29T13:37:32.795782206Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \tcould not start resource:\n"}
-{"Time":"2023-03-29T13:37:32.795787591Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.795793763Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
-{"Time":"2023-03-29T13:37:32.795796926Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
-{"Time":"2023-03-29T13:37:32.795799647Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \n"}
-{"Time":"2023-03-29T13:37:32.795802415Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
-{"Time":"2023-03-29T13:37:32.795805244Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
-{"Time":"2023-03-29T13:37:32.795808186Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.79581094Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
-{"Time":"2023-03-29T13:37:32.795813828Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t github.com/coder/coder/cli_test.TestServerCreateAdminUser.func4\n"}
-{"Time":"2023-03-29T13:37:32.79581676Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:186\n"}
-{"Time":"2023-03-29T13:37:32.795819484Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t testing.tRunner\n"}
-{"Time":"2023-03-29T13:37:32.795822305Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
-{"Time":"2023-03-29T13:37:32.795826355Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t runtime.goexit\n"}
-{"Time":"2023-03-29T13:37:32.795829152Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
-{"Time":"2023-03-29T13:37:32.795832058Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \tTest: \tTestServerCreateAdminUser/Stdin\n"}
-{"Time":"2023-03-29T13:37:32.795846761Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Output":"--- FAIL: TestServerCreateAdminUser/Stdin (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.79585045Z","Action":"fail","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Stdin","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.795853001Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env"}
-{"Time":"2023-03-29T13:37:32.795855433Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":"=== CONT TestServerCreateAdminUser/Env\n"}
-{"Time":"2023-03-29T13:37:32.796339444Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" server_createadminuser_test.go:153: \n"}
-{"Time":"2023-03-29T13:37:32.796345738Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:153\n"}
-{"Time":"2023-03-29T13:37:32.796349118Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \tError: \tReceived unexpected error:\n"}
-{"Time":"2023-03-29T13:37:32.796351839Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \tcould not start resource:\n"}
-{"Time":"2023-03-29T13:37:32.796354772Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.796357683Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
-{"Time":"2023-03-29T13:37:32.796360546Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
-{"Time":"2023-03-29T13:37:32.79636323Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \n"}
-{"Time":"2023-03-29T13:37:32.796366079Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
-{"Time":"2023-03-29T13:37:32.796368987Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
-{"Time":"2023-03-29T13:37:32.79637254Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.79637535Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
-{"Time":"2023-03-29T13:37:32.796378207Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t github.com/coder/coder/cli_test.TestServerCreateAdminUser.func3\n"}
-{"Time":"2023-03-29T13:37:32.796381015Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:152\n"}
-{"Time":"2023-03-29T13:37:32.796383831Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t testing.tRunner\n"}
-{"Time":"2023-03-29T13:37:32.796386544Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
-{"Time":"2023-03-29T13:37:32.796389783Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t runtime.goexit\n"}
-{"Time":"2023-03-29T13:37:32.796394885Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
-{"Time":"2023-03-29T13:37:32.796400461Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":" \tTest: \tTestServerCreateAdminUser/Env\n"}
-{"Time":"2023-03-29T13:37:32.796405793Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Output":"--- FAIL: TestServerCreateAdminUser/Env (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.796409018Z","Action":"fail","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser/Env","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.796411751Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser","Output":"--- FAIL: TestServerCreateAdminUser (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.796414761Z","Action":"fail","Package":"github.com/coder/coder/cli","Test":"TestServerCreateAdminUser","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.796417599Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer"}
-{"Time":"2023-03-29T13:37:32.796420175Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer","Output":"=== RUN TestServer\n"}
-{"Time":"2023-03-29T13:37:32.796424448Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Production"}
-{"Time":"2023-03-29T13:37:32.796426853Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":"=== RUN TestServer/Production\n"}
-{"Time":"2023-03-29T13:37:32.797198344Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" server_test.go:109: \n"}
-{"Time":"2023-03-29T13:37:32.797204437Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_test.go:109\n"}
-{"Time":"2023-03-29T13:37:32.797207471Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \tError: \tReceived unexpected error:\n"}
-{"Time":"2023-03-29T13:37:32.797210203Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \tcould not start resource:\n"}
-{"Time":"2023-03-29T13:37:32.797213234Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.797216169Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
-{"Time":"2023-03-29T13:37:32.797219132Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
-{"Time":"2023-03-29T13:37:32.797221978Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t \n"}
-{"Time":"2023-03-29T13:37:32.797224749Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
-{"Time":"2023-03-29T13:37:32.797227673Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
-{"Time":"2023-03-29T13:37:32.797230597Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t github.com/coder/coder/coderd/database/postgres.Open\n"}
-{"Time":"2023-03-29T13:37:32.797234931Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
-{"Time":"2023-03-29T13:37:32.797243511Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t github.com/coder/coder/cli_test.TestServer.func1\n"}
-{"Time":"2023-03-29T13:37:32.797248163Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_test.go:108\n"}
-{"Time":"2023-03-29T13:37:32.79725113Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t testing.tRunner\n"}
-{"Time":"2023-03-29T13:37:32.797253983Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
-{"Time":"2023-03-29T13:37:32.797257318Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t runtime.goexit\n"}
-{"Time":"2023-03-29T13:37:32.797261252Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
-{"Time":"2023-03-29T13:37:32.797266495Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":" \tTest: \tTestServer/Production\n"}
-{"Time":"2023-03-29T13:37:32.797274297Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Output":"--- FAIL: TestServer/Production (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.797277522Z","Action":"fail","Package":"github.com/coder/coder/cli","Test":"TestServer/Production","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.797280535Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres"}
-{"Time":"2023-03-29T13:37:32.797283019Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":"=== RUN TestServer/BuiltinPostgres\n"}
-{"Time":"2023-03-29T13:37:32.797286137Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":"=== PAUSE TestServer/BuiltinPostgres\n"}
-{"Time":"2023-03-29T13:37:32.797288614Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres"}
-{"Time":"2023-03-29T13:37:32.797294343Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL"}
-{"Time":"2023-03-29T13:37:32.797296802Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":"=== RUN TestServer/BuiltinPostgresURL\n"}
-{"Time":"2023-03-29T13:37:32.797299815Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":"=== PAUSE TestServer/BuiltinPostgresURL\n"}
-{"Time":"2023-03-29T13:37:32.797302293Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL"}
-{"Time":"2023-03-29T13:37:32.797306699Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw"}
-{"Time":"2023-03-29T13:37:32.797309403Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":"=== RUN TestServer/BuiltinPostgresURLRaw\n"}
-{"Time":"2023-03-29T13:37:32.79731478Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":"=== PAUSE TestServer/BuiltinPostgresURLRaw\n"}
-{"Time":"2023-03-29T13:37:32.797319293Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw"}
-{"Time":"2023-03-29T13:37:32.797324667Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL"}
-{"Time":"2023-03-29T13:37:32.797328467Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":"=== RUN TestServer/LocalAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.797331431Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":"=== PAUSE TestServer/LocalAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.797333768Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL"}
-{"Time":"2023-03-29T13:37:32.797341584Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL"}
-{"Time":"2023-03-29T13:37:32.797345334Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":"=== RUN TestServer/RemoteAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.79737723Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":"=== PAUSE TestServer/RemoteAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.797380853Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL"}
-{"Time":"2023-03-29T13:37:32.797385247Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL"}
-{"Time":"2023-03-29T13:37:32.797387813Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":"=== RUN TestServer/NoWarningWithRemoteAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.797405636Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":"=== PAUSE TestServer/NoWarningWithRemoteAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.797408839Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL"}
-{"Time":"2023-03-29T13:37:32.797426981Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/NoSchemeAccessURL"}
-{"Time":"2023-03-29T13:37:32.797430439Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoSchemeAccessURL","Output":"=== RUN TestServer/NoSchemeAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.797434847Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoSchemeAccessURL","Output":"=== PAUSE TestServer/NoSchemeAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.797437343Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/NoSchemeAccessURL"}
-{"Time":"2023-03-29T13:37:32.797445885Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadVersion"}
-{"Time":"2023-03-29T13:37:32.797448325Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadVersion","Output":"=== RUN TestServer/TLSBadVersion\n"}
-{"Time":"2023-03-29T13:37:32.797466497Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadVersion","Output":"=== PAUSE TestServer/TLSBadVersion\n"}
-{"Time":"2023-03-29T13:37:32.797471662Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadVersion"}
-{"Time":"2023-03-29T13:37:32.797476114Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadClientAuth"}
-{"Time":"2023-03-29T13:37:32.797478506Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadClientAuth","Output":"=== RUN TestServer/TLSBadClientAuth\n"}
-{"Time":"2023-03-29T13:37:32.797495642Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadClientAuth","Output":"=== PAUSE TestServer/TLSBadClientAuth\n"}
-{"Time":"2023-03-29T13:37:32.79750134Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadClientAuth"}
-{"Time":"2023-03-29T13:37:32.797508036Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid"}
-{"Time":"2023-03-29T13:37:32.797510579Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid","Output":"=== RUN TestServer/TLSInvalid\n"}
-{"Time":"2023-03-29T13:37:32.797525872Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid","Output":"=== PAUSE TestServer/TLSInvalid\n"}
-{"Time":"2023-03-29T13:37:32.797528971Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid"}
-{"Time":"2023-03-29T13:37:32.797533231Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid"}
-{"Time":"2023-03-29T13:37:32.797535658Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":"=== RUN TestServer/TLSValid\n"}
-{"Time":"2023-03-29T13:37:32.797552494Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":"=== PAUSE TestServer/TLSValid\n"}
-{"Time":"2023-03-29T13:37:32.797556455Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid"}
-{"Time":"2023-03-29T13:37:32.797560606Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple"}
-{"Time":"2023-03-29T13:37:32.797563041Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":"=== RUN TestServer/TLSValidMultiple\n"}
-{"Time":"2023-03-29T13:37:32.797579028Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":"=== PAUSE TestServer/TLSValidMultiple\n"}
-{"Time":"2023-03-29T13:37:32.797582214Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple"}
-{"Time":"2023-03-29T13:37:32.797586334Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP"}
-{"Time":"2023-03-29T13:37:32.797588893Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":"=== RUN TestServer/TLSAndHTTP\n"}
-{"Time":"2023-03-29T13:37:32.797623711Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":"=== PAUSE TestServer/TLSAndHTTP\n"}
-{"Time":"2023-03-29T13:37:32.797626474Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP"}
-{"Time":"2023-03-29T13:37:32.797631971Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect"}
-{"Time":"2023-03-29T13:37:32.797634471Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect","Output":"=== RUN TestServer/TLSRedirect\n"}
-{"Time":"2023-03-29T13:37:32.797650491Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect","Output":"=== PAUSE TestServer/TLSRedirect\n"}
-{"Time":"2023-03-29T13:37:32.797654521Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect"}
-{"Time":"2023-03-29T13:37:32.797659191Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4"}
-{"Time":"2023-03-29T13:37:32.797661727Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":"=== RUN TestServer/CanListenUnspecifiedv4\n"}
-{"Time":"2023-03-29T13:37:32.797677081Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":"=== PAUSE TestServer/CanListenUnspecifiedv4\n"}
-{"Time":"2023-03-29T13:37:32.797679898Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4"}
-{"Time":"2023-03-29T13:37:32.797685398Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6"}
-{"Time":"2023-03-29T13:37:32.797688055Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":"=== RUN TestServer/CanListenUnspecifiedv6\n"}
-{"Time":"2023-03-29T13:37:32.797706704Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":"=== PAUSE TestServer/CanListenUnspecifiedv6\n"}
-{"Time":"2023-03-29T13:37:32.797710667Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6"}
-{"Time":"2023-03-29T13:37:32.797714823Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/NoAddress"}
-{"Time":"2023-03-29T13:37:32.797717235Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoAddress","Output":"=== RUN TestServer/NoAddress\n"}
-{"Time":"2023-03-29T13:37:32.797732629Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoAddress","Output":"=== PAUSE TestServer/NoAddress\n"}
-{"Time":"2023-03-29T13:37:32.797735715Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/NoAddress"}
-{"Time":"2023-03-29T13:37:32.797739744Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/NoTLSAddress"}
-{"Time":"2023-03-29T13:37:32.797742309Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoTLSAddress","Output":"=== RUN TestServer/NoTLSAddress\n"}
-{"Time":"2023-03-29T13:37:32.797754504Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoTLSAddress","Output":"=== PAUSE TestServer/NoTLSAddress\n"}
-{"Time":"2023-03-29T13:37:32.797757251Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/NoTLSAddress"}
-{"Time":"2023-03-29T13:37:32.797762513Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress"}
-{"Time":"2023-03-29T13:37:32.797764941Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress","Output":"=== RUN TestServer/DeprecatedAddress\n"}
-{"Time":"2023-03-29T13:37:32.797791112Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress","Output":"=== PAUSE TestServer/DeprecatedAddress\n"}
-{"Time":"2023-03-29T13:37:32.797794156Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress"}
-{"Time":"2023-03-29T13:37:32.797818478Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown"}
-{"Time":"2023-03-29T13:37:32.797821565Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":"=== RUN TestServer/Shutdown\n"}
-{"Time":"2023-03-29T13:37:32.799148069Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerShutdown1335635398/002 server --in-memory --http-address :0 --access-url http://example.com --provisioner-daemons 1 --cache-dir /tmp/TestServerShutdown1335635398/001\n"}
-{"Time":"2023-03-29T13:37:32.799996289Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:39611\n"}
-{"Time":"2023-03-29T13:37:32.803857037Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:32.820483862Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:32.840616097Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:32.840652908Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:32.840837548Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:32.840996757Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:32.841130055Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:32.843139909Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Output":"--- PASS: TestServer/Shutdown (0.05s)\n"}
-{"Time":"2023-03-29T13:37:32.843152696Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/Shutdown","Elapsed":0.05}
-{"Time":"2023-03-29T13:37:32.843181686Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak"}
-{"Time":"2023-03-29T13:37:32.843185961Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":"=== RUN TestServer/TracerNoLeak\n"}
-{"Time":"2023-03-29T13:37:32.84324137Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":"=== PAUSE TestServer/TracerNoLeak\n"}
-{"Time":"2023-03-29T13:37:32.843248162Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak"}
-{"Time":"2023-03-29T13:37:32.84327344Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry"}
-{"Time":"2023-03-29T13:37:32.843276473Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":"=== RUN TestServer/Telemetry\n"}
-{"Time":"2023-03-29T13:37:32.843298273Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":"=== PAUSE TestServer/Telemetry\n"}
-{"Time":"2023-03-29T13:37:32.843301329Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry"}
-{"Time":"2023-03-29T13:37:32.843328619Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus"}
-{"Time":"2023-03-29T13:37:32.843332448Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":"=== RUN TestServer/Prometheus\n"}
-{"Time":"2023-03-29T13:37:32.843360436Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":"=== PAUSE TestServer/Prometheus\n"}
-{"Time":"2023-03-29T13:37:32.843363322Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus"}
-{"Time":"2023-03-29T13:37:32.843393432Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth"}
-{"Time":"2023-03-29T13:37:32.843398556Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":"=== RUN TestServer/GitHubOAuth\n"}
-{"Time":"2023-03-29T13:37:32.843457011Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":"=== PAUSE TestServer/GitHubOAuth\n"}
-{"Time":"2023-03-29T13:37:32.843461316Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth"}
-{"Time":"2023-03-29T13:37:32.843486876Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit"}
-{"Time":"2023-03-29T13:37:32.84349017Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit","Output":"=== RUN TestServer/RateLimit\n"}
-{"Time":"2023-03-29T13:37:32.843512128Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit","Output":"=== PAUSE TestServer/RateLimit\n"}
-{"Time":"2023-03-29T13:37:32.843515107Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit"}
-{"Time":"2023-03-29T13:37:32.843530012Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging"}
-{"Time":"2023-03-29T13:37:32.843532716Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging","Output":"=== RUN TestServer/Logging\n"}
-{"Time":"2023-03-29T13:37:32.843562048Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging","Output":"=== PAUSE TestServer/Logging\n"}
-{"Time":"2023-03-29T13:37:32.843565639Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging"}
-{"Time":"2023-03-29T13:37:32.843591297Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres"}
-{"Time":"2023-03-29T13:37:32.843594335Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":"=== CONT TestServer/BuiltinPostgres\n"}
-{"Time":"2023-03-29T13:37:32.845419328Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerBuiltinPostgres1969653008/002 server --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerBuiltinPostgres1969653008/001\n"}
-{"Time":"2023-03-29T13:37:32.846522439Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Using built-in PostgreSQL (/tmp/TestServerBuiltinPostgres1969653008/002/postgres)\n"}
-{"Time":"2023-03-29T13:37:32.847782539Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging"}
-{"Time":"2023-03-29T13:37:32.847787939Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging","Output":"=== CONT TestServer/Logging\n"}
-{"Time":"2023-03-29T13:37:32.847793104Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile"}
-{"Time":"2023-03-29T13:37:32.847797472Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":"=== RUN TestServer/Logging/CreatesFile\n"}
-{"Time":"2023-03-29T13:37:32.847809954Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":"=== PAUSE TestServer/Logging/CreatesFile\n"}
-{"Time":"2023-03-29T13:37:32.847814894Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile"}
-{"Time":"2023-03-29T13:37:32.847821007Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human"}
-{"Time":"2023-03-29T13:37:32.847823557Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":"=== RUN TestServer/Logging/Human\n"}
-{"Time":"2023-03-29T13:37:32.847827971Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":"=== PAUSE TestServer/Logging/Human\n"}
-{"Time":"2023-03-29T13:37:32.847830572Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human"}
-{"Time":"2023-03-29T13:37:32.847846314Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON"}
-{"Time":"2023-03-29T13:37:32.847849769Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":"=== RUN TestServer/Logging/JSON\n"}
-{"Time":"2023-03-29T13:37:32.847855516Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":"=== PAUSE TestServer/Logging/JSON\n"}
-{"Time":"2023-03-29T13:37:32.847858116Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON"}
-{"Time":"2023-03-29T13:37:32.847862399Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver"}
-{"Time":"2023-03-29T13:37:32.847864919Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":"=== RUN TestServer/Logging/Stackdriver\n"}
-{"Time":"2023-03-29T13:37:32.847879575Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":"=== PAUSE TestServer/Logging/Stackdriver\n"}
-{"Time":"2023-03-29T13:37:32.847884818Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver"}
-{"Time":"2023-03-29T13:37:32.847891759Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple"}
-{"Time":"2023-03-29T13:37:32.847894796Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":"=== RUN TestServer/Logging/Multiple\n"}
-{"Time":"2023-03-29T13:37:32.847919729Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":"=== PAUSE TestServer/Logging/Multiple\n"}
-{"Time":"2023-03-29T13:37:32.847922769Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple"}
-{"Time":"2023-03-29T13:37:32.847927067Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile"}
-{"Time":"2023-03-29T13:37:32.847929708Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":"=== CONT TestServer/Logging/CreatesFile\n"}
-{"Time":"2023-03-29T13:37:32.848797106Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingCreatesFile2700332728/002 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-human /tmp/TestServerLoggingCreatesFile2700332728/001/coder-logging-test-2490710733\n"}
-{"Time":"2023-03-29T13:37:32.849489309Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:46231\n"}
-{"Time":"2023-03-29T13:37:32.849644524Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit"}
-{"Time":"2023-03-29T13:37:32.849648931Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit","Output":"=== CONT TestServer/RateLimit\n"}
-{"Time":"2023-03-29T13:37:32.849654201Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default"}
-{"Time":"2023-03-29T13:37:32.849658963Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":"=== RUN TestServer/RateLimit/Default\n"}
-{"Time":"2023-03-29T13:37:32.849673813Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":"=== PAUSE TestServer/RateLimit/Default\n"}
-{"Time":"2023-03-29T13:37:32.849676949Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default"}
-{"Time":"2023-03-29T13:37:32.849690853Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed"}
-{"Time":"2023-03-29T13:37:32.849694907Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":"=== RUN TestServer/RateLimit/Changed\n"}
-{"Time":"2023-03-29T13:37:32.849700056Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":"=== PAUSE TestServer/RateLimit/Changed\n"}
-{"Time":"2023-03-29T13:37:32.849702697Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed"}
-{"Time":"2023-03-29T13:37:32.849706931Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled"}
-{"Time":"2023-03-29T13:37:32.849709323Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":"=== RUN TestServer/RateLimit/Disabled\n"}
-{"Time":"2023-03-29T13:37:32.849713561Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":"=== PAUSE TestServer/RateLimit/Disabled\n"}
-{"Time":"2023-03-29T13:37:32.849716093Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled"}
-{"Time":"2023-03-29T13:37:32.849720102Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default"}
-{"Time":"2023-03-29T13:37:32.849722411Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":"=== CONT TestServer/RateLimit/Default\n"}
-{"Time":"2023-03-29T13:37:32.850485755Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerRateLimitDefault1881118474/001 server --in-memory --http-address :0 --access-url http://example.com\n"}
-{"Time":"2023-03-29T13:37:32.851011889Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:38127\n"}
-{"Time":"2023-03-29T13:37:32.851175576Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth"}
-{"Time":"2023-03-29T13:37:32.851180015Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":"=== CONT TestServer/GitHubOAuth\n"}
-{"Time":"2023-03-29T13:37:32.851983967Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerGitHubOAuth724593823/001 server --in-memory --http-address :0 --access-url http://example.com --oauth2-github-allow-everyone --oauth2-github-client-id fake --oauth2-github-client-secret fake --oauth2-github-enterprise-base-url https://fake-url.com\n"}
-{"Time":"2023-03-29T13:37:32.852521551Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:33313\n"}
-{"Time":"2023-03-29T13:37:32.852663734Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus"}
-{"Time":"2023-03-29T13:37:32.852668353Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":"=== CONT TestServer/Prometheus\n"}
-{"Time":"2023-03-29T13:37:32.853556318Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerPrometheus2050744846/002 server --in-memory --http-address :0 --access-url http://example.com --provisioner-daemons 1 --prometheus-enable --prometheus-address :37569 --cache-dir /tmp/TestServerPrometheus2050744846/001\n"}
-{"Time":"2023-03-29T13:37:32.854050354Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:38381\n"}
-{"Time":"2023-03-29T13:37:32.854220704Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry"}
-{"Time":"2023-03-29T13:37:32.854225473Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":"=== CONT TestServer/Telemetry\n"}
-{"Time":"2023-03-29T13:37:32.855113604Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTelemetry4206954660/002 server --in-memory --http-address :0 --access-url http://example.com --telemetry --telemetry-url http://127.0.0.1:46805 --cache-dir /tmp/TestServerTelemetry4206954660/001\n"}
-{"Time":"2023-03-29T13:37:32.855607409Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:46851\n"}
-{"Time":"2023-03-29T13:37:32.855759635Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak"}
-{"Time":"2023-03-29T13:37:32.85576445Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":"=== CONT TestServer/TracerNoLeak\n"}
-{"Time":"2023-03-29T13:37:32.856575033Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTracerNoLeak127485117/002 server --in-memory --http-address :0 --access-url http://example.com --trace=true --cache-dir /tmp/TestServerTracerNoLeak127485117/001\n"}
-{"Time":"2023-03-29T13:37:32.859583574Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress"}
-{"Time":"2023-03-29T13:37:32.859590623Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress","Output":"=== CONT TestServer/DeprecatedAddress\n"}
-{"Time":"2023-03-29T13:37:32.859595635Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP"}
-{"Time":"2023-03-29T13:37:32.859598523Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":"=== RUN TestServer/DeprecatedAddress/HTTP\n"}
-{"Time":"2023-03-29T13:37:32.860822192Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/NoTLSAddress"}
-{"Time":"2023-03-29T13:37:32.860826569Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoTLSAddress","Output":"=== CONT TestServer/NoTLSAddress\n"}
-{"Time":"2023-03-29T13:37:32.861575027Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoTLSAddress","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerNoTLSAddress2369561878/001 server --in-memory --tls-enable=true --tls-address \n"}
-{"Time":"2023-03-29T13:37:32.861999568Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoTLSAddress","Output":"--- PASS: TestServer/NoTLSAddress (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.863329199Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/NoTLSAddress","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.863344025Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/NoAddress"}
-{"Time":"2023-03-29T13:37:32.863348554Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoAddress","Output":"=== CONT TestServer/NoAddress\n"}
-{"Time":"2023-03-29T13:37:32.864056037Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoAddress","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerNoAddress3728350894/001 server --in-memory --http-address :80 --tls-enable=false --tls-address \n"}
-{"Time":"2023-03-29T13:37:32.864465815Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoAddress","Output":"--- PASS: TestServer/NoAddress (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.865769258Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/NoAddress","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.865776672Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6"}
-{"Time":"2023-03-29T13:37:32.865779982Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":"=== CONT TestServer/CanListenUnspecifiedv6\n"}
-{"Time":"2023-03-29T13:37:32.866452947Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerCanListenUnspecifiedv63515544106/001 server --in-memory --http-address [::]:0 --access-url http://example.com\n"}
-{"Time":"2023-03-29T13:37:32.86788654Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4"}
-{"Time":"2023-03-29T13:37:32.867892077Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":"=== CONT TestServer/CanListenUnspecifiedv4\n"}
-{"Time":"2023-03-29T13:37:32.868594181Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerCanListenUnspecifiedv43698525072/001 server --in-memory --http-address 0.0.0.0:0 --access-url http://example.com\n"}
-{"Time":"2023-03-29T13:37:32.87981375Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect"}
-{"Time":"2023-03-29T13:37:32.879832688Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect","Output":"=== CONT TestServer/TLSRedirect\n"}
-{"Time":"2023-03-29T13:37:32.879840427Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK"}
-{"Time":"2023-03-29T13:37:32.879843579Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":"=== RUN TestServer/TLSRedirect/OK\n"}
-{"Time":"2023-03-29T13:37:32.879847493Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":"=== PAUSE TestServer/TLSRedirect/OK\n"}
-{"Time":"2023-03-29T13:37:32.879850007Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK"}
-{"Time":"2023-03-29T13:37:32.879858919Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect"}
-{"Time":"2023-03-29T13:37:32.87986153Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":"=== RUN TestServer/TLSRedirect/NoRedirect\n"}
-{"Time":"2023-03-29T13:37:32.879868217Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":"=== PAUSE TestServer/TLSRedirect/NoRedirect\n"}
-{"Time":"2023-03-29T13:37:32.879871031Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect"}
-{"Time":"2023-03-29T13:37:32.879882319Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard"}
-{"Time":"2023-03-29T13:37:32.879885302Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":"=== RUN TestServer/TLSRedirect/NoRedirectWithWildcard\n"}
-{"Time":"2023-03-29T13:37:32.879901278Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":"=== PAUSE TestServer/TLSRedirect/NoRedirectWithWildcard\n"}
-{"Time":"2023-03-29T13:37:32.879904339Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard"}
-{"Time":"2023-03-29T13:37:32.879917091Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener"}
-{"Time":"2023-03-29T13:37:32.879920112Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":"=== RUN TestServer/TLSRedirect/NoTLSListener\n"}
-{"Time":"2023-03-29T13:37:32.881385273Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP"}
-{"Time":"2023-03-29T13:37:32.881392419Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":"=== CONT TestServer/TLSAndHTTP\n"}
-{"Time":"2023-03-29T13:37:32.883317205Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSAndHTTP1565439063/003 server --in-memory --http-address :0 --access-url https://example.com --tls-enable --tls-redirect-http-to-https=false --tls-address :0 --tls-cert-file /tmp/TestServerTLSAndHTTP1565439063/001/1540336369 --tls-key-file /tmp/TestServerTLSAndHTTP1565439063/001/228555534 --cache-dir /tmp/TestServerTLSAndHTTP1565439063/002\n"}
-{"Time":"2023-03-29T13:37:32.886709195Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple"}
-{"Time":"2023-03-29T13:37:32.886720913Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":"=== CONT TestServer/TLSValidMultiple\n"}
-{"Time":"2023-03-29T13:37:32.888650035Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSValidMultiple3077156975/004 server --in-memory --http-address --access-url https://example.com --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSValidMultiple3077156975/001/3448842699 --tls-key-file /tmp/TestServerTLSValidMultiple3077156975/001/3382329005 --tls-cert-file /tmp/TestServerTLSValidMultiple3077156975/002/610618616 --tls-key-file /tmp/TestServerTLSValidMultiple3077156975/002/3728409390 --cache-dir /tmp/TestServerTLSValidMultiple3077156975/003\n"}
-{"Time":"2023-03-29T13:37:32.889032741Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid"}
-{"Time":"2023-03-29T13:37:32.889040091Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":"=== CONT TestServer/TLSValid\n"}
-{"Time":"2023-03-29T13:37:32.890094104Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSValid1911968885/003 server --in-memory --http-address --access-url https://example.com --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSValid1911968885/001/91528180 --tls-key-file /tmp/TestServerTLSValid1911968885/001/1223943395 --cache-dir /tmp/TestServerTLSValid1911968885/002\n"}
-{"Time":"2023-03-29T13:37:32.890711151Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Started TLS/HTTPS listener at https://[::]:38747\n"}
-{"Time":"2023-03-29T13:37:32.893432414Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid"}
-{"Time":"2023-03-29T13:37:32.893439577Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid","Output":"=== CONT TestServer/TLSInvalid\n"}
-{"Time":"2023-03-29T13:37:32.895534213Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert"}
-{"Time":"2023-03-29T13:37:32.895540534Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert","Output":"=== RUN TestServer/TLSInvalid/NoCert\n"}
-{"Time":"2023-03-29T13:37:32.895545749Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert","Output":"=== PAUSE TestServer/TLSInvalid/NoCert\n"}
-{"Time":"2023-03-29T13:37:32.895548511Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert"}
-{"Time":"2023-03-29T13:37:32.89555469Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey"}
-{"Time":"2023-03-29T13:37:32.895559989Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey","Output":"=== RUN TestServer/TLSInvalid/NoKey\n"}
-{"Time":"2023-03-29T13:37:32.895564748Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey","Output":"=== PAUSE TestServer/TLSInvalid/NoKey\n"}
-{"Time":"2023-03-29T13:37:32.895567562Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey"}
-{"Time":"2023-03-29T13:37:32.895571832Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount"}
-{"Time":"2023-03-29T13:37:32.895574371Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":"=== RUN TestServer/TLSInvalid/MismatchedCount\n"}
-{"Time":"2023-03-29T13:37:32.895579082Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":"=== PAUSE TestServer/TLSInvalid/MismatchedCount\n"}
-{"Time":"2023-03-29T13:37:32.895581688Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount"}
-{"Time":"2023-03-29T13:37:32.895586121Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey"}
-{"Time":"2023-03-29T13:37:32.895588766Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":"=== RUN TestServer/TLSInvalid/MismatchedCertAndKey\n"}
-{"Time":"2023-03-29T13:37:32.895593302Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":"=== PAUSE TestServer/TLSInvalid/MismatchedCertAndKey\n"}
-{"Time":"2023-03-29T13:37:32.895595797Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey"}
-{"Time":"2023-03-29T13:37:32.895598709Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert"}
-{"Time":"2023-03-29T13:37:32.895601189Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert","Output":"=== CONT TestServer/TLSInvalid/NoCert\n"}
-{"Time":"2023-03-29T13:37:32.896383938Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSInvalidNoCert155343146/002 server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidNoCert155343146/001 --tls-enable --tls-key-file /tmp/TestServerTLSInvalid1610620518/001/1816833089\n"}
-{"Time":"2023-03-29T13:37:32.896712838Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:41521\n"}
-{"Time":"2023-03-29T13:37:32.896777738Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert","Output":" server_test.go:344: args: [server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidNoCert155343146/001 --tls-enable --tls-key-file /tmp/TestServerTLSInvalid1610620518/001/1816833089]\n"}
-{"Time":"2023-03-29T13:37:32.896926023Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert","Output":"--- PASS: TestServer/TLSInvalid/NoCert (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.896932574Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoCert","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.896936656Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadClientAuth"}
-{"Time":"2023-03-29T13:37:32.896939204Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadClientAuth","Output":"=== CONT TestServer/TLSBadClientAuth\n"}
-{"Time":"2023-03-29T13:37:32.897600801Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadClientAuth","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSBadClientAuth2976087810/002 server --in-memory --http-address --access-url http://example.com --tls-enable --tls-address :0 --tls-client-auth something --cache-dir /tmp/TestServerTLSBadClientAuth2976087810/001\n"}
-{"Time":"2023-03-29T13:37:32.897994004Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadClientAuth","Output":"--- PASS: TestServer/TLSBadClientAuth (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.898002196Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadClientAuth","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.89800515Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadVersion"}
-{"Time":"2023-03-29T13:37:32.898008134Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadVersion","Output":"=== CONT TestServer/TLSBadVersion\n"}
-{"Time":"2023-03-29T13:37:32.898635976Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadVersion","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSBadVersion2460276843/002 server --in-memory --http-address --access-url http://example.com --tls-enable --tls-address :0 --tls-min-version tls9 --cache-dir /tmp/TestServerTLSBadVersion2460276843/001\n"}
-{"Time":"2023-03-29T13:37:32.899017393Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadVersion","Output":"--- PASS: TestServer/TLSBadVersion (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.899025249Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSBadVersion","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.899028489Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/NoSchemeAccessURL"}
-{"Time":"2023-03-29T13:37:32.899031117Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoSchemeAccessURL","Output":"=== CONT TestServer/NoSchemeAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.899735022Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoSchemeAccessURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerNoSchemeAccessURL3227162288/002 server --in-memory --http-address :0 --access-url google.com --cache-dir /tmp/TestServerNoSchemeAccessURL3227162288/001\n"}
-{"Time":"2023-03-29T13:37:32.900078052Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoSchemeAccessURL","Output":"--- PASS: TestServer/NoSchemeAccessURL (0.00s)\n"}
-{"Time":"2023-03-29T13:37:32.900133291Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/NoSchemeAccessURL","Elapsed":0}
-{"Time":"2023-03-29T13:37:32.900136928Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL"}
-{"Time":"2023-03-29T13:37:32.900139387Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":"=== CONT TestServer/NoWarningWithRemoteAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.900802695Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerNoWarningWithRemoteAccessURL2261451002/002 server --in-memory --http-address :0 --access-url https://google.com --cache-dir /tmp/TestServerNoWarningWithRemoteAccessURL2261451002/001\n"}
-{"Time":"2023-03-29T13:37:32.901428258Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL"}
-{"Time":"2023-03-29T13:37:32.901434866Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":"=== CONT TestServer/RemoteAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.902088159Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerRemoteAccessURL917985260/002 server --in-memory --http-address :0 --access-url https://foobarbaz.mydomain --cache-dir /tmp/TestServerRemoteAccessURL917985260/001\n"}
-{"Time":"2023-03-29T13:37:32.904875871Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL"}
-{"Time":"2023-03-29T13:37:32.90488445Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":"=== CONT TestServer/LocalAccessURL\n"}
-{"Time":"2023-03-29T13:37:32.905523425Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLocalAccessURL3554694382/002 server --in-memory --http-address :0 --access-url http://localhost:3000/ --cache-dir /tmp/TestServerLocalAccessURL3554694382/001\n"}
-{"Time":"2023-03-29T13:37:32.909203431Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw"}
-{"Time":"2023-03-29T13:37:32.909214782Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":"=== CONT TestServer/BuiltinPostgresURLRaw\n"}
-{"Time":"2023-03-29T13:37:32.90987112Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerBuiltinPostgresURLRaw2301128244/001 server postgres-builtin-url --raw-url\n"}
-{"Time":"2023-03-29T13:37:32.910387857Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL"}
-{"Time":"2023-03-29T13:37:32.910393909Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":"=== CONT TestServer/BuiltinPostgresURL\n"}
-{"Time":"2023-03-29T13:37:32.911031385Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerBuiltinPostgresURL2022412164/001 server postgres-builtin-url\n"}
-{"Time":"2023-03-29T13:37:32.911696996Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple"}
-{"Time":"2023-03-29T13:37:32.911705024Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":"=== CONT TestServer/Logging/Multiple\n"}
-{"Time":"2023-03-29T13:37:32.912435053Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingMultiple1018156314/004 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-human /tmp/TestServerLoggingMultiple1018156314/001/coder-logging-test-2240709984 --log-json /tmp/TestServerLoggingMultiple1018156314/002/coder-logging-test-2164710923 --log-stackdriver /tmp/TestServerLoggingMultiple1018156314/003/coder-logging-test-3557853095\n"}
-{"Time":"2023-03-29T13:37:32.912514796Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver"}
-{"Time":"2023-03-29T13:37:32.912520309Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":"=== CONT TestServer/Logging/Stackdriver\n"}
-{"Time":"2023-03-29T13:37:32.913204422Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingStackdriver1654522233/002 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-stackdriver /tmp/TestServerLoggingStackdriver1654522233/001/coder-logging-test-3531177805\n"}
-{"Time":"2023-03-29T13:37:32.913286393Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON"}
-{"Time":"2023-03-29T13:37:32.913292549Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":"=== CONT TestServer/Logging/JSON\n"}
-{"Time":"2023-03-29T13:37:32.913937207Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingJSON1509288451/002 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-json /tmp/TestServerLoggingJSON1509288451/001/coder-logging-test-115410787\n"}
-{"Time":"2023-03-29T13:37:32.913982029Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human"}
-{"Time":"2023-03-29T13:37:32.913985447Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":"=== CONT TestServer/Logging/Human\n"}
-{"Time":"2023-03-29T13:37:32.914664441Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingHuman1910502850/002 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-human /tmp/TestServerLoggingHuman1910502850/001/coder-logging-test-2040592756\n"}
-{"Time":"2023-03-29T13:37:32.915201663Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:42891\n"}
-{"Time":"2023-03-29T13:37:32.915334373Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled"}
-{"Time":"2023-03-29T13:37:32.91533902Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":"=== CONT TestServer/RateLimit/Disabled\n"}
-{"Time":"2023-03-29T13:37:32.915962046Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerRateLimitDisabled1912095220/001 server --in-memory --http-address :0 --access-url http://example.com --api-rate-limit -1\n"}
-{"Time":"2023-03-29T13:37:32.916390927Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:35065\n"}
-{"Time":"2023-03-29T13:37:32.917528951Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stderr: 2023-03-29 13:37:32.917 [WARN]\t\u003cgithub.com/coder/coder/cli/server.go:310\u003e\t(*RootCmd).Server.func1\tstart telemetry exporter ...\n"}
-{"Time":"2023-03-29T13:37:32.917535354Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" \"error\": default exporter:\n"}
-{"Time":"2023-03-29T13:37:32.917538568Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" github.com/coder/coder/coderd/tracing.TracerProvider\n"}
-{"Time":"2023-03-29T13:37:32.917543903Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" /home/mafredri/src/coder/coder/coderd/tracing/exporter.go:51\n"}
-{"Time":"2023-03-29T13:37:32.917547458Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" - create otlp exporter:\n"}
-{"Time":"2023-03-29T13:37:32.917552386Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" github.com/coder/coder/coderd/tracing.DefaultExporter\n"}
-{"Time":"2023-03-29T13:37:32.917557328Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" /home/mafredri/src/coder/coder/coderd/tracing/exporter.go:109\n"}
-{"Time":"2023-03-29T13:37:32.917562236Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" - context canceled\n"}
-{"Time":"2023-03-29T13:37:32.917576569Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:42093\n"}
-{"Time":"2023-03-29T13:37:32.917619611Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stderr: \u001b[1;mWARN: \u001b[0mThe access URL \u001b[;mhttp://example.com\u001b[0m could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n"}
-{"Time":"2023-03-29T13:37:32.917642615Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:32.920130044Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:32.920155751Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:32.920178993Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:32.920200709Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:32.920249847Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":"=== PAUSE TestServer/DeprecatedAddress/HTTP\n"}
-{"Time":"2023-03-29T13:37:32.92025399Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP"}
-{"Time":"2023-03-29T13:37:32.920269801Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS"}
-{"Time":"2023-03-29T13:37:32.920274896Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":"=== RUN TestServer/DeprecatedAddress/TLS\n"}
-{"Time":"2023-03-29T13:37:32.920279669Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":"=== PAUSE TestServer/DeprecatedAddress/TLS\n"}
-{"Time":"2023-03-29T13:37:32.920282197Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS"}
-{"Time":"2023-03-29T13:37:32.92570047Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":"=== PAUSE TestServer/TLSRedirect/NoTLSListener\n"}
-{"Time":"2023-03-29T13:37:32.92570807Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener"}
-{"Time":"2023-03-29T13:37:32.925713393Z","Action":"run","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener"}
-{"Time":"2023-03-29T13:37:32.92571595Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":"=== RUN TestServer/TLSRedirect/NoHTTPListener\n"}
-{"Time":"2023-03-29T13:37:32.925720434Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":"=== PAUSE TestServer/TLSRedirect/NoHTTPListener\n"}
-{"Time":"2023-03-29T13:37:32.925722986Z","Action":"pause","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener"}
-{"Time":"2023-03-29T13:37:32.92724642Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.927: cmd: \"Started HTTP listener at http://[::]:39671\"\n"}
-{"Time":"2023-03-29T13:37:32.927965116Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.927: cmd: \"Started HTTP listener at http://[::]:42445\"\n"}
-{"Time":"2023-03-29T13:37:32.928006932Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.927: cmd: \"WARN: The access URL http://localhost:3000/ isn't externally reachable, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
-{"Time":"2023-03-29T13:37:32.928023919Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:32.92802928Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:32.928042009Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \"View the Web UI: http://localhost:3000/\\r\"\n"}
-{"Time":"2023-03-29T13:37:32.928061275Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:32.928077795Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:32.973554406Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" server_test.go:173: 2023-03-29 13:37:32.973: cmd: matched newline = \"postgres://coder@localhost:43211/coder?sslmode=disable\u0026password=Xha7Pt7Mcuv0IlkT\"\n"}
-{"Time":"2023-03-29T13:37:32.973585539Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:83: 2023-03-29 13:37:32.973: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:32.973602051Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:74: 2023-03-29 13:37:32.973: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:32.973653232Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:110: 2023-03-29 13:37:32.973: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:32.973673734Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:111: 2023-03-29 13:37:32.973: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:32.973681952Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:113: 2023-03-29 13:37:32.973: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:32.973745487Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:76: 2023-03-29 13:37:32.973: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:32.973755467Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:74: 2023-03-29 13:37:32.973: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:32.973767632Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:76: 2023-03-29 13:37:32.973: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:32.973787295Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:74: 2023-03-29 13:37:32.973: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:32.973794335Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:76: 2023-03-29 13:37:32.973: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:32.973897477Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" server_test.go:161: 2023-03-29 13:37:32.973: cmd: matched \"psql\" = \" psql\"\n"}
-{"Time":"2023-03-29T13:37:32.973911816Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:83: 2023-03-29 13:37:32.973: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:32.97395369Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:74: 2023-03-29 13:37:32.973: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:32.973977688Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:110: 2023-03-29 13:37:32.973: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:32.973985059Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:111: 2023-03-29 13:37:32.973: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:32.974011895Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:113: 2023-03-29 13:37:32.973: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:32.974059281Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:76: 2023-03-29 13:37:32.974: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:32.974066822Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:74: 2023-03-29 13:37:32.974: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:32.974073474Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:76: 2023-03-29 13:37:32.974: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:32.974079891Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:74: 2023-03-29 13:37:32.974: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:32.974111815Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:76: 2023-03-29 13:37:32.974: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:32.976190183Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:32.976 [DEBUG]\t\u003cgithub.com/coder/coder/cli/server.go:260\u003e\t(*RootCmd).Server.func1\tstarted debug logging\n"}
-{"Time":"2023-03-29T13:37:32.976499482Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:45725\n"}
-{"Time":"2023-03-29T13:37:32.977583615Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:32.977594677Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed"}
-{"Time":"2023-03-29T13:37:32.977598319Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":"=== CONT TestServer/RateLimit/Changed\n"}
-{"Time":"2023-03-29T13:37:32.979536902Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerRateLimitChanged2140102987/001 server --in-memory --http-address :0 --access-url http://example.com --api-rate-limit 100\n"}
-{"Time":"2023-03-29T13:37:32.981248381Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:45133\n"}
-{"Time":"2023-03-29T13:37:32.982290561Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:32.982: cmd: \"Started HTTP listener at http://0.0.0.0:37181\"\n"}
-{"Time":"2023-03-29T13:37:32.982961159Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" server_test.go:718: 2023-03-29 13:37:32.982: cmd: matched \"Started HTTP listener\" = \"Started HTTP listener\"\n"}
-{"Time":"2023-03-29T13:37:32.983105329Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" server_test.go:719: 2023-03-29 13:37:32.983: cmd: matched \"http://0.0.0.0:\" = \" at http://0.0.0.0:\"\n"}
-{"Time":"2023-03-29T13:37:32.983250515Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:32.983: cmd: \"Started HTTP listener at http://[::]:33561\"\n"}
-{"Time":"2023-03-29T13:37:32.984671213Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" server_test.go:738: 2023-03-29 13:37:32.983: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
-{"Time":"2023-03-29T13:37:32.984712571Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" server_test.go:739: 2023-03-29 13:37:32.983: cmd: matched \"http://[::]:\" = \" http://[::]:\"\n"}
-{"Time":"2023-03-29T13:37:32.984733017Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:32.996108891Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:32.996314264Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:33.00125555Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:33.001437292Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:33.002347544Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:33.00471704Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:33.004816922Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stderr: \u001b[1;mWARN: \u001b[0mThe access URL \u001b[;mhttp://example.com\u001b[0m could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n"}
-{"Time":"2023-03-29T13:37:33.004828798Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:33.008304441Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:33.008334151Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:33.008344183Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:33.008351448Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:33.008401366Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 3...\n"}
-{"Time":"2023-03-29T13:37:33.00852168Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 3\n"}
-{"Time":"2023-03-29T13:37:33.008558749Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP"}
-{"Time":"2023-03-29T13:37:33.008572474Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":"=== CONT TestServer/DeprecatedAddress/HTTP\n"}
-{"Time":"2023-03-29T13:37:33.009297224Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerDeprecatedAddressHTTP1174595269/002 server --in-memory --address :0 --access-url http://example.com --cache-dir /tmp/TestServerDeprecatedAddressHTTP1174595269/001\n"}
-{"Time":"2023-03-29T13:37:33.011200842Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" server_test.go:196: 2023-03-29 13:37:33.011: cmd: matched \"this may cause unexpected problems when creating workspaces\" = \"Started HTTP listener at http://[::]:42445\\r\\nWARN: The access URL http://localhost:3000/ isn't externally reachable, this may cause unexpected problems when creating workspaces\"\n"}
-{"Time":"2023-03-29T13:37:33.011256139Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" server_test.go:197: 2023-03-29 13:37:33.011: cmd: matched \"View the Web UI: http://localhost:3000/\" = \". Generate a unique *.try.coder.app URL by not specifying an access URL.\\r\\n \\r\\r\\n \\r\\nView the Web UI: http://localhost:3000/\"\n"}
-{"Time":"2023-03-29T13:37:33.011375838Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: \u001b[1;mWARN: \u001b[0mThe access URL \u001b[;mhttp://example.com\u001b[0m could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n"}
-{"Time":"2023-03-29T13:37:33.01139606Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:33.013674142Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:33.013696745Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:33.013708678Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:33.013717271Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:33.013756177Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 3...\n"}
-{"Time":"2023-03-29T13:37:33.013807024Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.013 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\t(*Server).closeWithError\tclosing server with error\t{\"error\": null}\n"}
-{"Time":"2023-03-29T13:37:33.013861318Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 3\n"}
-{"Time":"2023-03-29T13:37:33.014354405Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:121: 2023-03-29 13:37:33.014: cmd: \"postgres://coder@localhost:43211/coder?sslmode=disable\u0026password=Xha7Pt7Mcuv0IlkT\"\n"}
-{"Time":"2023-03-29T13:37:33.014367303Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:102: 2023-03-29 13:37:33.014: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:33.01451535Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":"--- PASS: TestServer/BuiltinPostgresURLRaw (0.11s)\n"}
-{"Time":"2023-03-29T13:37:33.014525519Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURLRaw","Elapsed":0.11}
-{"Time":"2023-03-29T13:37:33.014534922Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK"}
-{"Time":"2023-03-29T13:37:33.014543883Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":"=== CONT TestServer/TLSRedirect/OK\n"}
-{"Time":"2023-03-29T13:37:33.01571179Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectOK4195649967/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectOK4195649967/002 --http-address :0 --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSRedirectOK4195649967/001/1898764516 --tls-key-file /tmp/TestServerTLSRedirectOK4195649967/001/3656697468 --wildcard-access-url *.example.com --access-url https://example.com --redirect-to-access-url\n"}
-{"Time":"2023-03-29T13:37:33.015887089Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.015: cmd: \" psql \\\"postgres://coder@localhost:43265/coder?sslmode=disable\u0026password=qZX0YVm9trLHmHzY\\\" \"\n"}
-{"Time":"2023-03-29T13:37:33.015903823Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:102: 2023-03-29 13:37:33.015: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:33.016045291Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Output":"--- PASS: TestServer/BuiltinPostgresURL (0.11s)\n"}
-{"Time":"2023-03-29T13:37:33.01632566Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgresURL","Elapsed":0.11}
-{"Time":"2023-03-29T13:37:33.016345189Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.022419375Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:33.099478164Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:33.138846249Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.137: cmd: \"Started TLS/HTTPS listener at https://[::]:46747\"\n"}
-{"Time":"2023-03-29T13:37:33.138887737Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 1...\n"}
-{"Time":"2023-03-29T13:37:33.138894754Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 1\n"}
-{"Time":"2023-03-29T13:37:33.138900199Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 2...\n"}
-{"Time":"2023-03-29T13:37:33.138905607Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 2\n"}
-{"Time":"2023-03-29T13:37:33.138911333Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:33.141820899Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.140 [DEBUG]\t(coderd.metrics_cache)\t\u003cgithub.com/coder/coder/coderd/metricscache/metricscache.go:272\u003e\t(*Cache).run\tdeployment stats metrics refreshed\t{\"took\": \"23.786µs\", \"interval\": \"30s\"}\n"}
-{"Time":"2023-03-29T13:37:33.141862791Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.141 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\t(*Server).connect\tconnected\n"}
-{"Time":"2023-03-29T13:37:33.141871378Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.141 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\t(*Server).connect\tconnected\n"}
-{"Time":"2023-03-29T13:37:33.14187713Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 1...\n"}
-{"Time":"2023-03-29T13:37:33.141883292Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.141 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\t(*Server).closeWithError\tclosing server with error\t{\"error\": null}\n"}
-{"Time":"2023-03-29T13:37:33.141893432Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 1\n"}
-{"Time":"2023-03-29T13:37:33.141898548Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 2...\n"}
-{"Time":"2023-03-29T13:37:33.141959656Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.141 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\t(*Server).closeWithError\tclosing server with error\t{\"error\": null}\n"}
-{"Time":"2023-03-29T13:37:33.142091685Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 2\n"}
-{"Time":"2023-03-29T13:37:33.142184362Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:33.142408919Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stderr: \u001b[1;mWARN: \u001b[0mThe access URL \u001b[;mhttp://example.com\u001b[0m could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n"}
-{"Time":"2023-03-29T13:37:33.142501883Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:33.147218812Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:33.147251166Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:33.14725864Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:33.147298554Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:33.147355456Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 3...\n"}
-{"Time":"2023-03-29T13:37:33.14745485Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 3\n"}
-{"Time":"2023-03-29T13:37:33.148482198Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey"}
-{"Time":"2023-03-29T13:37:33.148502985Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":"=== CONT TestServer/TLSInvalid/MismatchedCertAndKey\n"}
-{"Time":"2023-03-29T13:37:33.149452866Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSInvalidMismatchedCertAndKey978167281/002 server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidMismatchedCertAndKey978167281/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081 --tls-key-file /tmp/TestServerTLSInvalid1610620518/002/2775674587\n"}
-{"Time":"2023-03-29T13:37:33.149984351Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:38873\n"}
-{"Time":"2023-03-29T13:37:33.150363114Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":" server_test.go:344: args: [server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidMismatchedCertAndKey978167281/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081 --tls-key-file /tmp/TestServerTLSInvalid1610620518/002/2775674587]\n"}
-{"Time":"2023-03-29T13:37:33.150581402Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":"--- PASS: TestServer/TLSInvalid/MismatchedCertAndKey (0.00s)\n"}
-{"Time":"2023-03-29T13:37:33.15059771Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Elapsed":0}
-{"Time":"2023-03-29T13:37:33.150605526Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount"}
-{"Time":"2023-03-29T13:37:33.150609996Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":"=== CONT TestServer/TLSInvalid/MismatchedCount\n"}
-{"Time":"2023-03-29T13:37:33.151854633Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSInvalidMismatchedCount803880522/002 server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidMismatchedCount803880522/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081 --tls-key-file /tmp/TestServerTLSInvalid1610620518/001/1816833089 --tls-cert-file /tmp/TestServerTLSInvalid1610620518/002/3269350839\n"}
-{"Time":"2023-03-29T13:37:33.152410162Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:37839\n"}
-{"Time":"2023-03-29T13:37:33.15252017Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":" server_test.go:344: args: [server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidMismatchedCount803880522/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081 --tls-key-file /tmp/TestServerTLSInvalid1610620518/001/1816833089 --tls-cert-file /tmp/TestServerTLSInvalid1610620518/002/3269350839]\n"}
-{"Time":"2023-03-29T13:37:33.152732054Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":"--- PASS: TestServer/TLSInvalid/MismatchedCount (0.00s)\n"}
-{"Time":"2023-03-29T13:37:33.152746399Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Elapsed":0}
-{"Time":"2023-03-29T13:37:33.152754347Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey"}
-{"Time":"2023-03-29T13:37:33.152758935Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey","Output":"=== CONT TestServer/TLSInvalid/NoKey\n"}
-{"Time":"2023-03-29T13:37:33.153794977Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSInvalidNoKey281486761/002 server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidNoKey281486761/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081\n"}
-{"Time":"2023-03-29T13:37:33.154207106Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:43607\n"}
-{"Time":"2023-03-29T13:37:33.15427222Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey","Output":" server_test.go:344: args: [server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidNoKey281486761/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081]\n"}
-{"Time":"2023-03-29T13:37:33.154468443Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey","Output":"--- PASS: TestServer/TLSInvalid/NoKey (0.00s)\n"}
-{"Time":"2023-03-29T13:37:33.154623549Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid/NoKey","Elapsed":0}
-{"Time":"2023-03-29T13:37:33.154633176Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid","Output":"--- PASS: TestServer/TLSInvalid (0.00s)\n"}
-{"Time":"2023-03-29T13:37:33.154761857Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSInvalid","Elapsed":0}
-{"Time":"2023-03-29T13:37:33.154770493Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.154: cmd: \"WARN: The access URL http://example.com could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
-{"Time":"2023-03-29T13:37:33.439719632Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.43976961Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.439791036Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"View the Web UI: http://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.439807711Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.43982432Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.439851264Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:33.439869598Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:33.439886994Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.439905518Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:33.439924649Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.439941905Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:83: 2023-03-29 13:37:33.439: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:33.4399588Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:74: 2023-03-29 13:37:33.439: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:33.440230934Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.440: cmd: \"Started HTTP listener at http://[::]:45645\"\n"}
-{"Time":"2023-03-29T13:37:33.440246712Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.440: cmd: \"Started TLS/HTTPS listener at https://[::]:33779\"\n"}
-{"Time":"2023-03-29T13:37:33.440251896Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:634: 2023-03-29 13:37:33.440: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
-{"Time":"2023-03-29T13:37:33.440278535Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:635: 2023-03-29 13:37:33.440: cmd: ReadLine ctx has no deadline, using 10s\n"}
-{"Time":"2023-03-29T13:37:33.440295931Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:635: 2023-03-29 13:37:33.440: cmd: matched newline = \" http://[::]:45645\"\n"}
-{"Time":"2023-03-29T13:37:33.440327613Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:641: 2023-03-29 13:37:33.440: cmd: matched \"Started TLS/HTTPS listener at\" = \"Started TLS/HTTPS listener at\"\n"}
-{"Time":"2023-03-29T13:37:33.440339171Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:642: 2023-03-29 13:37:33.440: cmd: ReadLine ctx has no deadline, using 10s\n"}
-{"Time":"2023-03-29T13:37:33.440353544Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:642: 2023-03-29 13:37:33.440: cmd: matched newline = \" https://[::]:33779\"\n"}
-{"Time":"2023-03-29T13:37:33.462318671Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.155: cmd: \"WARN: Redirect HTTP to HTTPS is deprecated, please use Redirect to Access URL instead.\"\n"}
-{"Time":"2023-03-29T13:37:33.462349606Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.462356521Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"Started HTTP listener at http://[::]:38281\"\n"}
-{"Time":"2023-03-29T13:37:33.462372929Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"WARN: --tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.462395287Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"Started TLS/HTTPS listener at https://[::]:37895\"\n"}
-{"Time":"2023-03-29T13:37:33.46242925Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.462438148Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"View the Web UI: https://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.462469497Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.462484536Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.462637037Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:76: 2023-03-29 13:37:33.462: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.462649797Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:33.462659463Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.462670357Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:33.462694313Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.462718576Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:110: 2023-03-29 13:37:33.462: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.46272687Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:111: 2023-03-29 13:37:33.462: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:33.462736639Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:113: 2023-03-29 13:37:33.462: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.462758116Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:74: 2023-03-29 13:37:33.462: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:33.462768617Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:76: 2023-03-29 13:37:33.462: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.462788026Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:74: 2023-03-29 13:37:33.462: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:33.462795774Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:76: 2023-03-29 13:37:33.462: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.462816156Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:102: 2023-03-29 13:37:33.462: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:33.462931778Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":"--- PASS: TestServer/CanListenUnspecifiedv4 (0.60s)\n"}
-{"Time":"2023-03-29T13:37:33.462940935Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv4","Elapsed":0.6}
-{"Time":"2023-03-29T13:37:33.462948149Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener"}
-{"Time":"2023-03-29T13:37:33.462952114Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":"=== CONT TestServer/TLSRedirect/NoHTTPListener\n"}
-{"Time":"2023-03-29T13:37:33.464080755Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectNoHTTPListener857312755/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectNoHTTPListener857312755/002 --http-address --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSRedirectNoHTTPListener857312755/001/774178655 --tls-key-file /tmp/TestServerTLSRedirectNoHTTPListener857312755/001/2109956146 --wildcard-access-url *.example.com --access-url https://example.com\n"}
-{"Time":"2023-03-29T13:37:33.464227163Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.464: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.464240384Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.464: cmd: \"View the Web UI: https://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.464252515Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.464: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.46428589Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.464: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.472563896Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.155: cmd: \"WARN: The access URL http://example.com could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
-{"Time":"2023-03-29T13:37:33.472612391Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.47263698Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.472648847Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"View the Web UI: http://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.472703374Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.472714667Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.473028818Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:33.473041801Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:33.473048085Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.473053454Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:33.47305943Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.473064898Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:33.473070019Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.473075028Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:33.473080237Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.47308794Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:488: 2023-03-29 13:37:33.472: cmd: matched \"Started HTTP listener at\" = \"WARN: Redirect HTTP to HTTPS is deprecated, please use Redirect to Access URL instead.\\r\\n \\r\\r\\nStarted HTTP listener at\"\n"}
-{"Time":"2023-03-29T13:37:33.473094939Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:489: 2023-03-29 13:37:33.472: cmd: ReadLine ctx has no deadline, using 10s\n"}
-{"Time":"2023-03-29T13:37:33.473099786Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:489: 2023-03-29 13:37:33.472: cmd: matched newline = \" http://[::]:38281\"\n"}
-{"Time":"2023-03-29T13:37:33.473628418Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:493: 2023-03-29 13:37:33.473: cmd: matched \"Started TLS/HTTPS listener at \" = \"WARN: --tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead\\r\\r\\nStarted TLS/HTTPS listener at \"\n"}
-{"Time":"2023-03-29T13:37:33.473644808Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:494: 2023-03-29 13:37:33.473: cmd: ReadLine ctx has no deadline, using 10s\n"}
-{"Time":"2023-03-29T13:37:33.47365119Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:494: 2023-03-29 13:37:33.473: cmd: matched newline = \"https://[::]:37895\"\n"}
-{"Time":"2023-03-29T13:37:33.475250161Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:33.475281933Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:33.490892952Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:33.492145943Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:83: 2023-03-29 13:37:33.491: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:33.492183748Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:74: 2023-03-29 13:37:33.491: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:33.492192673Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:110: 2023-03-29 13:37:33.492: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.492199945Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:111: 2023-03-29 13:37:33.492: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:33.492206118Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:113: 2023-03-29 13:37:33.492: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.492296422Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:76: 2023-03-29 13:37:33.492: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.492316694Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:74: 2023-03-29 13:37:33.492: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:33.49232562Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:76: 2023-03-29 13:37:33.492: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.492355439Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:74: 2023-03-29 13:37:33.492: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:33.492364574Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:76: 2023-03-29 13:37:33.492: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.49240481Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:102: 2023-03-29 13:37:33.492: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:33.495511552Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":"--- PASS: TestServer/CanListenUnspecifiedv6 (0.63s)\n"}
-{"Time":"2023-03-29T13:37:33.495557907Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/CanListenUnspecifiedv6","Elapsed":0.63}
-{"Time":"2023-03-29T13:37:33.495569713Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener"}
-{"Time":"2023-03-29T13:37:33.495575692Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":"=== CONT TestServer/TLSRedirect/NoTLSListener\n"}
-{"Time":"2023-03-29T13:37:33.495582515Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectNoTLSListener2050465310/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectNoTLSListener2050465310/002 --http-address :0 --access-url https://example.com\n"}
-{"Time":"2023-03-29T13:37:33.514163923Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:33.51646695Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: View the Web UI: https://example.com\n"}
-{"Time":"2023-03-29T13:37:33.532093856Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.532820236Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:33.533254024Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.533887464Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Output":"--- PASS: TestServer/Logging/CreatesFile (0.69s)\n"}
-{"Time":"2023-03-29T13:37:33.580391994Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/CreatesFile","Elapsed":0.69}
-{"Time":"2023-03-29T13:37:33.580437708Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.194: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:33.580458446Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:33.580471806Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.58048911Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:33.580504524Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.580522972Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:33.5805328Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.580555757Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:33.580587561Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.580846451Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Output":"--- PASS: TestServer/Logging/JSON (0.67s)\n"}
-{"Time":"2023-03-29T13:37:33.580862096Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/JSON","Elapsed":0.67}
-{"Time":"2023-03-29T13:37:33.580868926Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect"}
-{"Time":"2023-03-29T13:37:33.580873653Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":"=== CONT TestServer/TLSRedirect/NoRedirect\n"}
-{"Time":"2023-03-29T13:37:33.582048889Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectNoRedirect2668740580/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectNoRedirect2668740580/002 --http-address :0 --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSRedirectNoRedirect2668740580/001/1517171254 --tls-key-file /tmp/TestServerTLSRedirectNoRedirect2668740580/001/1491051903 --wildcard-access-url *.example.com --access-url https://example.com\n"}
-{"Time":"2023-03-29T13:37:33.582187252Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard"}
-{"Time":"2023-03-29T13:37:33.582197387Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":"=== CONT TestServer/TLSRedirect/NoRedirectWithWildcard\n"}
-{"Time":"2023-03-29T13:37:33.583144917Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectNoRedirectWithWildcard3241718280/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectNoRedirectWithWildcard3241718280/002 --http-address --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSRedirectNoRedirectWithWildcard3241718280/001/2297363344 --tls-key-file /tmp/TestServerTLSRedirectNoRedirectWithWildcard3241718280/001/3868867528 --wildcard-access-url *.example.com --access-url https://example.com --redirect-to-access-url\n"}
-{"Time":"2023-03-29T13:37:33.609797366Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Output":"--- PASS: TestServer/TracerNoLeak (0.75s)\n"}
-{"Time":"2023-03-29T13:37:33.610858133Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TracerNoLeak","Elapsed":0.75}
-{"Time":"2023-03-29T13:37:33.6108818Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.202: cmd: \"WARN: Address is deprecated, please use HTTP Address and TLS Address instead.\"\n"}
-{"Time":"2023-03-29T13:37:33.610892881Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.610900074Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \"Started HTTP listener at http://[::]:32777\"\n"}
-{"Time":"2023-03-29T13:37:33.610935322Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.610944309Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \"View the Web UI: http://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.610972945Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.610991889Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.61104088Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:83: 2023-03-29 13:37:33.611: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:33.611050128Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.611: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:33.611098802Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:110: 2023-03-29 13:37:33.611: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.611114878Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:111: 2023-03-29 13:37:33.611: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:33.611120793Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:113: 2023-03-29 13:37:33.611: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.611180989Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.611: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.611193294Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.611: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:33.611200866Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.611: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.611204553Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.611: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:33.611207462Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.611: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.611215565Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:102: 2023-03-29 13:37:33.611: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:33.611336071Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Output":"--- PASS: TestServer/LocalAccessURL (0.71s)\n"}
-{"Time":"2023-03-29T13:37:33.613044416Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/LocalAccessURL","Elapsed":0.71}
-{"Time":"2023-03-29T13:37:33.613054355Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 1...\n"}
-{"Time":"2023-03-29T13:37:33.613144221Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 1\n"}
-{"Time":"2023-03-29T13:37:33.613176406Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" server_test.go:799: 2023-03-29 13:37:33.613: cmd: matched \"is deprecated\" = \"WARN: Address is deprecated\"\n"}
-{"Time":"2023-03-29T13:37:33.613666866Z","Action":"cont","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS"}
-{"Time":"2023-03-29T13:37:33.613682333Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":"=== CONT TestServer/DeprecatedAddress/TLS\n"}
-{"Time":"2023-03-29T13:37:33.614665673Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerDeprecatedAddressTLS1194868532/003 server --in-memory --address :0 --access-url https://example.com --tls-enable --tls-cert-file /tmp/TestServerDeprecatedAddressTLS1194868532/001/3357512070 --tls-key-file /tmp/TestServerDeprecatedAddressTLS1194868532/001/2886385591 --cache-dir /tmp/TestServerDeprecatedAddressTLS1194868532/002\n"}
-{"Time":"2023-03-29T13:37:33.614774341Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 2...\n"}
-{"Time":"2023-03-29T13:37:33.614835489Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 2\n"}
-{"Time":"2023-03-29T13:37:33.614847064Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:33.615151651Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.251: cmd: \"Started HTTP listener at http://[::]:46789\"\n"}
-{"Time":"2023-03-29T13:37:33.615187666Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \"WARN: The access URL https://foobarbaz.mydomain could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
-{"Time":"2023-03-29T13:37:33.615200461Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.615214043Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.615230151Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \"View the Web UI: https://foobarbaz.mydomain\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.615243576Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.61525094Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.616015109Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.617106937Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.251: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.617129585Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \"View the Web UI: https://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.617140564Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.617159456Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.617293213Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" server_test.go:219: 2023-03-29 13:37:33.617: cmd: matched \"this may cause unexpected problems when creating workspaces\" = \"Started HTTP listener at http://[::]:46789\\r\\nWARN: The access URL https://foobarbaz.mydomain could not be resolved, this may cause unexpected problems when creating workspaces\"\n"}
-{"Time":"2023-03-29T13:37:33.617419153Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" server_test.go:220: 2023-03-29 13:37:33.617: cmd: matched \"View the Web UI: https://foobarbaz.mydomain\" = \". Generate a unique *.try.coder.app URL by not specifying an access URL.\\r\\n \\r\\r\\n \\r\\nView the Web UI: https://foobarbaz.mydomain\"\n"}
-{"Time":"2023-03-29T13:37:33.617702539Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Output":"--- PASS: TestServer/Logging/Human (0.70s)\n"}
-{"Time":"2023-03-29T13:37:33.617717314Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Human","Elapsed":0.7}
-{"Time":"2023-03-29T13:37:33.617728397Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.346: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.617738582Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \"View the Web UI: https://google.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.617745792Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.617755568Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.617776141Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" server_test.go:238: 2023-03-29 13:37:33.617: cmd: matched \"View the Web UI: https://google.com\" = \"Started HTTP listener at http://[::]:39671\\r\\n \\r\\nView the Web UI: https://google.com\"\n"}
-{"Time":"2023-03-29T13:37:33.618700109Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:83: 2023-03-29 13:37:33.618: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:33.618717448Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.618: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:33.618729015Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:110: 2023-03-29 13:37:33.618: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.618738942Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:111: 2023-03-29 13:37:33.618: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:33.618748331Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:113: 2023-03-29 13:37:33.618: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.618809556Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.618: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.618823389Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.618: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:33.618833326Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.618: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.618840112Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.618: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:33.61884905Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.618: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.618860551Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:102: 2023-03-29 13:37:33.618: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:33.618993236Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":"--- PASS: TestServer/NoWarningWithRemoteAccessURL (0.72s)\n"}
-{"Time":"2023-03-29T13:37:33.620193889Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Elapsed":0.72}
-{"Time":"2023-03-29T13:37:33.620216262Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:33.620558208Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.620: cmd: \"Started TLS/HTTPS listener at https://[::]:40599\"\n"}
-{"Time":"2023-03-29T13:37:33.62057844Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.620: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.620586258Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.620: cmd: \"View the Web UI: https://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.620613734Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" server_test.go:641: 2023-03-29 13:37:33.620: cmd: matched \"Started TLS/HTTPS listener at\" = \"Started TLS/HTTPS listener at\"\n"}
-{"Time":"2023-03-29T13:37:33.620652863Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" server_test.go:642: 2023-03-29 13:37:33.620: cmd: ReadLine ctx has no deadline, using 10s\n"}
-{"Time":"2023-03-29T13:37:33.620669386Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" server_test.go:642: 2023-03-29 13:37:33.620: cmd: matched newline = \" https://[::]:40599\"\n"}
-{"Time":"2023-03-29T13:37:33.623941782Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:33.634109465Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.634: cmd: \"Started HTTP listener at http://[::]:44615\"\n"}
-{"Time":"2023-03-29T13:37:33.6341392Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" server_test.go:634: 2023-03-29 13:37:33.634: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
-{"Time":"2023-03-29T13:37:33.634159059Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" server_test.go:635: 2023-03-29 13:37:33.634: cmd: ReadLine ctx has no deadline, using 10s\n"}
-{"Time":"2023-03-29T13:37:33.634168084Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" server_test.go:635: 2023-03-29 13:37:33.634: cmd: matched newline = \" http://[::]:44615\"\n"}
-{"Time":"2023-03-29T13:37:33.639592274Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.639: cmd: \"WARN: Address is deprecated, please use HTTP Address and TLS Address instead.\"\n"}
-{"Time":"2023-03-29T13:37:33.639622715Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.639: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.639633384Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.639: cmd: \"Started TLS/HTTPS listener at https://[::]:44869\"\n"}
-{"Time":"2023-03-29T13:37:33.642427153Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.642: cmd: \"Started TLS/HTTPS listener at https://[::]:43889\"\n"}
-{"Time":"2023-03-29T13:37:33.642448651Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.642: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.642456925Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.642: cmd: \"View the Web UI: https://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.642468331Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" server_test.go:641: 2023-03-29 13:37:33.642: cmd: matched \"Started TLS/HTTPS listener at\" = \"Started TLS/HTTPS listener at\"\n"}
-{"Time":"2023-03-29T13:37:33.642477804Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" server_test.go:642: 2023-03-29 13:37:33.642: cmd: ReadLine ctx has no deadline, using 10s\n"}
-{"Time":"2023-03-29T13:37:33.642487898Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" server_test.go:642: 2023-03-29 13:37:33.642: cmd: matched newline = \" https://[::]:43889\"\n"}
-{"Time":"2023-03-29T13:37:33.646526946Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.646: cmd: \"Started HTTP listener at http://[::]:33323\"\n"}
-{"Time":"2023-03-29T13:37:33.646552176Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.646: cmd: \"Started TLS/HTTPS listener at https://[::]:36951\"\n"}
-{"Time":"2023-03-29T13:37:33.64656429Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.646: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.646572476Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.646: cmd: \"View the Web UI: https://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.646580011Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:634: 2023-03-29 13:37:33.646: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
-{"Time":"2023-03-29T13:37:33.646591361Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:635: 2023-03-29 13:37:33.646: cmd: ReadLine ctx has no deadline, using 10s\n"}
-{"Time":"2023-03-29T13:37:33.646616475Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:635: 2023-03-29 13:37:33.646: cmd: matched newline = \" http://[::]:33323\"\n"}
-{"Time":"2023-03-29T13:37:33.646662584Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:641: 2023-03-29 13:37:33.646: cmd: matched \"Started TLS/HTTPS listener at\" = \"Started TLS/HTTPS listener at\"\n"}
-{"Time":"2023-03-29T13:37:33.646681881Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:642: 2023-03-29 13:37:33.646: cmd: ReadLine ctx has no deadline, using 10s\n"}
-{"Time":"2023-03-29T13:37:33.646699362Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:642: 2023-03-29 13:37:33.646: cmd: matched newline = \" https://[::]:36951\"\n"}
-{"Time":"2023-03-29T13:37:33.648933788Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_api_active_users_duration_hour The number of users that have been active within the last hour.\n"}
-{"Time":"2023-03-29T13:37:33.648967031Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_api_active_users_duration_hour gauge\n"}
-{"Time":"2023-03-29T13:37:33.648980231Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_api_concurrent_requests The number of concurrent API requests.\n"}
-{"Time":"2023-03-29T13:37:33.649053919Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_api_concurrent_requests gauge\n"}
-{"Time":"2023-03-29T13:37:33.649069592Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_api_concurrent_requests 0\n"}
-{"Time":"2023-03-29T13:37:33.649078578Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_api_concurrent_websockets The total number of concurrent API websockets.\n"}
-{"Time":"2023-03-29T13:37:33.64908451Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_api_concurrent_websockets gauge\n"}
-{"Time":"2023-03-29T13:37:33.64908947Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_api_concurrent_websockets 0\n"}
-{"Time":"2023-03-29T13:37:33.649096688Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_api_workspace_latest_build_total The latest workspace builds with a status.\n"}
-{"Time":"2023-03-29T13:37:33.649103792Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_api_workspace_latest_build_total gauge\n"}
-{"Time":"2023-03-29T13:37:33.649110998Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_authz_authorize_duration_seconds Duration of the 'Authorize' call in seconds. Only counts calls that succeed.\n"}
-{"Time":"2023-03-29T13:37:33.649125082Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_authz_authorize_duration_seconds histogram\n"}
-{"Time":"2023-03-29T13:37:33.649136372Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.0005\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649158836Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.001\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649167411Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.002\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649174438Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.003\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649184378Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.005\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649205265Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.01\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649213251Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.02\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649222908Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.035\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649235415Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.05\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649253778Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.075\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649261752Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.1\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649271274Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.25\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649292314Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.75\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649300333Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"1\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649310057Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"+Inf\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649320729Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_sum{allowed=\"true\"} 0.002876051\n"}
-{"Time":"2023-03-29T13:37:33.649345041Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_count{allowed=\"true\"} 2\n"}
-{"Time":"2023-03-29T13:37:33.649356377Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_authz_prepare_authorize_duration_seconds Duration of the 'PrepareAuthorize' call in seconds.\n"}
-{"Time":"2023-03-29T13:37:33.649363746Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_authz_prepare_authorize_duration_seconds histogram\n"}
-{"Time":"2023-03-29T13:37:33.649370497Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.0005\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649391953Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.001\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649399779Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.002\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649410888Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.003\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649428841Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.005\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649438526Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.01\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649445746Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.02\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.64947999Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.035\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649486523Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.05\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649643599Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.075\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649652811Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.1\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649659776Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.25\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649677202Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.75\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.649684812Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"1\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.64970109Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"+Inf\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.64971819Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_sum 0\n"}
-{"Time":"2023-03-29T13:37:33.649725513Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_count 0\n"}
-{"Time":"2023-03-29T13:37:33.649741363Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"}
-{"Time":"2023-03-29T13:37:33.649751815Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_gc_duration_seconds summary\n"}
-{"Time":"2023-03-29T13:37:33.649770366Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"0\"} 1.6651e-05\n"}
-{"Time":"2023-03-29T13:37:33.649779436Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"0.25\"} 3.0073e-05\n"}
-{"Time":"2023-03-29T13:37:33.649795479Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"0.5\"} 3.3851e-05\n"}
-{"Time":"2023-03-29T13:37:33.649803042Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"0.75\"} 5.0874e-05\n"}
-{"Time":"2023-03-29T13:37:33.649822842Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"1\"} 0.000164674\n"}
-{"Time":"2023-03-29T13:37:33.649830516Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds_sum 0.000461803\n"}
-{"Time":"2023-03-29T13:37:33.64984149Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds_count 9\n"}
-{"Time":"2023-03-29T13:37:33.649859074Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_goroutines Number of goroutines that currently exist.\n"}
-{"Time":"2023-03-29T13:37:33.649876364Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_goroutines gauge\n"}
-{"Time":"2023-03-29T13:37:33.649883909Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_goroutines 400\n"}
-{"Time":"2023-03-29T13:37:33.649893852Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_info Information about the Go environment.\n"}
-{"Time":"2023-03-29T13:37:33.649915539Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_info gauge\n"}
-{"Time":"2023-03-29T13:37:33.649923065Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_info{version=\"go1.20\"} 1\n"}
-{"Time":"2023-03-29T13:37:33.649934513Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.\n"}
-{"Time":"2023-03-29T13:37:33.64994537Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_alloc_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.649964236Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_alloc_bytes 4.4139992e+07\n"}
-{"Time":"2023-03-29T13:37:33.649974501Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.\n"}
-{"Time":"2023-03-29T13:37:33.649985399Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_alloc_bytes_total counter\n"}
-{"Time":"2023-03-29T13:37:33.650002195Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_alloc_bytes_total 1.07033304e+08\n"}
-{"Time":"2023-03-29T13:37:33.650019066Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.\n"}
-{"Time":"2023-03-29T13:37:33.650026662Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_buck_hash_sys_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650037208Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_buck_hash_sys_bytes 1.487772e+06\n"}
-{"Time":"2023-03-29T13:37:33.650056379Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_frees_total Total number of frees.\n"}
-{"Time":"2023-03-29T13:37:33.650063678Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_frees_total counter\n"}
-{"Time":"2023-03-29T13:37:33.650079718Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_frees_total 341416\n"}
-{"Time":"2023-03-29T13:37:33.650087192Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.\n"}
-{"Time":"2023-03-29T13:37:33.65010648Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_gc_sys_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650113893Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_gc_sys_bytes 9.376592e+06\n"}
-{"Time":"2023-03-29T13:37:33.650132561Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and still in use.\n"}
-{"Time":"2023-03-29T13:37:33.650140278Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_alloc_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650152649Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_alloc_bytes 4.4139992e+07\n"}
-{"Time":"2023-03-29T13:37:33.650314526Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used.\n"}
-{"Time":"2023-03-29T13:37:33.650323426Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_idle_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650332952Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_idle_bytes 4.481024e+06\n"}
-{"Time":"2023-03-29T13:37:33.65035073Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use.\n"}
-{"Time":"2023-03-29T13:37:33.650358078Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_inuse_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650374519Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_inuse_bytes 4.6473216e+07\n"}
-{"Time":"2023-03-29T13:37:33.650392405Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_objects Number of allocated objects.\n"}
-{"Time":"2023-03-29T13:37:33.650400167Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_objects gauge\n"}
-{"Time":"2023-03-29T13:37:33.650411Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_objects 198877\n"}
-{"Time":"2023-03-29T13:37:33.650427198Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_released_bytes Number of heap bytes released to OS.\n"}
-{"Time":"2023-03-29T13:37:33.650443875Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_released_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.6504512Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_released_bytes 172032\n"}
-{"Time":"2023-03-29T13:37:33.650466769Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system.\n"}
-{"Time":"2023-03-29T13:37:33.650476593Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_sys_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650493555Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_sys_bytes 5.095424e+07\n"}
-{"Time":"2023-03-29T13:37:33.650501064Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection.\n"}
-{"Time":"2023-03-29T13:37:33.650517983Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_last_gc_time_seconds gauge\n"}
-{"Time":"2023-03-29T13:37:33.650530998Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_last_gc_time_seconds 1.680097053302355e+09\n"}
-{"Time":"2023-03-29T13:37:33.65054228Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_lookups_total Total number of pointer lookups.\n"}
-{"Time":"2023-03-29T13:37:33.650552934Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_lookups_total counter\n"}
-{"Time":"2023-03-29T13:37:33.650563901Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_lookups_total 0\n"}
-{"Time":"2023-03-29T13:37:33.650584173Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mallocs_total Total number of mallocs.\n"}
-{"Time":"2023-03-29T13:37:33.650591833Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mallocs_total counter\n"}
-{"Time":"2023-03-29T13:37:33.650607986Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mallocs_total 540293\n"}
-{"Time":"2023-03-29T13:37:33.650615529Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures.\n"}
-{"Time":"2023-03-29T13:37:33.650641182Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mcache_inuse_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650649185Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mcache_inuse_bytes 1200\n"}
-{"Time":"2023-03-29T13:37:33.650655792Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system.\n"}
-{"Time":"2023-03-29T13:37:33.650675584Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mcache_sys_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650682964Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mcache_sys_bytes 15600\n"}
-{"Time":"2023-03-29T13:37:33.650699492Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures.\n"}
-{"Time":"2023-03-29T13:37:33.65070695Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mspan_inuse_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650725284Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mspan_inuse_bytes 415680\n"}
-{"Time":"2023-03-29T13:37:33.650732919Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system.\n"}
-{"Time":"2023-03-29T13:37:33.650742233Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mspan_sys_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650753796Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mspan_sys_bytes 440640\n"}
-{"Time":"2023-03-29T13:37:33.650773086Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place.\n"}
-{"Time":"2023-03-29T13:37:33.650780844Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_next_gc_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650791619Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_next_gc_bytes 5.744056e+07\n"}
-{"Time":"2023-03-29T13:37:33.650939729Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations.\n"}
-{"Time":"2023-03-29T13:37:33.650948462Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_other_sys_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650960125Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_other_sys_bytes 407532\n"}
-{"Time":"2023-03-29T13:37:33.65097098Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_stack_inuse_bytes Number of bytes in use by the stack allocator.\n"}
-{"Time":"2023-03-29T13:37:33.650989112Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_stack_inuse_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.650999328Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_stack_inuse_bytes 3.506176e+06\n"}
-{"Time":"2023-03-29T13:37:33.651015975Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator.\n"}
-{"Time":"2023-03-29T13:37:33.651023745Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_stack_sys_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.651042622Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_stack_sys_bytes 3.506176e+06\n"}
-{"Time":"2023-03-29T13:37:33.651050141Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_sys_bytes Number of bytes obtained from system.\n"}
-{"Time":"2023-03-29T13:37:33.651066559Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_sys_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.65107676Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_sys_bytes 6.6188552e+07\n"}
-{"Time":"2023-03-29T13:37:33.651096334Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_threads Number of OS threads created.\n"}
-{"Time":"2023-03-29T13:37:33.651104041Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_threads gauge\n"}
-{"Time":"2023-03-29T13:37:33.651114838Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_threads 11\n"}
-{"Time":"2023-03-29T13:37:33.651133818Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.\n"}
-{"Time":"2023-03-29T13:37:33.651141836Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_cpu_seconds_total counter\n"}
-{"Time":"2023-03-29T13:37:33.651152615Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_cpu_seconds_total 0.47\n"}
-{"Time":"2023-03-29T13:37:33.651169215Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_max_fds Maximum number of open file descriptors.\n"}
-{"Time":"2023-03-29T13:37:33.651188139Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_max_fds gauge\n"}
-{"Time":"2023-03-29T13:37:33.651196288Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_max_fds 1.048576e+06\n"}
-{"Time":"2023-03-29T13:37:33.651207299Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_open_fds Number of open file descriptors.\n"}
-{"Time":"2023-03-29T13:37:33.651218057Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_open_fds gauge\n"}
-{"Time":"2023-03-29T13:37:33.651236861Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_open_fds 144\n"}
-{"Time":"2023-03-29T13:37:33.651248494Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_resident_memory_bytes Resident memory size in bytes.\n"}
-{"Time":"2023-03-29T13:37:33.651257364Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_resident_memory_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.651280196Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_resident_memory_bytes 1.14364416e+08\n"}
-{"Time":"2023-03-29T13:37:33.65128834Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_start_time_seconds Start time of the process since unix epoch in seconds.\n"}
-{"Time":"2023-03-29T13:37:33.651296824Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_start_time_seconds gauge\n"}
-{"Time":"2023-03-29T13:37:33.651307674Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_start_time_seconds 1.68009705209e+09\n"}
-{"Time":"2023-03-29T13:37:33.651326547Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_virtual_memory_bytes Virtual memory size in bytes.\n"}
-{"Time":"2023-03-29T13:37:33.651334348Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_virtual_memory_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.651345305Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_virtual_memory_bytes 1.4835712e+09\n"}
-{"Time":"2023-03-29T13:37:33.651361504Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_virtual_memory_max_bytes Maximum amount of virtual memory available in bytes.\n"}
-{"Time":"2023-03-29T13:37:33.651378854Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_virtual_memory_max_bytes gauge\n"}
-{"Time":"2023-03-29T13:37:33.651386339Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_virtual_memory_max_bytes 1.8446744073709552e+19\n"}
-{"Time":"2023-03-29T13:37:33.651397458Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.\n"}
-{"Time":"2023-03-29T13:37:33.65149956Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE promhttp_metric_handler_requests_in_flight gauge\n"}
-{"Time":"2023-03-29T13:37:33.65150712Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned promhttp_metric_handler_requests_in_flight 1\n"}
-{"Time":"2023-03-29T13:37:33.651512875Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.\n"}
-{"Time":"2023-03-29T13:37:33.651517405Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE promhttp_metric_handler_requests_total counter\n"}
-{"Time":"2023-03-29T13:37:33.651608377Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned promhttp_metric_handler_requests_total{code=\"200\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.651617771Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned promhttp_metric_handler_requests_total{code=\"500\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.651624722Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned promhttp_metric_handler_requests_total{code=\"503\"} 0\n"}
-{"Time":"2023-03-29T13:37:33.653302095Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:33.687811194Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.687: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.68784324Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.687: cmd: \"View the Web UI: https://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.687847743Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.687: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.687859388Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.687: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.69314768Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" server_test.go:829: 2023-03-29 13:37:33.690: cmd: matched \"is deprecated\" = \"WARN: Address is deprecated\"\n"}
-{"Time":"2023-03-29T13:37:33.699837388Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:33.700125946Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:33.700416667Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.701833349Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Output":"--- PASS: TestServer/RateLimit/Default (0.85s)\n"}
-{"Time":"2023-03-29T13:37:33.792395824Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Default","Elapsed":0.85}
-{"Time":"2023-03-29T13:37:33.792432021Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:33.792443805Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:33.796165422Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:83: 2023-03-29 13:37:33.795: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:33.796198633Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.796: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:33.797173408Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.797427041Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:33.79757722Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:33.797913033Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:33.797928952Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:33.803483718Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.803533703Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:33.803541349Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:33.803549183Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.80355496Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:33.803560722Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.803565907Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:33.803571719Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.803577128Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:33.803582995Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.803588238Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:110: 2023-03-29 13:37:33.799: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.803593211Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:111: 2023-03-29 13:37:33.799: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:33.803598107Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:113: 2023-03-29 13:37:33.799: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.807382663Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.807: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.807425699Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.807: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.812449918Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.812: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.812479278Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.812: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.81249279Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.812: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.812540743Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.812: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.81937956Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.819: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.819501503Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.819: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:33.819519256Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.819: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.819527309Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.819: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:33.819534022Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.819: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:33.819605916Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:102: 2023-03-29 13:37:33.819: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:33.819682321Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Output":"--- PASS: TestServer/RemoteAccessURL (0.92s)\n"}
-{"Time":"2023-03-29T13:37:33.819757496Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/RemoteAccessURL","Elapsed":0.92}
-{"Time":"2023-03-29T13:37:33.819772079Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.819: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.819781749Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.819: cmd: \"View the Web UI: https://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.819797518Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.819: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:33.819813792Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.819: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:33.851914311Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:33.85194979Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:33.852148613Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Output":"--- PASS: TestServer/Prometheus (1.00s)\n"}
-{"Time":"2023-03-29T13:37:33.855235903Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/Prometheus","Elapsed":1}
-{"Time":"2023-03-29T13:37:33.855263447Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:33.855284404Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:33.855351311Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.85648797Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Output":"--- PASS: TestServer/GitHubOAuth (1.01s)\n"}
-{"Time":"2023-03-29T13:37:33.898898347Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/GitHubOAuth","Elapsed":1.01}
-{"Time":"2023-03-29T13:37:33.898943644Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:33.898966965Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:33.899910542Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:33.899951915Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:33.899979591Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:33.931576217Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:33.931632315Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.932359012Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Output":"--- PASS: TestServer/RateLimit/Disabled (1.02s)\n"}
-{"Time":"2023-03-29T13:37:33.932605839Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Disabled","Elapsed":1.02}
-{"Time":"2023-03-29T13:37:33.932638521Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:33.944423479Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:33.944456273Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:33.944471028Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.944486505Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:33.944506728Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.948894392Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:33.949085454Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Output":"--- PASS: TestServer/Telemetry (1.09s)\n"}
-{"Time":"2023-03-29T13:37:33.973592223Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/Telemetry","Elapsed":1.09}
-{"Time":"2023-03-29T13:37:33.973627583Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:33.982109361Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Output":"--- PASS: TestServer/TLSValid (1.09s)\n"}
-{"Time":"2023-03-29T13:37:33.982145968Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValid","Elapsed":1.09}
-{"Time":"2023-03-29T13:37:33.982166462Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:83: 2023-03-29 13:37:33.982: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:33.982185903Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:74: 2023-03-29 13:37:33.982: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:33.983908041Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.983: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:33.983932298Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.983: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.983950164Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.983: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:33.983971383Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.983: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.983987252Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:110: 2023-03-29 13:37:33.983: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.984001811Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:111: 2023-03-29 13:37:33.983: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:33.984029981Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:113: 2023-03-29 13:37:33.983: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:33.999767444Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:33.999792066Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:33.999806103Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:33.999825199Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:33.999838052Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.000132942Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:34.000152725Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:34.013522575Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.013: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.013539719Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.013: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:34.013545396Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.013: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.013549166Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.013: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:34.013552799Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.013: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.013557098Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:102: 2023-03-29 13:37:34.013: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:34.013685528Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":"--- PASS: TestServer/DeprecatedAddress/HTTP (1.01s)\n"}
-{"Time":"2023-03-29T13:37:34.024907745Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/HTTP","Elapsed":1.01}
-{"Time":"2023-03-29T13:37:34.024925201Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:83: 2023-03-29 13:37:34.024: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:34.024929754Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:74: 2023-03-29 13:37:34.024: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:34.037436741Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:34.046649783Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:34.046: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:34.04666228Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:34.046: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.046671065Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:34.046: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:34.046674499Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:34.046: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.046678741Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:110: 2023-03-29 13:37:34.046: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.046682976Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:111: 2023-03-29 13:37:34.046: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:34.046687631Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:113: 2023-03-29 13:37:34.046: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.048234545Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:34.048465929Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:76: 2023-03-29 13:37:34.048: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.048473751Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:74: 2023-03-29 13:37:34.048: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:34.04848652Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:76: 2023-03-29 13:37:34.048: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.048494431Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:74: 2023-03-29 13:37:34.048: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:34.0485018Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:76: 2023-03-29 13:37:34.048: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.048515549Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:102: 2023-03-29 13:37:34.048: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:34.048685756Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":"--- PASS: TestServer/TLSRedirect/NoRedirectWithWildcard (0.47s)\n"}
-{"Time":"2023-03-29T13:37:34.049232577Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Elapsed":0.47}
-{"Time":"2023-03-29T13:37:34.049241005Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:34.049926527Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Output":"--- PASS: TestServer/RateLimit/Changed (1.07s)\n"}
-{"Time":"2023-03-29T13:37:34.049934686Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit/Changed","Elapsed":1.07}
-{"Time":"2023-03-29T13:37:34.049940803Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit","Output":"--- PASS: TestServer/RateLimit (0.00s)\n"}
-{"Time":"2023-03-29T13:37:34.050192839Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/RateLimit","Elapsed":0}
-{"Time":"2023-03-29T13:37:34.05020025Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:83: 2023-03-29 13:37:34.050: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:34.050204931Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.050: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:34.050237731Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:83: 2023-03-29 13:37:34.050: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:34.050243867Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.050: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:34.05027121Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:83: 2023-03-29 13:37:34.050: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:34.050278957Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:74: 2023-03-29 13:37:34.050: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:34.051736931Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:34.05174611Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:34.051751453Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.051758881Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:34.051780431Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.05178826Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:34.051870799Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.051881957Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:34.051887542Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.05189248Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:110: 2023-03-29 13:37:34.051: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.05189708Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:111: 2023-03-29 13:37:34.051: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:34.05190198Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:113: 2023-03-29 13:37:34.051: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.051911515Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:34.051922383Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:34.051927984Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.051938866Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:34.051946554Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.051962503Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:34.051971415Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.0519782Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:34.051987362Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.052007059Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:110: 2023-03-29 13:37:34.051: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.052015346Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:111: 2023-03-29 13:37:34.052: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:34.052021784Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:113: 2023-03-29 13:37:34.052: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.052400141Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:34.052410137Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:34.052420866Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.052427625Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:34.052440372Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.052448298Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:34.052457892Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.052465595Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:34.052483713Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.052491345Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:110: 2023-03-29 13:37:34.052: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.052498064Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:111: 2023-03-29 13:37:34.052: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:34.052506921Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:113: 2023-03-29 13:37:34.052: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.078632301Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.07865445Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:74: 2023-03-29 13:37:34.078: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:34.078658395Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.078664194Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:74: 2023-03-29 13:37:34.078: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:34.078667573Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.078670569Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:102: 2023-03-29 13:37:34.078: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:34.078838607Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Output":"--- PASS: TestServer/TLSRedirect/OK (1.06s)\n"}
-{"Time":"2023-03-29T13:37:34.078885902Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/OK","Elapsed":1.06}
-{"Time":"2023-03-29T13:37:34.078892321Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.078897167Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.078: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:34.078901364Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.078912845Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.078: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:34.078917725Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.07894293Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:102: 2023-03-29 13:37:34.078: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:34.079092731Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":"--- PASS: TestServer/TLSRedirect/NoHTTPListener (0.62s)\n"}
-{"Time":"2023-03-29T13:37:34.079144047Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Elapsed":0.62}
-{"Time":"2023-03-29T13:37:34.079152217Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.079: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.079158662Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.079: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:34.079162915Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.079: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.079168988Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.079: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:34.079178829Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.079: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.079196269Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:102: 2023-03-29 13:37:34.079: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:34.079336643Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":"--- PASS: TestServer/TLSRedirect/NoTLSListener (0.59s)\n"}
-{"Time":"2023-03-29T13:37:34.091089368Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Elapsed":0.59}
-{"Time":"2023-03-29T13:37:34.091112345Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:83: 2023-03-29 13:37:34.091: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:34.091119428Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.091: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:34.115772917Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:34.115801947Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:34.11581297Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.115819506Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:34.115824758Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.115830066Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:34.11583724Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.115844144Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:34.115860103Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.115888103Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:110: 2023-03-29 13:37:34.115: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.115903904Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:111: 2023-03-29 13:37:34.115: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:34.115919206Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:113: 2023-03-29 13:37:34.115: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.115976431Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:34.115986313Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:34.115997881Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.11602101Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:34.116029108Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.116: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.116476692Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.116: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.116491796Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.116: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:34.116497774Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.116: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.116504918Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.116: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:34.1165175Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.116: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.116539123Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:102: 2023-03-29 13:37:34.116: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:34.116717402Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Output":"--- PASS: TestServer/TLSAndHTTP (1.24s)\n"}
-{"Time":"2023-03-29T13:37:34.117404728Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSAndHTTP","Elapsed":1.24}
-{"Time":"2023-03-29T13:37:34.117418357Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:83: 2023-03-29 13:37:34.117: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:34.117441857Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:74: 2023-03-29 13:37:34.117: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:34.117475322Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:110: 2023-03-29 13:37:34.117: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.117484444Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:111: 2023-03-29 13:37:34.117: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:34.117494423Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:113: 2023-03-29 13:37:34.117: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.11754208Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:76: 2023-03-29 13:37:34.117: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.117551844Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:74: 2023-03-29 13:37:34.117: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:34.117570608Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:76: 2023-03-29 13:37:34.117: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.117578465Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:74: 2023-03-29 13:37:34.117: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:34.117597311Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:76: 2023-03-29 13:37:34.117: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.117616074Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:102: 2023-03-29 13:37:34.117: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:34.11776518Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":"--- PASS: TestServer/DeprecatedAddress/TLS (0.50s)\n"}
-{"Time":"2023-03-29T13:37:34.117776019Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress/TLS","Elapsed":0.5}
-{"Time":"2023-03-29T13:37:34.117782763Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress","Output":"--- PASS: TestServer/DeprecatedAddress (0.06s)\n"}
-{"Time":"2023-03-29T13:37:34.117841805Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/DeprecatedAddress","Elapsed":0.06}
-{"Time":"2023-03-29T13:37:34.117849931Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:83: 2023-03-29 13:37:34.117: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:34.117859828Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:74: 2023-03-29 13:37:34.117: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:34.118127485Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:83: 2023-03-29 13:37:34.118: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:34.118138813Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:74: 2023-03-29 13:37:34.118: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:34.118170526Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:110: 2023-03-29 13:37:34.118: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.11817918Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:111: 2023-03-29 13:37:34.118: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:34.118194579Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:113: 2023-03-29 13:37:34.118: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.11825734Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:76: 2023-03-29 13:37:34.118: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.118277979Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:74: 2023-03-29 13:37:34.118: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:34.118286381Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:76: 2023-03-29 13:37:34.118: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.118291513Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:74: 2023-03-29 13:37:34.118: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:34.118295899Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:76: 2023-03-29 13:37:34.118: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.11830286Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:102: 2023-03-29 13:37:34.118: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:34.118448813Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Output":"--- PASS: TestServer/TLSValidMultiple (1.23s)\n"}
-{"Time":"2023-03-29T13:37:34.118485143Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSValidMultiple","Elapsed":1.23}
-{"Time":"2023-03-29T13:37:34.118492964Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:34.118500337Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:34.118505085Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.118511668Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:34.118536832Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.118542137Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:34.118548654Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.118562305Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:34.118568736Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:34.1185733Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:110: 2023-03-29 13:37:34.118: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.118577677Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:111: 2023-03-29 13:37:34.118: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:34.118583972Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:113: 2023-03-29 13:37:34.118: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:34.347394026Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:76: 2023-03-29 13:37:34.347: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.347481519Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:74: 2023-03-29 13:37:34.347: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:34.347503045Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:76: 2023-03-29 13:37:34.347: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.347521875Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:74: 2023-03-29 13:37:34.347: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:34.347538956Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:76: 2023-03-29 13:37:34.347: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:34.347563965Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:102: 2023-03-29 13:37:34.347: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:34.347597239Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":"--- PASS: TestServer/TLSRedirect/NoRedirect (0.77s)\n"}
-{"Time":"2023-03-29T13:37:34.347618369Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect/NoRedirect","Elapsed":0.77}
-{"Time":"2023-03-29T13:37:34.347639094Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect","Output":"--- PASS: TestServer/TLSRedirect (0.05s)\n"}
-{"Time":"2023-03-29T13:37:37.495996215Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/TLSRedirect","Elapsed":0.05}
-{"Time":"2023-03-29T13:37:37.496041088Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:42403\n"}
-{"Time":"2023-03-29T13:37:37.497561163Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
-{"Time":"2023-03-29T13:37:37.854158017Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
-{"Time":"2023-03-29T13:37:37.871207973Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
-{"Time":"2023-03-29T13:37:37.871239014Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
-{"Time":"2023-03-29T13:37:37.871296925Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
-{"Time":"2023-03-29T13:37:37.871384307Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
-{"Time":"2023-03-29T13:37:37.871872118Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
-{"Time":"2023-03-29T13:37:37.872659674Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Stopping built-in PostgreSQL...\n"}
-{"Time":"2023-03-29T13:37:37.974547475Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Stopped built-in PostgreSQL\n"}
-{"Time":"2023-03-29T13:37:38.01812179Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Output":"--- PASS: TestServer/BuiltinPostgres (5.17s)\n"}
-{"Time":"2023-03-29T13:37:46.072262917Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/BuiltinPostgres","Elapsed":5.17}
-{"Time":"2023-03-29T13:37:46.072311252Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \"Started HTTP listener at http://[::]:35645\"\n"}
-{"Time":"2023-03-29T13:37:46.072335279Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \"WARN: The access URL http://example.com could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
-{"Time":"2023-03-29T13:37:46.07241558Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.072459905Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:46.07251014Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \"View the Web UI: http://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:46.072679873Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" server_test.go:1240: 2023-03-29 13:37:46.072: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
-{"Time":"2023-03-29T13:37:46.078543671Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:46.078573522Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:46.07859364Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:46.07861453Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"ERROR: Unexpected error, shutting down server: context deadline exceeded\"\n"}
-{"Time":"2023-03-29T13:37:46.078631014Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.078647037Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:46.078658003Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.078668373Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:46.078678662Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.078702364Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Shutting down provisioner daemon 3...\"\n"}
-{"Time":"2023-03-29T13:37:46.078719655Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.078730465Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Gracefully shut down provisioner daemon 3\"\n"}
-{"Time":"2023-03-29T13:37:46.078744589Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.078763734Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Shutting down provisioner daemon 1...\"\n"}
-{"Time":"2023-03-29T13:37:46.07878276Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.078801865Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Gracefully shut down provisioner daemon 1\"\n"}
-{"Time":"2023-03-29T13:37:46.07881665Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.078826759Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Shutting down provisioner daemon 2...\"\n"}
-{"Time":"2023-03-29T13:37:46.078840612Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.07885439Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Gracefully shut down provisioner daemon 2\"\n"}
-{"Time":"2023-03-29T13:37:46.07887537Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.081392321Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:46.081408574Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.081421854Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:46.081431192Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:46.081449657Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \"WARN: Graceful shutdown timed out\\r\"\n"}
-{"Time":"2023-03-29T13:37:46.14910839Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:83: 2023-03-29 13:37:46.149: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:46.149142371Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:74: 2023-03-29 13:37:46.149: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:46.149157567Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:110: 2023-03-29 13:37:46.149: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:46.149166448Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:111: 2023-03-29 13:37:46.149: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:46.149175898Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:113: 2023-03-29 13:37:46.149: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:46.149187205Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:76: 2023-03-29 13:37:46.149: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:46.14919623Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:74: 2023-03-29 13:37:46.149: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:46.149207373Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:76: 2023-03-29 13:37:46.149: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:46.149215685Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:74: 2023-03-29 13:37:46.149: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:46.149223347Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:76: 2023-03-29 13:37:46.149: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:46.149234369Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:102: 2023-03-29 13:37:46.149: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:46.149426722Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Output":"--- PASS: TestServer/Logging/Multiple (13.24s)\n"}
-{"Time":"2023-03-29T13:37:59.1117031Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Multiple","Elapsed":13.24}
-{"Time":"2023-03-29T13:37:59.111755068Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.111: cmd: \"2023-03-29 13:37:59.110 [DEBUG]\\t\u003cgithub.com/coder/coder/cli/server.go:260\u003e\\t(*RootCmd).Server.func1\\tstarted debug logging\"\n"}
-{"Time":"2023-03-29T13:37:59.111775176Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.111: cmd: \"Started HTTP listener at http://[::]:40007\"\n"}
-{"Time":"2023-03-29T13:37:59.111795675Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" server_test.go:1204: 2023-03-29 13:37:59.111: cmd: matched \"Started HTTP listener at\" = \"2023-03-29 13:37:59.110 [DEBUG]\\t\u003cgithub.com/coder/coder/cli/server.go:260\u003e\\t(*RootCmd).Server.func1\\tstarted debug logging\\r\\nStarted HTTP listener at\"\n"}
-{"Time":"2023-03-29T13:37:59.123488003Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.123: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:59.123536759Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.123: cmd: \"View the Web UI: http://example.com\\r\"\n"}
-{"Time":"2023-03-29T13:37:59.123550652Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.123: cmd: \" \"\n"}
-{"Time":"2023-03-29T13:37:59.123559998Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.123: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
-{"Time":"2023-03-29T13:37:59.144165093Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.123 [DEBUG]\\t(coderd.metrics_cache)\\t\u003cgithub.com/coder/coder/coderd/metricscache/metricscache.go:272\u003e\\t(*Cache).run\\tdeployment stats metrics refreshed\\t{\\\"took\\\": \\\"20.37µs\\\", \\\"interval\\\": \\\"30s\\\"}\"\n"}
-{"Time":"2023-03-29T13:37:59.144212142Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.139 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\\t(*Server).connect\\tconnected\"\n"}
-{"Time":"2023-03-29T13:37:59.1442281Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.139 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\\t(*Server).connect\\tconnected\"\n"}
-{"Time":"2023-03-29T13:37:59.14427036Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.139 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\\t(*Server).connect\\tconnected\"\n"}
-{"Time":"2023-03-29T13:37:59.14428344Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.143 [DEBUG]\\t(coderd.metrics_cache)\\t\u003cgithub.com/coder/coder/coderd/metricscache/metricscache.go:272\u003e\\t(*Cache).run\\ttemplate daus metrics refreshed\\t{\\\"took\\\": \\\"3.61474ms\\\", \\\"interval\\\": \\\"1h0m0s\\\"}\"\n"}
-{"Time":"2023-03-29T13:37:59.14429578Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
-{"Time":"2023-03-29T13:37:59.14430588Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Shutting down API server...\"\n"}
-{"Time":"2023-03-29T13:37:59.144315009Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.144324164Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Gracefully shut down API server\"\n"}
-{"Time":"2023-03-29T13:37:59.144339556Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.144624294Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Shutting down provisioner daemon 1...\"\n"}
-{"Time":"2023-03-29T13:37:59.144639861Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.14465833Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.144 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\\t(*Server).closeWithError\\tclosing server with error\\t{\\\"error\\\": null}\"\n"}
-{"Time":"2023-03-29T13:37:59.14468626Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Gracefully shut down provisioner daemon 1\"\n"}
-{"Time":"2023-03-29T13:37:59.144695878Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.144705145Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Shutting down provisioner daemon 2...\"\n"}
-{"Time":"2023-03-29T13:37:59.144718072Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.144727731Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.144 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\\t(*Server).closeWithError\\tclosing server with error\\t{\\\"error\\\": null}\"\n"}
-{"Time":"2023-03-29T13:37:59.144750279Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Gracefully shut down provisioner daemon 2\"\n"}
-{"Time":"2023-03-29T13:37:59.144764838Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.14477793Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Shutting down provisioner daemon 3...\"\n"}
-{"Time":"2023-03-29T13:37:59.144787313Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.144797136Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.144 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\\t(*Server).closeWithError\\tclosing server with error\\t{\\\"error\\\": null}\"\n"}
-{"Time":"2023-03-29T13:37:59.144810893Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Gracefully shut down provisioner daemon 3\"\n"}
-{"Time":"2023-03-29T13:37:59.144821417Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.144842079Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Waiting for WebSocket connections to close...\"\n"}
-{"Time":"2023-03-29T13:37:59.144855916Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.144867744Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Done waiting for WebSocket connections\"\n"}
-{"Time":"2023-03-29T13:37:59.14488065Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
-{"Time":"2023-03-29T13:37:59.146012139Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:83: 2023-03-29 13:37:59.145: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:37:59.146025303Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:74: 2023-03-29 13:37:59.145: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:37:59.14606956Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:110: 2023-03-29 13:37:59.146: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:59.146083103Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:111: 2023-03-29 13:37:59.146: cmd: closing out\n"}
-{"Time":"2023-03-29T13:37:59.146095911Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:113: 2023-03-29 13:37:59.146: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:37:59.146143558Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:76: 2023-03-29 13:37:59.146: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:59.146156843Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:74: 2023-03-29 13:37:59.146: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:37:59.146169893Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:76: 2023-03-29 13:37:59.146: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:59.146182763Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:74: 2023-03-29 13:37:59.146: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:37:59.146191645Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:76: 2023-03-29 13:37:59.146: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:37:59.146205122Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:102: 2023-03-29 13:37:59.146: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:37:59.146368404Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Output":"--- PASS: TestServer/Logging/Stackdriver (26.23s)\n"}
-{"Time":"2023-03-29T13:37:59.146391543Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging/Stackdriver","Elapsed":26.23}
-{"Time":"2023-03-29T13:37:59.146412558Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging","Output":"--- PASS: TestServer/Logging (0.00s)\n"}
-{"Time":"2023-03-29T13:37:59.146438143Z","Action":"pass","Package":"github.com/coder/coder/cli","Test":"TestServer/Logging","Elapsed":0}
-{"Time":"2023-03-29T13:37:59.1464552Z","Action":"output","Package":"github.com/coder/coder/cli","Test":"TestServer","Output":"--- FAIL: TestServer (0.05s)\n"}
-{"Time":"2023-03-29T13:37:59.146474821Z","Action":"fail","Package":"github.com/coder/coder/cli","Test":"TestServer","Elapsed":0.05}
-{"Time":"2023-03-29T13:37:59.146491309Z","Action":"output","Package":"github.com/coder/coder/cli","Output":"FAIL\n"}
-{"Time":"2023-03-29T13:37:59.158021068Z","Action":"output","Package":"github.com/coder/coder/cli","Output":"FAIL\tgithub.com/coder/coder/cli\t26.514s\n"}
-{"Time":"2023-03-29T13:37:59.158054855Z","Action":"fail","Package":"github.com/coder/coder/cli","Elapsed":26.514}
-{"Time":"2023-03-29T13:38:02.724238056Z","Action":"start","Package":"github.com/coder/coder/cli/cliui"}
-{"Time":"2023-03-29T13:38:02.754440648Z","Action":"run","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth"}
-{"Time":"2023-03-29T13:38:02.75448054Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":"=== RUN TestGitAuth\n"}
-{"Time":"2023-03-29T13:38:02.754486705Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":"=== PAUSE TestGitAuth\n"}
-{"Time":"2023-03-29T13:38:02.754490044Z","Action":"pause","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth"}
-{"Time":"2023-03-29T13:38:02.754493443Z","Action":"run","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt"}
-{"Time":"2023-03-29T13:38:02.754496272Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt","Output":"=== RUN TestPrompt\n"}
-{"Time":"2023-03-29T13:38:02.754504892Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt","Output":"=== PAUSE TestPrompt\n"}
-{"Time":"2023-03-29T13:38:02.754507539Z","Action":"pause","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt"}
-{"Time":"2023-03-29T13:38:02.754510534Z","Action":"cont","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth"}
-{"Time":"2023-03-29T13:38:02.754514471Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":"=== CONT TestGitAuth\n"}
-{"Time":"2023-03-29T13:38:02.754642422Z","Action":"cont","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt"}
-{"Time":"2023-03-29T13:38:02.754653067Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt","Output":"=== CONT TestPrompt\n"}
-{"Time":"2023-03-29T13:38:02.754658206Z","Action":"run","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success"}
-{"Time":"2023-03-29T13:38:02.754660941Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":"=== RUN TestPrompt/Success\n"}
-{"Time":"2023-03-29T13:38:02.754664503Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":"=== PAUSE TestPrompt/Success\n"}
-{"Time":"2023-03-29T13:38:02.754666941Z","Action":"pause","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success"}
-{"Time":"2023-03-29T13:38:02.754671476Z","Action":"run","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm"}
-{"Time":"2023-03-29T13:38:02.754673908Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":"=== RUN TestPrompt/Confirm\n"}
-{"Time":"2023-03-29T13:38:02.754676919Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":"=== PAUSE TestPrompt/Confirm\n"}
-{"Time":"2023-03-29T13:38:02.754683157Z","Action":"pause","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm"}
-{"Time":"2023-03-29T13:38:02.754688172Z","Action":"run","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip"}
-{"Time":"2023-03-29T13:38:02.754690617Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":"=== RUN TestPrompt/Skip\n"}
-{"Time":"2023-03-29T13:38:02.754693754Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":"=== PAUSE TestPrompt/Skip\n"}
-{"Time":"2023-03-29T13:38:02.754696128Z","Action":"pause","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip"}
-{"Time":"2023-03-29T13:38:02.754700672Z","Action":"run","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON"}
-{"Time":"2023-03-29T13:38:02.754703008Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":"=== RUN TestPrompt/JSON\n"}
-{"Time":"2023-03-29T13:38:02.754718094Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":"=== PAUSE TestPrompt/JSON\n"}
-{"Time":"2023-03-29T13:38:02.754723349Z","Action":"pause","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON"}
-{"Time":"2023-03-29T13:38:02.754728958Z","Action":"run","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON"}
-{"Time":"2023-03-29T13:38:02.754731492Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":"=== RUN TestPrompt/BadJSON\n"}
-{"Time":"2023-03-29T13:38:02.754734435Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":"=== PAUSE TestPrompt/BadJSON\n"}
-{"Time":"2023-03-29T13:38:02.754736902Z","Action":"pause","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON"}
-{"Time":"2023-03-29T13:38:02.754748953Z","Action":"run","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON"}
-{"Time":"2023-03-29T13:38:02.754751439Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":"=== RUN TestPrompt/MultilineJSON\n"}
-{"Time":"2023-03-29T13:38:02.754755982Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":"=== PAUSE TestPrompt/MultilineJSON\n"}
-{"Time":"2023-03-29T13:38:02.754760728Z","Action":"pause","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON"}
-{"Time":"2023-03-29T13:38:02.754764892Z","Action":"cont","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success"}
-{"Time":"2023-03-29T13:38:02.754767252Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":"=== CONT TestPrompt/Success\n"}
-{"Time":"2023-03-29T13:38:02.754976869Z","Action":"cont","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON"}
-{"Time":"2023-03-29T13:38:02.754982653Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":"=== CONT TestPrompt/MultilineJSON\n"}
-{"Time":"2023-03-29T13:38:02.755108229Z","Action":"cont","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON"}
-{"Time":"2023-03-29T13:38:02.755113844Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":"=== CONT TestPrompt/BadJSON\n"}
-{"Time":"2023-03-29T13:38:02.755212757Z","Action":"cont","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON"}
-{"Time":"2023-03-29T13:38:02.755218041Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":"=== CONT TestPrompt/JSON\n"}
-{"Time":"2023-03-29T13:38:02.755315155Z","Action":"cont","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip"}
-{"Time":"2023-03-29T13:38:02.755318778Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":"=== CONT TestPrompt/Skip\n"}
-{"Time":"2023-03-29T13:38:02.755513491Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:83: 2023-03-29 13:38:02.755: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:38:02.755529621Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.755: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:38:02.755596722Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.755: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.755601928Z","Action":"cont","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm"}
-{"Time":"2023-03-29T13:38:02.755604522Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":"=== CONT TestPrompt/Confirm\n"}
-{"Time":"2023-03-29T13:38:02.756154509Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:110: 2023-03-29 13:38:02.756: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.756161274Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:111: 2023-03-29 13:38:02.756: cmd: closing out\n"}
-{"Time":"2023-03-29T13:38:02.756179269Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:113: 2023-03-29 13:38:02.756: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.756195571Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:38:02.75620977Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.756222494Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:38:02.756250235Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.756263435Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:102: 2023-03-29 13:38:02.756: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:38:02.75631973Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:83: 2023-03-29 13:38:02.756: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:38:02.756334455Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:38:02.756398184Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed pty: pty: closed\n"}
-{"Time":"2023-03-29T13:38:02.75641208Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:38:02.756425561Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.756442572Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:38:02.756457245Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.756473084Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:102: 2023-03-29 13:38:02.756: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:38:02.756487964Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Output":"--- PASS: TestPrompt/Skip (0.00s)\n"}
-{"Time":"2023-03-29T13:38:02.756674486Z","Action":"pass","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Skip","Elapsed":0}
-{"Time":"2023-03-29T13:38:02.756683362Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" prompt_test.go:51: 2023-03-29 13:38:02.756: cmd: matched \"Example\" = \"\u003e Example\"\n"}
-{"Time":"2023-03-29T13:38:02.756700414Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" prompt_test.go:52: 2023-03-29 13:38:02.756: cmd: stdin: \"yes\\r\"\n"}
-{"Time":"2023-03-29T13:38:02.756759525Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" prompt_test.go:108: 2023-03-29 13:38:02.756: cmd: matched \"Example\" = \"\u003e Example\"\n"}
-{"Time":"2023-03-29T13:38:02.75678767Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" prompt_test.go:109: 2023-03-29 13:38:02.756: cmd: stdin: \"{}\\r\"\n"}
-{"Time":"2023-03-29T13:38:02.756838147Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" prompt_test.go:124: 2023-03-29 13:38:02.756: cmd: matched \"Example\" = \"\u003e Example\"\n"}
-{"Time":"2023-03-29T13:38:02.756862647Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" prompt_test.go:125: 2023-03-29 13:38:02.756: cmd: stdin: \"{a\\r\"\n"}
-{"Time":"2023-03-29T13:38:02.756932142Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" prompt_test.go:140: 2023-03-29 13:38:02.756: cmd: matched \"Example\" = \"\u003e Example\"\n"}
-{"Time":"2023-03-29T13:38:02.756958031Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" prompt_test.go:141: 2023-03-29 13:38:02.756: cmd: stdin: \"{\\n\\\"test\\\": \\\"wow\\\"\\n}\\r\"\n"}
-{"Time":"2023-03-29T13:38:02.757013878Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.756: cmd: \"You must authenticate with GitHub to create a workspace with this template. Visit:\"\n"}
-{"Time":"2023-03-29T13:38:02.757025551Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\"\n"}
-{"Time":"2023-03-29T13:38:02.757047114Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\\thttps://example.com/gitauth/github?redirect=%2Fgitauth%3Fnotify\"\n"}
-{"Time":"2023-03-29T13:38:02.757065197Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\"\n"}
-{"Time":"2023-03-29T13:38:02.757096029Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\\r\\r⠈⠁ Waiting for Git authentication...\\rSuccessfully authenticated with GitHub!\"\n"}
-{"Time":"2023-03-29T13:38:02.757108294Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\"\n"}
-{"Time":"2023-03-29T13:38:02.757140128Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" gitauth_test.go:53: 2023-03-29 13:38:02.757: cmd: matched \"You must authenticate with\" = \"You must authenticate with\"\n"}
-{"Time":"2023-03-29T13:38:02.757190596Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" gitauth_test.go:54: 2023-03-29 13:38:02.757: cmd: matched \"https://example.com/gitauth/github\" = \" GitHub to create a workspace with this template. Visit:\\r\\n\\r\\n\\thttps://example.com/gitauth/github\"\n"}
-{"Time":"2023-03-29T13:38:02.757264811Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" gitauth_test.go:55: 2023-03-29 13:38:02.757: cmd: matched \"Successfully authenticated with GitHub\" = \"?redirect=%2Fgitauth%3Fnotify\\r\\n\\r\\n\\r\\r⠈⠁ Waiting for Git authentication...\\rSuccessfully authenticated with GitHub\"\n"}
-{"Time":"2023-03-29T13:38:02.757294284Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:83: 2023-03-29 13:38:02.757: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:38:02.757307619Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:38:02.757350699Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:110: 2023-03-29 13:38:02.757: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.757369646Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:111: 2023-03-29 13:38:02.757: cmd: closing out\n"}
-{"Time":"2023-03-29T13:38:02.757388269Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:113: 2023-03-29 13:38:02.757: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.757437516Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.7574545Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:38:02.757468757Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.757483649Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:38:02.757496041Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.757513203Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:102: 2023-03-29 13:38:02.757: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:38:02.757518992Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Output":"--- PASS: TestGitAuth (0.00s)\n"}
-{"Time":"2023-03-29T13:38:02.75757314Z","Action":"pass","Package":"github.com/coder/coder/cli/cliui","Test":"TestGitAuth","Elapsed":0}
-{"Time":"2023-03-29T13:38:02.757576987Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" prompt_test.go:34: 2023-03-29 13:38:02.757: cmd: matched \"Example\" = \"\u003e Example\"\n"}
-{"Time":"2023-03-29T13:38:02.757605985Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" prompt_test.go:35: 2023-03-29 13:38:02.757: cmd: stdin: \"hello\\r\"\n"}
-{"Time":"2023-03-29T13:38:02.757679461Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\u003e Example hello\"\n"}
-{"Time":"2023-03-29T13:38:02.757731096Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:83: 2023-03-29 13:38:02.757: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:38:02.757745488Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:38:02.757786052Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:110: 2023-03-29 13:38:02.757: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.757793961Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:111: 2023-03-29 13:38:02.757: cmd: closing out\n"}
-{"Time":"2023-03-29T13:38:02.757809024Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:113: 2023-03-29 13:38:02.757: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.757853587Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.757869751Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:38:02.757888281Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.757896143Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:38:02.757912615Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.757929309Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:102: 2023-03-29 13:38:02.757: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:38:02.757934794Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Output":"--- PASS: TestPrompt/Success (0.00s)\n"}
-{"Time":"2023-03-29T13:38:02.757963355Z","Action":"pass","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Success","Elapsed":0}
-{"Time":"2023-03-29T13:38:02.757966707Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\u003e Example {\"\n"}
-{"Time":"2023-03-29T13:38:02.757981822Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\\\"test\\\": \\\"wow\\\"\"\n"}
-{"Time":"2023-03-29T13:38:02.757994456Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"}\"\n"}
-{"Time":"2023-03-29T13:38:02.75807554Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:83: 2023-03-29 13:38:02.758: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:38:02.758091845Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:38:02.758122927Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:110: 2023-03-29 13:38:02.758: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.758135875Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:111: 2023-03-29 13:38:02.758: cmd: closing out\n"}
-{"Time":"2023-03-29T13:38:02.758148587Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:113: 2023-03-29 13:38:02.758: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.758193703Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.758208689Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:38:02.75822251Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.758236906Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:38:02.758250264Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.758266392Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:102: 2023-03-29 13:38:02.758: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:38:02.758271916Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":"--- PASS: TestPrompt/MultilineJSON (0.00s)\n"}
-{"Time":"2023-03-29T13:38:02.758311206Z","Action":"pass","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/MultilineJSON","Elapsed":0}
-{"Time":"2023-03-29T13:38:02.758314623Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.758: cmd: \"\u003e Example {a\"\n"}
-{"Time":"2023-03-29T13:38:02.75836333Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:83: 2023-03-29 13:38:02.758: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:38:02.758378803Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:38:02.758418274Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:110: 2023-03-29 13:38:02.758: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.758425803Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:111: 2023-03-29 13:38:02.758: cmd: closing out\n"}
-{"Time":"2023-03-29T13:38:02.75844102Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:113: 2023-03-29 13:38:02.758: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.758482492Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.758498572Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:38:02.758513045Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.758526569Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:38:02.75853994Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.758557098Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:102: 2023-03-29 13:38:02.758: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:38:02.758562364Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Output":"--- PASS: TestPrompt/BadJSON (0.00s)\n"}
-{"Time":"2023-03-29T13:38:02.758589722Z","Action":"pass","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/BadJSON","Elapsed":0}
-{"Time":"2023-03-29T13:38:02.758593068Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.758: cmd: \"\u003e Example {}\"\n"}
-{"Time":"2023-03-29T13:38:02.758640512Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:83: 2023-03-29 13:38:02.758: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:38:02.75865573Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:38:02.75868581Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:110: 2023-03-29 13:38:02.758: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.758697594Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:111: 2023-03-29 13:38:02.758: cmd: closing out\n"}
-{"Time":"2023-03-29T13:38:02.758712004Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:113: 2023-03-29 13:38:02.758: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.758753059Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.758789238Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:38:02.75880265Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.758821412Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:38:02.758835398Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.758851842Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:102: 2023-03-29 13:38:02.758: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:38:02.758857116Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Output":"--- PASS: TestPrompt/JSON (0.00s)\n"}
-{"Time":"2023-03-29T13:38:02.758897163Z","Action":"pass","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/JSON","Elapsed":0}
-{"Time":"2023-03-29T13:38:02.758900525Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:121: 2023-03-29 13:38:02.758: cmd: \"\u003e Example (yes/no) yes\"\n"}
-{"Time":"2023-03-29T13:38:02.758967291Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:83: 2023-03-29 13:38:02.758: cmd: closing tpty: close\n"}
-{"Time":"2023-03-29T13:38:02.758981151Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing pty\n"}
-{"Time":"2023-03-29T13:38:02.759023005Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:110: 2023-03-29 13:38:02.758: cmd: copy done: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.759035878Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:111: 2023-03-29 13:38:02.759: cmd: closing out\n"}
-{"Time":"2023-03-29T13:38:02.759048644Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:113: 2023-03-29 13:38:02.759: cmd: closed out: read /dev/ptmx: file already closed\n"}
-{"Time":"2023-03-29T13:38:02.759094323Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:76: 2023-03-29 13:38:02.759: cmd: closed pty: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.759115364Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:74: 2023-03-29 13:38:02.759: cmd: closing logw\n"}
-{"Time":"2023-03-29T13:38:02.75913071Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:76: 2023-03-29 13:38:02.759: cmd: closed logw: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.759144071Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:74: 2023-03-29 13:38:02.759: cmd: closing logr\n"}
-{"Time":"2023-03-29T13:38:02.75915741Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:76: 2023-03-29 13:38:02.759: cmd: closed logr: \u003cnil\u003e\n"}
-{"Time":"2023-03-29T13:38:02.759174685Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:102: 2023-03-29 13:38:02.759: cmd: closed tpty\n"}
-{"Time":"2023-03-29T13:38:02.759180019Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Output":"--- PASS: TestPrompt/Confirm (0.00s)\n"}
-{"Time":"2023-03-29T13:38:02.759187539Z","Action":"pass","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt/Confirm","Elapsed":0}
-{"Time":"2023-03-29T13:38:02.75919072Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt","Output":"--- PASS: TestPrompt (0.00s)\n"}
-{"Time":"2023-03-29T13:38:02.759194742Z","Action":"pass","Package":"github.com/coder/coder/cli/cliui","Test":"TestPrompt","Elapsed":0}
-{"Time":"2023-03-29T13:38:02.759198362Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Output":"PASS\n"}
-{"Time":"2023-03-29T13:38:02.761021961Z","Action":"output","Package":"github.com/coder/coder/cli/cliui","Output":"ok \tgithub.com/coder/coder/cli/cliui\t0.037s\n"}
-{"Time":"2023-03-29T13:38:02.761046557Z","Action":"pass","Package":"github.com/coder/coder/cli/cliui","Elapsed":0.037}
+{"Time":"2023-03-29T13:37:23.355347397Z","Action":"start","Package":"github.com/coder/coder/v2/agent"}
+{"Time":"2023-03-29T13:37:23.381695238Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec"}
+{"Time":"2023-03-29T13:37:23.38177342Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":"=== RUN TestAgent_SessionExec\n"}
+{"Time":"2023-03-29T13:37:23.381791755Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":"=== PAUSE TestAgent_SessionExec\n"}
+{"Time":"2023-03-29T13:37:23.381805147Z","Action":"pause","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec"}
+{"Time":"2023-03-29T13:37:23.381827974Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell"}
+{"Time":"2023-03-29T13:37:23.381835977Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":"=== RUN TestAgent_SessionTTYShell\n"}
+{"Time":"2023-03-29T13:37:23.381850018Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":"=== PAUSE TestAgent_SessionTTYShell\n"}
+{"Time":"2023-03-29T13:37:23.381857444Z","Action":"pause","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell"}
+{"Time":"2023-03-29T13:37:23.381868815Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode"}
+{"Time":"2023-03-29T13:37:23.381876252Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== RUN TestAgent_SessionTTYExitCode\n"}
+{"Time":"2023-03-29T13:37:23.381885049Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== PAUSE TestAgent_SessionTTYExitCode\n"}
+{"Time":"2023-03-29T13:37:23.381896641Z","Action":"pause","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode"}
+{"Time":"2023-03-29T13:37:23.381914968Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD"}
+{"Time":"2023-03-29T13:37:23.381930694Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":"=== RUN TestAgent_Session_TTY_MOTD\n"}
+{"Time":"2023-03-29T13:37:23.459584829Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:23.45962803Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:23.459637144Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:23.459709589Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:23.459766441Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:23.459896565Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:23.45992711Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:23.460013936Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.459 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:23.460047337Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:23.460151722Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:23.460178571Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:23.460311639Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:07ad6d06cd8b5ff2\n"}
+{"Time":"2023-03-29T13:37:23.460356174Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:23.460498076Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:23.460536017Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:23.460590141Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:23.460620636Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:23.460662149Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:23.460698929Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:23.460733289Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:23.460789117Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:23.460856196Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:23.460986291Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.460 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:23.46141926Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.461506329Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:23.461608094Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:23.46164708Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
+{"Time":"2023-03-29T13:37:23.461997997Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 48127, \"DERPPort\": 44839, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"/tmp/TestAgent_Session_TTY_MOTD1157664819/001/motd\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
+{"Time":"2023-03-29T13:37:23.462041275Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.461 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
+{"Time":"2023-03-29T13:37:23.462418253Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:23.46244618Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:23.462489007Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:23.462532307Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:23.462584588Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:23.462669431Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:23.462699701Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:23.46277017Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:23.46280348Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:23.46284612Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:23.462890638Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:23.463014252Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.462 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:8ac4cc2c7460d56f\n"}
+{"Time":"2023-03-29T13:37:23.463040585Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:23.463167416Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:23.463228086Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:23.463265117Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:23.463307341Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:23.463345133Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:23.463380146Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:23.463437865Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:23.463474458Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:23.463513083Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:23.463651429Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:23.463966826Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.463992242Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.463 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:23.464079789Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.464 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
+{"Time":"2023-03-29T13:37:23.464100162Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.464 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
+{"Time":"2023-03-29T13:37:23.464314405Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.464 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.464098Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:23.470436775Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.470 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:23.473142737Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.473 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:23.473905461Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.473 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:23.476012898Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.475 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.4615Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:23.476335333Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.476 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:23.476591432Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.476 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:23.476641778Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.476 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:23.476677236Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.476 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:23.47834267Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.478 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:23.478773607Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.478 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:23.479289417Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.479 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:23.484191154Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.4615Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:23.484233027Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.484426557Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
+{"Time":"2023-03-29T13:37:23.484790305Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:23.484946523Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.484 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:23.485230922Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:23.485304276Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:23.485410816Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:23.485506941Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:23.485747079Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:23.485857203Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:23.485942868Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.485 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:23.486354495Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.464098Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:23.486406209Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.486580191Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\n"}
+{"Time":"2023-03-29T13:37:23.486731116Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:23.486910536Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.486 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:23.48721125Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:23.487271545Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:23.487362767Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:23.487505661Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:23.48757023Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:23.487687075Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:23.487755179Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.487 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:23.533579774Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.533 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:41471 derp=1 derpdist=1v4:5ms\n"}
+{"Time":"2023-03-29T13:37:23.533653394Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.533 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:23.534036522Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.533 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:41471 (stun), 172.20.0.2:41471 (local)\n"}
+{"Time":"2023-03-29T13:37:23.534320881Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.534012576 +0000 UTC m=+0.173619877 Peers:[] LocalAddrs:[{Addr:127.0.0.1:41471 Type:stun} {Addr:172.20.0.2:41471 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:23.534485142Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:34768 derp=1 derpdist=1v4:4ms\n"}
+{"Time":"2023-03-29T13:37:23.534597588Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:23.534893919Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:34768 (stun), 172.20.0.2:34768 (local)\n"}
+{"Time":"2023-03-29T13:37:23.535056614Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.534 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.534872863 +0000 UTC m=+0.174480149 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34768 Type:stun} {Addr:172.20.0.2:34768 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:23.535722359Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.535 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:23.535840193Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.535 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:23.53601927Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.535 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.535826298 +0000 UTC m=+0.175433549 Peers:[] LocalAddrs:[{Addr:127.0.0.1:41471 Type:stun} {Addr:172.20.0.2:41471 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:23.536284979Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.536 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.004534057}}}\n"}
+{"Time":"2023-03-29T13:37:23.536457311Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.536 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.5343Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.536888005Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.536 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.5343Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.536962253Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.536 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.537168204Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:23.537809136Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.537 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:23.537959175Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:23.538044116Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:23.538236588Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.538043332 +0000 UTC m=+0.177650587 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34768 Type:stun} {Addr:172.20.0.2:34768 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:23.538347057Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.004475683}}}\n"}
+{"Time":"2023-03-29T13:37:23.538488084Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.535038Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.538915728Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.535038Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.538974002Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.538 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.539154829Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.539 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:23.539540545Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.539 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:23.539953465Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.539 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.539778Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.004534057}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.540373922Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.540 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.539778Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.004534057}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.540832675Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.540 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.540920246Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.540 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:23.541080962Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.541 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:23.541207198Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.541 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:23.541540025Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.541 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:23.541869031Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.541 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.541715Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.004475683}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.542270097Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.541715Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.004475683}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.542592821Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.542683196Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:23.542867815Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:23.542985478Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.542 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:23.55226213Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.552 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:23.552392256Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.552 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:23.589932282Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.589 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:34768 derp=1 derpdist=1v4:4ms\n"}
+{"Time":"2023-03-29T13:37:23.591567427Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.591 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:41471 derp=1 derpdist=1v4:2ms\n"}
+{"Time":"2023-03-29T13:37:23.598751818Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.598 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0x7ra] ...\n"}
+{"Time":"2023-03-29T13:37:23.598954894Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.598 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [0x7ra] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:23.599390218Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.599 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [vT+Vd] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:23.599516651Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.599 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [0x7ra] d:8ac4cc2c7460d56f now using 172.20.0.2:34768\n"}
+{"Time":"2023-03-29T13:37:23.599643596Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.599 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0x7ra] ...\n"}
+{"Time":"2023-03-29T13:37:23.59972717Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.599 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0x7ra] ...\n"}
+{"Time":"2023-03-29T13:37:23.600132385Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:23.600385251Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:23.600423261Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:23.600476824Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:23.600511572Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:23.600547765Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:23.600583838Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Starting\n"}
+{"Time":"2023-03-29T13:37:23.600650591Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Sending handshake initiation\n"}
+{"Time":"2023-03-29T13:37:23.601044073Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.600 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [vT+Vd] now active, reconfiguring WireGuard\n"}
+{"Time":"2023-03-29T13:37:23.601107152Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:23.601327676Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:23.601369666Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:23.601396751Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:23.601460927Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:23.601495639Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:23.601526183Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Starting\n"}
+{"Time":"2023-03-29T13:37:23.60175503Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Received handshake initiation\n"}
+{"Time":"2023-03-29T13:37:23.601775856Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.601 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Sending handshake response\n"}
+{"Time":"2023-03-29T13:37:23.602280259Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.602 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.602127595 +0000 UTC m=+0.241734812 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33}] LocalAddrs:[{Addr:127.0.0.1:34768 Type:stun} {Addr:172.20.0.2:34768 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:23.602838109Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.602 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Received handshake response\n"}
+{"Time":"2023-03-29T13:37:23.602930775Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.602 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [vT+Vd] d:07ad6d06cd8b5ff2 now using 172.20.0.2:41471\n"}
+{"Time":"2023-03-29T13:37:23.603134941Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.602 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:23.602980142 +0000 UTC m=+0.242587360 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:23.602828352 +0000 UTC NodeKey:nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229}] LocalAddrs:[{Addr:127.0.0.1:41471 Type:stun} {Addr:172.20.0.2:41471 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:23.603731388Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.603 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [0x7ra] d:8ac4cc2c7460d56f now using 127.0.0.1:34768\n"}
+{"Time":"2023-03-29T13:37:23.629049874Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" agent_test.go:298: 2023-03-29 13:37:23.628: cmd: stdin: \"exit 0\\r\"\n"}
+{"Time":"2023-03-29T13:37:23.62993175Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:121: 2023-03-29 13:37:23.629: cmd: \"exit 0\"\n"}
+{"Time":"2023-03-29T13:37:23.643243989Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.643 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:34768 derp=1 derpdist=1v4:1ms\n"}
+{"Time":"2023-03-29T13:37:23.643873931Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.643 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:41471 derp=1 derpdist=1v4:0s\n"}
+{"Time":"2023-03-29T13:37:23.644469186Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.644 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000547591}}}\n"}
+{"Time":"2023-03-29T13:37:23.644715274Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.644 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.644461Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000547591}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.645390624Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.645 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 5764434400100518555, \"as_of\": \"2023-03-29T13:37:23.644461Z\", \"key\": \"nodekey:d31eeb68b6968cc6779e62454901fb98bcacab1dcb46e15bec2b92205cc82229\", \"disco\": \"discokey:8ac4cc2c7460d56ffcd8064d64a7752b43475c7244a316f626b321097e07630f\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000547591}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34768\", \"172.20.0.2:34768\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.64591875Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.645 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.646145075Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.645 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:23.646400847Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:23.646730863Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:23.646828936Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Sending keepalive packet\n"}
+{"Time":"2023-03-29T13:37:23.646930238Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:23.647158211Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.646 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000154898}}}\n"}
+{"Time":"2023-03-29T13:37:23.647376125Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.647 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.64715Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000154898}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.648003118Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.647 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3370308278017414080, \"as_of\": \"2023-03-29T13:37:23.64715Z\", \"key\": \"nodekey:bd3f9574e34fe33bab67dc45e49054f84d69e7c686af37bb2556989a8e6e9b33\", \"disco\": \"discokey:07ad6d06cd8b5ff2fd2b25fcd8f253332fd46d9820c6e0a670d9302db2d21411\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000154898}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775/128\"], \"endpoints\": [\"127.0.0.1:41471\", \"172.20.0.2:41471\"]}}\n"}
+{"Time":"2023-03-29T13:37:23.648256172Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:23.648338509Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:23.648471344Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:23.648609559Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:23.648638814Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Sending keepalive packet\n"}
+{"Time":"2023-03-29T13:37:23.648721589Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:23.648895668Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Receiving keepalive packet\n"}
+{"Time":"2023-03-29T13:37:23.648944175Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Receiving keepalive packet\n"}
+{"Time":"2023-03-29T13:37:23.983374775Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:23.983 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775): sending disco ping to [vT+Vd] ...\n"}
+{"Time":"2023-03-29T13:37:24.483975661Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.483 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775): sending disco ping to [vT+Vd] ...\n"}
+{"Time":"2023-03-29T13:37:24.983883086Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.983 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775): sending disco ping to [vT+Vd] ...\n"}
+{"Time":"2023-03-29T13:37:24.997284571Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:83: 2023-03-29 13:37:24.997: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:24.997307935Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:37:24.997: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:24.997315409Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:110: 2023-03-29 13:37:24.997: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:24.997319364Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:111: 2023-03-29 13:37:24.997: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:24.997323588Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:113: 2023-03-29 13:37:24.997: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:24.997370156Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:37:24.997: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:24.997381692Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:37:24.997: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:24.997385034Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:37:24.997: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:24.997389205Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:74: 2023-03-29 13:37:24.997: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:24.997393753Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:76: 2023-03-29 13:37:24.997: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:24.997405892Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" ptytest.go:102: 2023-03-29 13:37:24.997: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:24.997490606Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
+{"Time":"2023-03-29T13:37:24.997723999Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 1s\n"}
+{"Time":"2023-03-29T13:37:24.997783062Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:24.997839084Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:24.997864344Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:24.997932377Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:24.998046302Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.997 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:24.998086112Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:24.998136192Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0x7ra] - Stopping\n"}
+{"Time":"2023-03-29T13:37:24.998214902Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:24.998405401Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:24.998453108Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
+{"Time":"2023-03-29T13:37:24.998545966Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
+{"Time":"2023-03-29T13:37:24.998863807Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 1s\n"}
+{"Time":"2023-03-29T13:37:24.998907983Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:24.998974565Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:24.999012856Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.998 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:24.999084066Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:24.999163662Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4aeb:931f:72d2:b3f9:2775): sending disco ping to [vT+Vd] ...\n"}
+{"Time":"2023-03-29T13:37:24.999281214Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:24.999322797Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:24.999367964Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [vT+Vd] - Stopping\n"}
+{"Time":"2023-03-29T13:37:24.999477141Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" t.go:81: 2023-03-29 13:37:24.999 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:24.999790842Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":" stuntest.go:63: STUN server shutdown\n"}
+{"Time":"2023-03-29T13:37:24.999978482Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Output":"--- PASS: TestAgent_Session_TTY_MOTD (1.62s)\n"}
+{"Time":"2023-03-29T13:37:24.999989636Z","Action":"pass","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_MOTD","Elapsed":1.62}
+{"Time":"2023-03-29T13:37:25.000001861Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin"}
+{"Time":"2023-03-29T13:37:25.000006766Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"=== RUN TestAgent_Session_TTY_Hushlogin\n"}
+{"Time":"2023-03-29T13:37:25.061523057Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:25.061562172Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:25.061580317Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:25.061650184Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:25.061692748Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:25.061779714Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:25.061825894Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:25.0619026Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:25.061948469Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:25.061997351Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:25.062034009Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.061 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:25.062159882Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:de63960686d4d969\n"}
+{"Time":"2023-03-29T13:37:25.062211517Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:25.062292207Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:25.062336748Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:25.062378334Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:25.062420493Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:25.06246598Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:25.062509064Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:25.0625467Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:25.062582098Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:25.062621581Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:25.062741251Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.062 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:25.063047606Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.063098481Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:25.063228621Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:25.063275399Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
+{"Time":"2023-03-29T13:37:25.063394952Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 48719, \"DERPPort\": 45121, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"/tmp/TestAgent_Session_TTY_Hushlogin1510664063/001/motd\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
+{"Time":"2023-03-29T13:37:25.063449256Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
+{"Time":"2023-03-29T13:37:25.063787886Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:25.063828837Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:25.063873191Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:25.063920593Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:25.063972721Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.063 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:25.064056998Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:25.064104109Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:25.064172955Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:25.064220262Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:25.064265054Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:25.064311842Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:25.064424792Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:a2fa54a4398f4b14\n"}
+{"Time":"2023-03-29T13:37:25.064460776Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:25.064519398Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:25.064564764Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:25.064610015Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:25.064649665Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:25.064697586Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:25.064733375Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:25.06476809Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:25.064817752Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:25.064858351Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:25.064968632Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.064 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:25.065284782Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.065331529Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:25.065409197Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
+{"Time":"2023-03-29T13:37:25.065446163Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
+{"Time":"2023-03-29T13:37:25.065532991Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.065442Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:25.065984331Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.065 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:25.066385592Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.066 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:25.066774463Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.066 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:25.067577596Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.063134Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:25.067723294Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:25.067791082Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:25.067853127Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:25.067910376Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.067 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:25.0683366Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.068 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:25.07002731Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.069 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:25.071537156Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.071 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:25.073373256Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.065442Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:25.073396962Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.073480235Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\n"}
+{"Time":"2023-03-29T13:37:25.073564836Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:25.073671864Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:25.07378875Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:25.073820206Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:25.073854357Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:25.073896565Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:25.073931707Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:25.073965714Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:25.074005901Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.073 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:25.074880124Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.074 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.063134Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:25.074899766Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.074 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.074982929Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.074 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
+{"Time":"2023-03-29T13:37:25.075048527Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:25.075142259Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:25.075263679Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:25.075295728Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:25.075336116Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:25.075375372Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:25.075421587Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:25.075466864Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:25.075504912Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.075 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:25.117000024Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.116 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:25.124882906Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.124 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:60850 derp=1 derpdist=1v4:1ms\n"}
+{"Time":"2023-03-29T13:37:25.125053667Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.124 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:25.12561266Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.125 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:60850 (stun), 172.20.0.2:60850 (local)\n"}
+{"Time":"2023-03-29T13:37:25.125905661Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.125 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.125569748 +0000 UTC m=+1.765177074 Peers:[] LocalAddrs:[{Addr:127.0.0.1:60850 Type:stun} {Addr:172.20.0.2:60850 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:25.126844264Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.126 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:58304 derp=1 derpdist=1v4:1ms\n"}
+{"Time":"2023-03-29T13:37:25.127005238Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.126 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:25.127513222Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.127 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:58304 (stun), 172.20.0.2:58304 (local)\n"}
+{"Time":"2023-03-29T13:37:25.127757541Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.127 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.127481783 +0000 UTC m=+1.767089103 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58304 Type:stun} {Addr:172.20.0.2:58304 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:25.128599916Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.128 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:25.128762101Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.128 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:25.129009957Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.128 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.12872419 +0000 UTC m=+1.768331474 Peers:[] LocalAddrs:[{Addr:127.0.0.1:60850 Type:stun} {Addr:172.20.0.2:60850 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:25.129209925Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.128 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000985407}}}\n"}
+{"Time":"2023-03-29T13:37:25.129460508Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.129 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.125849Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.130127324Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.129 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.125849Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.130232734Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.130 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.13055158Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.130 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:25.131114968Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.130 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:25.131240194Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.131 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:25.131519421Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.131 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.131235659 +0000 UTC m=+1.770842948 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58304 Type:stun} {Addr:172.20.0.2:58304 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:25.131694593Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.131 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.001250656}}}\n"}
+{"Time":"2023-03-29T13:37:25.131999693Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.131 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.127736Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.132733057Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.132 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:25.133220162Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.132 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.132982Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000985407}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.133869838Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.133 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.132982Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000985407}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.134272814Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.134404044Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:25.134672632Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.134517Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001250656}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.134983325Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.127736Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.135024699Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.134 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.135198837Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:25.135284701Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:25.135438825Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:25.135566373Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:25.135876201Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.135 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.134517Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.001250656}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.136046662Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.136 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.136091872Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.136 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:25.136196604Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.136 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:25.136258441Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.136 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:25.143831824Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.143 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:25.143963014Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.143 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:25.181560631Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.181 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:60850 derp=1 derpdist=1v4:10ms\n"}
+{"Time":"2023-03-29T13:37:25.182840653Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.182 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:58304 derp=1 derpdist=1v4:8ms\n"}
+{"Time":"2023-03-29T13:37:25.197904655Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.197 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0LjuK] ...\n"}
+{"Time":"2023-03-29T13:37:25.198311943Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.198 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [0LjuK] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:25.199139623Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.199 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [S1KIY] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:25.199333828Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.199 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [0LjuK] d:a2fa54a4398f4b14 now using 172.20.0.2:60850\n"}
+{"Time":"2023-03-29T13:37:25.199614502Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.199 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0LjuK] ...\n"}
+{"Time":"2023-03-29T13:37:25.199798383Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.199 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [0LjuK] ...\n"}
+{"Time":"2023-03-29T13:37:25.200531317Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.200 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:25.20096595Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.200 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:25.201076837Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.200 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:25.201187536Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:25.201294507Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:25.201406637Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:25.20154342Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Starting\n"}
+{"Time":"2023-03-29T13:37:25.201642014Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.201 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Sending handshake initiation\n"}
+{"Time":"2023-03-29T13:37:25.202494139Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.202 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [S1KIY] now active, reconfiguring WireGuard\n"}
+{"Time":"2023-03-29T13:37:25.202613281Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.202 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:25.203067239Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.202 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:25.203194112Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:25.203305469Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:25.203432836Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:25.203643635Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:25.203694677Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Starting\n"}
+{"Time":"2023-03-29T13:37:25.204047455Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.203 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Received handshake initiation\n"}
+{"Time":"2023-03-29T13:37:25.20407267Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.204 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Sending handshake response\n"}
+{"Time":"2023-03-29T13:37:25.204499058Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.204 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.204364109 +0000 UTC m=+1.843971335 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262}] LocalAddrs:[{Addr:127.0.0.1:60850 Type:stun} {Addr:172.20.0.2:60850 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:25.204985233Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.204 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Received handshake response\n"}
+{"Time":"2023-03-29T13:37:25.205043968Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.204 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [S1KIY] d:de63960686d4d969 now using 172.20.0.2:58304\n"}
+{"Time":"2023-03-29T13:37:25.205186973Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.205 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:25.205077383 +0000 UTC m=+1.844684599 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:25.204972346 +0000 UTC NodeKey:nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c}] LocalAddrs:[{Addr:127.0.0.1:58304 Type:stun} {Addr:172.20.0.2:58304 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:25.205653441Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.205 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [0LjuK] d:a2fa54a4398f4b14 now using 127.0.0.1:60850\n"}
+{"Time":"2023-03-29T13:37:25.228059809Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" agent_test.go:344: 2023-03-29 13:37:25.227: cmd: stdin: \"exit 0\\r\"\n"}
+{"Time":"2023-03-29T13:37:25.228276231Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:121: 2023-03-29 13:37:25.228: cmd: \"exit 0\"\n"}
+{"Time":"2023-03-29T13:37:25.235085539Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:58304 derp=1 derpdist=1v4:0s\n"}
+{"Time":"2023-03-29T13:37:25.235327678Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:60850 derp=1 derpdist=1v4:0s\n"}
+{"Time":"2023-03-29T13:37:25.235573892Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000235695}}}\n"}
+{"Time":"2023-03-29T13:37:25.235627639Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.235549Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000235695}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.235912714Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.235 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2479211229855255143, \"as_of\": \"2023-03-29T13:37:25.235549Z\", \"key\": \"nodekey:4b52886038d0b10509d4ba999d1a1ad8721795e419e104079b9b0d4334ccf262\", \"disco\": \"discokey:de63960686d4d9696035b3395ad51c334b1b6762f27929160b70e330e59fc955\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000235695}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7/128\"], \"endpoints\": [\"127.0.0.1:58304\", \"172.20.0.2:58304\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.2361447Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.236185457Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:25.23632231Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:25.2364646Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:25.23648571Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Sending keepalive packet\n"}
+{"Time":"2023-03-29T13:37:25.236535173Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:25.236646389Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.000124794}}}\n"}
+{"Time":"2023-03-29T13:37:25.236691145Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.236616Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000124794}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.236953136Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.236 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2366729466453183316, \"as_of\": \"2023-03-29T13:37:25.236616Z\", \"key\": \"nodekey:d0b8ee28a0ee87a0b3a9c4e4d551d52d6abae50aac6b29587f8aaaecaf92dc1c\", \"disco\": \"discokey:a2fa54a4398f4b14ed94f97af08fb0c8f1a7276aa663caf7d25542e565ef4f28\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.000124794}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:60850\", \"172.20.0.2:60850\"]}}\n"}
+{"Time":"2023-03-29T13:37:25.237148814Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:25.23717419Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:25.23729907Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:25.237442993Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:25.237465362Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Sending keepalive packet\n"}
+{"Time":"2023-03-29T13:37:25.23748091Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:25.237621079Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Receiving keepalive packet\n"}
+{"Time":"2023-03-29T13:37:25.237646187Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.237 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Receiving keepalive packet\n"}
+{"Time":"2023-03-29T13:37:25.57358248Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:25.573 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7): sending disco ping to [S1KIY] ...\n"}
+{"Time":"2023-03-29T13:37:26.073909695Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.073 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7): sending disco ping to [S1KIY] ...\n"}
+{"Time":"2023-03-29T13:37:26.573442313Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.573 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7): sending disco ping to [S1KIY] ...\n"}
+{"Time":"2023-03-29T13:37:26.685974077Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:83: 2023-03-29 13:37:26.685: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:26.686024343Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:74: 2023-03-29 13:37:26.685: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:26.686044434Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:110: 2023-03-29 13:37:26.685: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:26.686057034Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:111: 2023-03-29 13:37:26.685: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:26.686068789Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:113: 2023-03-29 13:37:26.685: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:26.686157867Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:76: 2023-03-29 13:37:26.686: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:26.686173026Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:74: 2023-03-29 13:37:26.686: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:26.686188025Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:76: 2023-03-29 13:37:26.686: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:26.686198978Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:74: 2023-03-29 13:37:26.686: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:26.686213128Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:76: 2023-03-29 13:37:26.686: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:26.686224275Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" ptytest.go:102: 2023-03-29 13:37:26.686: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:26.686399517Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
+{"Time":"2023-03-29T13:37:26.686631954Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 2s\n"}
+{"Time":"2023-03-29T13:37:26.686672029Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:26.686752993Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:26.686793215Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:26.686883059Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:26.687010573Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:26.687040046Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.686 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:26.687106628Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [0LjuK] - Stopping\n"}
+{"Time":"2023-03-29T13:37:26.687190751Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:26.68730229Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:26.687319022Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
+{"Time":"2023-03-29T13:37:26.68739086Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
+{"Time":"2023-03-29T13:37:26.687791287Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 2s\n"}
+{"Time":"2023-03-29T13:37:26.687807566Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:26.687907277Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:26.687956258Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:26.68802861Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.687 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:26.688112368Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4c72:9ce3:62e1:7385:dec7): sending disco ping to [S1KIY] ...\n"}
+{"Time":"2023-03-29T13:37:26.688253692Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:26.688318345Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:26.688366659Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [S1KIY] - Stopping\n"}
+{"Time":"2023-03-29T13:37:26.688473063Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" t.go:81: 2023-03-29 13:37:26.688 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:26.688794731Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":" stuntest.go:63: STUN server shutdown\n"}
+{"Time":"2023-03-29T13:37:26.688993708Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Output":"--- PASS: TestAgent_Session_TTY_Hushlogin (1.69s)\n"}
+{"Time":"2023-03-29T13:37:26.689005169Z","Action":"pass","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_Hushlogin","Elapsed":1.69}
+{"Time":"2023-03-29T13:37:26.689017486Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput"}
+{"Time":"2023-03-29T13:37:26.689022846Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":"=== RUN TestAgent_Session_TTY_FastCommandHasOutput\n"}
+{"Time":"2023-03-29T13:37:26.689031231Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":"=== PAUSE TestAgent_Session_TTY_FastCommandHasOutput\n"}
+{"Time":"2023-03-29T13:37:26.689049237Z","Action":"pause","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput"}
+{"Time":"2023-03-29T13:37:26.689055783Z","Action":"run","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost"}
+{"Time":"2023-03-29T13:37:26.689060673Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":"=== RUN TestAgent_Session_TTY_HugeOutputIsNotLost\n"}
+{"Time":"2023-03-29T13:37:26.689069084Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":"=== PAUSE TestAgent_Session_TTY_HugeOutputIsNotLost\n"}
+{"Time":"2023-03-29T13:37:26.689074026Z","Action":"pause","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost"}
+{"Time":"2023-03-29T13:37:26.689083532Z","Action":"cont","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec"}
+{"Time":"2023-03-29T13:37:26.689090279Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":"=== CONT TestAgent_SessionExec\n"}
+{"Time":"2023-03-29T13:37:26.703787142Z","Action":"cont","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost"}
+{"Time":"2023-03-29T13:37:26.703825656Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":"=== CONT TestAgent_Session_TTY_HugeOutputIsNotLost\n"}
+{"Time":"2023-03-29T13:37:26.703839698Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":" agent_test.go:413: This test proves we have a bug where parts of large output on a PTY can be lost after the command exits, skipped to avoid test failures.\n"}
+{"Time":"2023-03-29T13:37:26.703868206Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Output":"--- SKIP: TestAgent_Session_TTY_HugeOutputIsNotLost (0.00s)\n"}
+{"Time":"2023-03-29T13:37:26.70388799Z","Action":"skip","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_HugeOutputIsNotLost","Elapsed":0}
+{"Time":"2023-03-29T13:37:26.703900175Z","Action":"cont","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput"}
+{"Time":"2023-03-29T13:37:26.703908033Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":"=== CONT TestAgent_Session_TTY_FastCommandHasOutput\n"}
+{"Time":"2023-03-29T13:37:26.723935151Z","Action":"cont","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode"}
+{"Time":"2023-03-29T13:37:26.723957967Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":"=== CONT TestAgent_SessionTTYExitCode\n"}
+{"Time":"2023-03-29T13:37:26.744050676Z","Action":"cont","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell"}
+{"Time":"2023-03-29T13:37:26.744073511Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":"=== CONT TestAgent_SessionTTYShell\n"}
+{"Time":"2023-03-29T13:37:26.975908391Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:26.975935102Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:26.975941463Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:26.975992643Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:26.976027672Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.975 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:26.976097148Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:26.976137127Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:26.976202552Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:26.976237559Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:26.976273529Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:26.976300958Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:26.976417909Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:17b5066de479f458\n"}
+{"Time":"2023-03-29T13:37:26.976445133Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:26.976529694Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:26.976575591Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:26.976603915Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:26.976641835Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:26.97666441Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:26.976690087Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:26.97672446Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:26.976749123Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:26.976786013Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:26.976893189Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.976 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:26.977219957Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.977 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:26.977264288Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.977 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:26.977393997Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:26.977 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.015978257Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.015 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
+{"Time":"2023-03-29T13:37:27.016072388Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.015 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 55109, \"DERPPort\": 34655, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
+{"Time":"2023-03-29T13:37:27.016101997Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
+{"Time":"2023-03-29T13:37:27.016454205Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:27.016484809Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:27.016527444Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:27.016579577Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:27.016616504Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:27.01670613Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.016736272Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.016795884Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.01683051Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.016869184Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.016901207Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.017009443Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.016 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:e6f05f1260bbd611\n"}
+{"Time":"2023-03-29T13:37:27.017032499Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:27.017087268Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:27.017133219Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:27.017166541Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:27.017198439Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:27.017231784Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.017255027Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:27.017285867Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.017313159Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:27.017341323Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:27.017453136Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:27.017777899Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.01781684Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:27.017877499Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
+{"Time":"2023-03-29T13:37:27.017903073Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
+{"Time":"2023-03-29T13:37:27.017983662Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.017 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.017904Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.018477038Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.018 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:27.018878036Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.018 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:27.019251435Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.019 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:27.020053918Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.019 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:26.977307Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.020198673Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.020 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.020242577Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.020 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:27.020285833Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.020 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:27.020317905Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.020 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:27.036521314Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.036 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:27.036924425Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.036 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:27.037332816Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.037 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:27.03841778Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.017904Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.038434547Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.03852175Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\n"}
+{"Time":"2023-03-29T13:37:27.038608926Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:27.038696362Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:27.038810178Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:27.038824686Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.038858134Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:27.038891063Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:27.038924242Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:27.038937309Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:27.038976455Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.038 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.039988711Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.039 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:26.977307Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.040010425Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.039 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.040059771Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
+{"Time":"2023-03-29T13:37:27.040126554Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:27.040195696Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:27.040300593Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:27.040315089Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.040342986Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:27.040371935Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:27.040410694Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:27.040423938Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:27.040453889Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.040 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.096588631Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.096 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
+{"Time":"2023-03-29T13:37:27.096797479Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.096 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.097032423Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.096 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.09709667Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.097 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
+{"Time":"2023-03-29T13:37:27.12109969Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:27.121135169Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:27.121150972Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:27.121167311Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:27.121216222Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:27.121292057Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.12132273Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.121381219Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.121408684Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.121435744Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.121461695Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.121594822Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:c7f1bea9d6ff269c\n"}
+{"Time":"2023-03-29T13:37:27.121620573Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:27.121655676Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:27.121730239Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:27.121756777Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:27.121781901Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:27.121808996Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.121833529Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:27.121858413Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.121888455Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:27.121913023Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:27.122044906Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.121 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:27.12234719Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.122 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.122375547Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.122 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:27.122495597Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.122 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.136791704Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.136 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.136826981Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.136 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
+{"Time":"2023-03-29T13:37:27.13694108Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.136 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 48864, \"DERPPort\": 33963, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
+{"Time":"2023-03-29T13:37:27.136979053Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.136 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
+{"Time":"2023-03-29T13:37:27.137291589Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:27.137308609Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:27.13732063Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:27.137373985Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:27.137407965Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:27.137495219Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.137519229Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.137572028Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.137602411Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.137622846Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.137667976Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.137770053Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:59083cba13956f00\n"}
+{"Time":"2023-03-29T13:37:27.137794544Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:27.137898354Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:27.137934758Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:27.137955272Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:27.138002548Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:27.138024513Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.137 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.138043252Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:27.138068121Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.138095784Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:27.138122918Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:27.138231919Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:27.138531438Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.138556077Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:27.138636819Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
+{"Time":"2023-03-29T13:37:27.138658918Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
+{"Time":"2023-03-29T13:37:27.138754966Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.138 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.138663Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.139212045Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.139 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:27.139609965Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.139 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:27.141001091Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.140 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:27.143964866Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.143 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.122393Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.144498996Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.144 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.144562824Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.144 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:27.144649797Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.144 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:27.144702964Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.144 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:27.147050678Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.146 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:27.152240491Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.152 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:27.157654353Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.157 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:27.16338877Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.16375523Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.138663Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.163780585Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.163864424Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\n"}
+{"Time":"2023-03-29T13:37:27.163963441Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.163 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:27.164079841Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:27.164217744Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:27.164235438Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.16424974Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:27.164316222Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:27.164338403Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:27.164386486Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:27.1644261Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.164 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.165498628Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.122393Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.165527893Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.16555513Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
+{"Time":"2023-03-29T13:37:27.165643091Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:27.165737176Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:27.165892091Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:27.165909805Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.165924292Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:27.165977872Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:27.166008015Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.165 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:27.166054821Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.166 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:27.166085097Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.166 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.187154465Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.187 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.247520351Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.247 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.247693682Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.247 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
+{"Time":"2023-03-29T13:37:27.247900828Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.247 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
+{"Time":"2023-03-29T13:37:27.248074095Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.248 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.248118748Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.248 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.267591238Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.267 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.327992838Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.327 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
+{"Time":"2023-03-29T13:37:27.328026983Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.327 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:59384 derp=1 derpdist=1v4:83ms\n"}
+{"Time":"2023-03-29T13:37:27.328082537Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.328 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.328318974Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.328 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:59384 (stun), 172.20.0.2:59384 (local)\n"}
+{"Time":"2023-03-29T13:37:27.328415723Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.328 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.328311263 +0000 UTC m=+3.967918487 Peers:[] LocalAddrs:[{Addr:127.0.0.1:59384 Type:stun} {Addr:172.20.0.2:59384 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.331309331Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:27.331346835Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:27.331498355Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.331349704 +0000 UTC m=+3.970956931 Peers:[] LocalAddrs:[{Addr:127.0.0.1:59384 Type:stun} {Addr:172.20.0.2:59384 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.331576885Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.083211738}}}\n"}
+{"Time":"2023-03-29T13:37:27.331699503Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.328409Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.331945606Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.328409Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.33196728Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.331 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.332108335Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.332325739Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.332542999Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.332444Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.083211738}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.332790784Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.332444Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.083211738}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.333024686Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.332 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.333062782Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.333 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.333182232Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.333 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.333249208Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.333 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.348154607Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.348 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.353941907Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:27.353956013Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:27.353983717Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.353 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:27.354104135Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:27.354170721Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:27.354248787Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.354272858Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.35432815Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.354379756Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.354437701Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.354522443Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.35465091Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:049e454260a62aa1\n"}
+{"Time":"2023-03-29T13:37:27.354686785Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:27.354820076Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:27.35484141Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:27.354884035Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:27.354900747Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:27.354943262Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.354962224Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:27.354997734Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.355013636Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.354 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:27.355062521Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:27.355172916Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:27.355579603Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
+{"Time":"2023-03-29T13:37:27.355609674Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:45837 derp=1 derpdist=1v4:83ms\n"}
+{"Time":"2023-03-29T13:37:27.35566873Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.355827579Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:45837 (stun), 172.20.0.2:45837 (local)\n"}
+{"Time":"2023-03-29T13:37:27.355913155Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.355 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.355809216 +0000 UTC m=+3.995416428 Peers:[] LocalAddrs:[{Addr:127.0.0.1:45837 Type:stun} {Addr:172.20.0.2:45837 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.356261459Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
+{"Time":"2023-03-29T13:37:27.356354227Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 34688, \"DERPPort\": 43117, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
+{"Time":"2023-03-29T13:37:27.35638277Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
+{"Time":"2023-03-29T13:37:27.356686207Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:27.356708299Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:27.356723833Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:27.356833939Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:27.356850125Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:27.356945712Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.356968923Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.357036251Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.356 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.357070033Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.35711526Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.357134582Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.357249522Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:34ff526bdd502e84\n"}
+{"Time":"2023-03-29T13:37:27.35727419Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:27.35742285Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:27.357459674Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:27.357483314Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:27.357523236Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:27.357548281Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.357567638Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:27.357589503Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.357626515Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:27.357652913Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:27.357769486Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:27.358036076Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.357 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.358075196Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:27.358178009Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
+{"Time":"2023-03-29T13:37:27.358202627Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.358 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
+{"Time":"2023-03-29T13:37:27.358298603Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.358 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.358191Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.363703025Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.363 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:27.368878553Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.368 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:27.374094674Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.373 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:27.379724421Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.379782057Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:27.379915271Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.37996992Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:27.380020183Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.379 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:27.380128212Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.380012238 +0000 UTC m=+4.019619457 Peers:[] LocalAddrs:[{Addr:127.0.0.1:45837 Type:stun} {Addr:172.20.0.2:45837 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.380193848Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.082795986}}}\n"}
+{"Time":"2023-03-29T13:37:27.380276172Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.355895Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.38052742Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.355895Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.380550207Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.380665436Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.380879187Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:27.380925379Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:27.380952567Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.380 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:27.386338613Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.386 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:27.387267029Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.387 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:27.387774697Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.387 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:27.389160607Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.358191Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.389186972Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.389275389Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\n"}
+{"Time":"2023-03-29T13:37:27.389397112Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:27.38946191Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:27.389582352Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:27.389605243Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.389634845Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:27.389680241Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:27.389711656Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:27.389725148Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:27.389767299Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.389833677Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.379813Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.390055453Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.389 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.379813Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.390076169Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.390126429Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
+{"Time":"2023-03-29T13:37:27.390188645Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:27.39025637Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:27.390371704Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:27.39038785Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.390406815Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:27.390435981Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:27.390450102Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:27.39048039Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:27.390509523Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.390585223Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.390644997Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.39085229Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.390753Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.082795986}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.391046642Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.390 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.390753Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.082795986}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.391253711Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.391 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.391294825Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.391 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.391387255Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.391 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.391491088Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.391 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.408305611Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:27.408340875Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:27.408352182Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:27.408381704Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:27.41114267Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.41133032Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
+{"Time":"2023-03-29T13:37:27.411379291Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:35595 derp=1 derpdist=1v4:84ms\n"}
+{"Time":"2023-03-29T13:37:27.411463359Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.411622433Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:35595 (stun), 172.20.0.2:35595 (local)\n"}
+{"Time":"2023-03-29T13:37:27.411704928Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.411 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.411617207 +0000 UTC m=+4.051224425 Peers:[] LocalAddrs:[{Addr:127.0.0.1:35595 Type:stun} {Addr:172.20.0.2:35595 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.412030111Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.411 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:188\u003e\t(*agent).runLoop\tconnecting to coderd\n"}
+{"Time":"2023-03-29T13:37:27.412110894Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:286\u003e\t(*agent).run\tfetched metadata\t{\"metadata\": {\"git_auth_configs\": 0, \"vscode_port_proxy_uri\": \"\", \"apps\": null, \"derpmap\": {\"Regions\": {\"1\": {\"EmbeddedRelay\": false, \"RegionID\": 1, \"RegionCode\": \"test\", \"RegionName\": \"Test\", \"Nodes\": [{\"Name\": \"t2\", \"RegionID\": 1, \"HostName\": \"\", \"IPv4\": \"127.0.0.1\", \"IPv6\": \"none\", \"STUNPort\": 51906, \"DERPPort\": 41275, \"InsecureForTests\": true}]}}}, \"environment_variables\": null, \"startup_script\": \"\", \"startup_script_timeout\": 0, \"directory\": \"\", \"motd_file\": \"\", \"shutdown_script\": \"\", \"shutdown_script_timeout\": 0}}\n"}
+{"Time":"2023-03-29T13:37:27.41214998Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"starting\", \"last\": \"\"}\n"}
+{"Time":"2023-03-29T13:37:27.412464057Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:270\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) tun device\n"}
+{"Time":"2023-03-29T13:37:27.412485841Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:274\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) OS network configurator\n"}
+{"Time":"2023-03-29T13:37:27.412515514Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:278\u003e\tNewUserspaceEngine\t[v1] using fake (no-op) DNS configurator\n"}
+{"Time":"2023-03-29T13:37:27.412564627Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: using dns.noopManager\n"}
+{"Time":"2023-03-29T13:37:27.412620272Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:27.412695891Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.412734645Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.412796459Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.41282973Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.412882896Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.412921148Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.413034159Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.412 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:cc502d2065d3910d\n"}
+{"Time":"2023-03-29T13:37:27.413066135Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:27.413142789Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:27.413196003Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:27.413234614Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:27.413259313Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:27.413291621Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.413317957Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:27.413346325Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.413369117Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:27.413402783Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:27.413518268Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:27.413809664Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.413845445Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:27.413910962Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:402\u003e\t(*agent).run\trunning tailnet connection coordinator\n"}
+{"Time":"2023-03-29T13:37:27.413937365Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:620\u003e\t(*agent).runCoordinator\tconnected to coordination endpoint\n"}
+{"Time":"2023-03-29T13:37:27.414017708Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.413 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.413933Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.414064655Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:27.414111814Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:27.414189184Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.414107708 +0000 UTC m=+4.053714921 Peers:[] LocalAddrs:[{Addr:127.0.0.1:35595 Type:stun} {Addr:172.20.0.2:35595 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.414245907Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.083765437}}}\n"}
+{"Time":"2023-03-29T13:37:27.414312155Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.4117Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.414517803Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.4117Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.414535126Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.414623995Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.414849045Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:27.414905554Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"ready\", \"last\": \"starting\"}\n"}
+{"Time":"2023-03-29T13:37:27.414936416Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.414 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:27.415525692Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.415 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:27.416019712Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.415 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:27.416475957Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.416 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:27.419029132Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.418 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.419447399Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.419 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.419321Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.083765437}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.42011877Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.419321Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.083765437}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.420376019Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.420736563Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.42087992Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.420979406Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.420 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.422951198Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.422 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
+{"Time":"2023-03-29T13:37:27.42298967Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.422 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:51685 derp=1 derpdist=1v4:84ms\n"}
+{"Time":"2023-03-29T13:37:27.423073917Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.423 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.423298046Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.423 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:51685 (stun), 172.20.0.2:51685 (local)\n"}
+{"Time":"2023-03-29T13:37:27.423385945Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.423 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.423272543 +0000 UTC m=+4.062879776 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51685 Type:stun} {Addr:172.20.0.2:51685 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.424393184Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:27.424445676Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:27.424603665Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.424442129 +0000 UTC m=+4.064049348 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51685 Type:stun} {Addr:172.20.0.2:51685 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.424655158Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.084146852}}}\n"}
+{"Time":"2023-03-29T13:37:27.424762683Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.423371Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.425045855Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.424 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.423371Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.425065382Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.425204191Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.425429938Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.425622509Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.425514Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.084146852}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.425845165Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.425514Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.084146852}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.426046304Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.425 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.426095567Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.426 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.426917109Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.426 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.426975294Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.426 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.427132044Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:59384 derp=1 derpdist=1v4:95ms\n"}
+{"Time":"2023-03-29T13:37:27.427810726Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.408 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:334\u003e\tNewUserspaceEngine\tlink state: interfaces.State{defaultRoute=eth0 ifs={eth0:[172.20.0.2/16]} v4=true v6=false}\n"}
+{"Time":"2023-03-29T13:37:27.427890723Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.427915145Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.42800586Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:306\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP read buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.428030074Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.427 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock_linux.go:310\u003e\ttrySetSocketBuffer.func1\tmagicsock: failed to force-set UDP write buffer size to 7340032: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.428084359Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:666\u003e\tNewConn\t[v1] couldn't create raw v4 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.428115313Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:672\u003e\tNewConn\t[v1] couldn't create raw v6 disco listener, using regular listener instead: raw disco listening disabled, SO_MARK unavailable\n"}
+{"Time":"2023-03-29T13:37:27.428260646Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1056\u003e\t(*Conn).DiscoPublicKey\tmagicsock: disco key = d:9c9ea8075f682592\n"}
+{"Time":"2023-03-29T13:37:27.428283953Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:412\u003e\tNewUserspaceEngine\tCreating WireGuard device...\n"}
+{"Time":"2023-03-29T13:37:27.428422249Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:437\u003e\tNewUserspaceEngine\tBringing WireGuard device up...\n"}
+{"Time":"2023-03-29T13:37:27.428458364Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] UDP bind has been updated\n"}
+{"Time":"2023-03-29T13:37:27.428479111Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Interface state was Down, requested Up, now Up\n"}
+{"Time":"2023-03-29T13:37:27.428530647Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:441\u003e\tNewUserspaceEngine\tBringing router up...\n"}
+{"Time":"2023-03-29T13:37:27.428549605Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:21\u003e\tfakeRouter.Up\t[v1] warning: fakeRouter.Up: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.428596203Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:449\u003e\tNewUserspaceEngine\tClearing router settings...\n"}
+{"Time":"2023-03-29T13:37:27.428629468Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.42866001Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:453\u003e\tNewUserspaceEngine\tStarting link monitor...\n"}
+{"Time":"2023-03-29T13:37:27.428692965Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:456\u003e\tNewUserspaceEngine\tEngine created.\n"}
+{"Time":"2023-03-29T13:37:27.42881653Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.428 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2444\u003e\t(*Conn).SetPrivateKey\tmagicsock: SetPrivateKey called (init)\n"}
+{"Time":"2023-03-29T13:37:27.430284733Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.094731332}}}\n"}
+{"Time":"2023-03-29T13:37:27.430366553Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.430277Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.094731332}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.430637096Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6193178684101620604, \"as_of\": \"2023-03-29T13:37:27.430277Z\", \"key\": \"nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370\", \"disco\": \"discokey:17b5066de479f45868013352cba173846e33492e64258b47a5e823c1746f8449\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.094731332}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4781:bb82:1540:3954:6a8/128\"], \"endpoints\": [\"127.0.0.1:59384\", \"172.20.0.2:59384\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.430871537Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.430971821Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.430 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:45837 derp=1 derpdist=1v4:37ms\n"}
+{"Time":"2023-03-29T13:37:27.436731495Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.436 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - started\n"}
+{"Time":"2023-03-29T13:37:27.441948864Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.441 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
+{"Time":"2023-03-29T13:37:27.442143804Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.442 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - started\n"}
+{"Time":"2023-03-29T13:37:27.447325985Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.447 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:58\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - started\n"}
+{"Time":"2023-03-29T13:37:27.452941046Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.452 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:187\u003e\tNewConn\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.452978923Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.452 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 0 peers\n"}
+{"Time":"2023-03-29T13:37:27.453138084Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.453213493Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.453354362Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.45343882Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.037239154}}}\n"}
+{"Time":"2023-03-29T13:37:27.453524949Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.453418Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.037239154}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.453796409Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 1396496777246732951, \"as_of\": \"2023-03-29T13:37:27.453418Z\", \"key\": \"nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24\", \"disco\": \"discokey:e6f05f1260bbd61182192a11c1541a28ccace412e36cdb487e15a598d8327a73\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.037239154}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:45837\", \"172.20.0.2:45837\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.454022251Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.453 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.454053656Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.454164006Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.454402409Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.454452969Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: netcheck: UDP is blocked, trying HTTPS\n"}
+{"Time":"2023-03-29T13:37:27.454661334Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.454 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.455180586Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.413933Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.45520381Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.455291748Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\n"}
+{"Time":"2023-03-29T13:37:27.455371391Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:27.45558374Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:27.455722626Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:27.455744892Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.455759263Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:27.455811196Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:27.455851745Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:27.455887337Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:27.45593531Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.456026705Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.455 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.453Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.456257727Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.453Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": []}}\n"}
+{"Time":"2023-03-29T13:37:27.456275526Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.456348245Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.netstack)\t\u003ctailscale.com/wgengine/netstack/netstack.go:367\u003e\t(*Impl).updateIPs\t[v2] netstack: registered IP fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\n"}
+{"Time":"2023-03-29T13:37:27.456430865Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/0 peers)\n"}
+{"Time":"2023-03-29T13:37:27.456533237Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] UAPI: Updating private key\n"}
+{"Time":"2023-03-29T13:37:27.456666838Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:921\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring router\n"}
+{"Time":"2023-03-29T13:37:27.456681257Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:26\u003e\tfakeRouter.Set\t[v1] warning: fakeRouter.Set: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.456709567Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:931\u003e\t(*userspaceEngine).Reconfig\twgengine: Reconfig: configuring DNS\n"}
+{"Time":"2023-03-29T13:37:27.456736103Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:0}\n"}
+{"Time":"2023-03-29T13:37:27.456785483Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: Resolvercfg: {Routes:{} Hosts:0 LocalDomains:[]}\n"}
+{"Time":"2023-03-29T13:37:27.456811479Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/net/dns/logger.go:98\u003e\tNewManager.func1\tdns: OScfg: {Nameservers:[] SearchDomains:[] MatchDomains:[] Hosts:[]}\n"}
+{"Time":"2023-03-29T13:37:27.456866526Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.456945718Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.457035437Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.456 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted\n"}
+{"Time":"2023-03-29T13:37:27.490404055Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.490 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:35595 derp=1 derpdist=1v4:45ms\n"}
+{"Time":"2023-03-29T13:37:27.490957872Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.490 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:57709 derp=1 derpdist=1v4:31ms\n"}
+{"Time":"2023-03-29T13:37:27.491013794Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.490 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.491185914Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:57709 (stun), 172.20.0.2:57709 (local)\n"}
+{"Time":"2023-03-29T13:37:27.491295468Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.491174325 +0000 UTC m=+4.130781564 Peers:[] LocalAddrs:[{Addr:127.0.0.1:57709 Type:stun} {Addr:172.20.0.2:57709 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.491675303Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.045323829}}}\n"}
+{"Time":"2023-03-29T13:37:27.491767926Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.491649Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.045323829}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.492071167Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.491 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6125567726523641784, \"as_of\": \"2023-03-29T13:37:27.491649Z\", \"key\": \"nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56\", \"disco\": \"discokey:59083cba13956f00814aa780f8a19b58ddd40f9ae6f940398e509d2f2c79076e\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.045323829}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:35595\", \"172.20.0.2:35595\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.492303429Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.492381183Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.492509729Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.492832285Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:27.492955047Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:27.493041705Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.492824353 +0000 UTC m=+4.132431585 Peers:[] LocalAddrs:[{Addr:127.0.0.1:57709 Type:stun} {Addr:172.20.0.2:57709 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.493059245Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.492 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.031410894}}}\n"}
+{"Time":"2023-03-29T13:37:27.493174012Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.491286Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.493508739Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.491286Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.493558301Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.493658522Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.493954418Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.493 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.49414611Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.494032Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.031410894}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.494394917Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.494032Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.031410894}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.494635514Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.494676622Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.494780288Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.494840889Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.494 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.495169992Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.495 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:59384 derp=1 derpdist=1v4:64ms\n"}
+{"Time":"2023-03-29T13:37:27.497129169Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.497 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [XHSZg] ...\n"}
+{"Time":"2023-03-29T13:37:27.497196731Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.497 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:51685 derp=1 derpdist=1v4:71ms\n"}
+{"Time":"2023-03-29T13:37:27.498816504Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.498 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for [XHSZg]\n"}
+{"Time":"2023-03-29T13:37:27.498899237Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.498 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:27.498921801Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.498 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [XHSZg] set to derp-1 (their home)\n"}
+{"Time":"2023-03-29T13:37:27.499047332Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.498 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.498914775 +0000 UTC m=+4.138522011 Peers:[] LocalAddrs:[] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.499167908Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.070836569}}}\n"}
+{"Time":"2023-03-29T13:37:27.499241573Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.499162Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.070836569}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.49953116Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 918863977196614381, \"as_of\": \"2023-03-29T13:37:27.499162Z\", \"key\": \"nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e\", \"disco\": \"discokey:c7f1bea9d6ff269c8662c153c39a3d92f57c567590b806c16bf693226039c84b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.070836569}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805/128\"], \"endpoints\": [\"127.0.0.1:51685\", \"172.20.0.2:51685\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.499756959Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.499820527Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.499954778Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.499 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.500094938Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.500 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.508017322Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.507 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:51993 derp=1 derpdist=1v4:48ms\n"}
+{"Time":"2023-03-29T13:37:27.50805261Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.507 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.508212683Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.508 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:51993 (stun), 172.20.0.2:51993 (local)\n"}
+{"Time":"2023-03-29T13:37:27.508285157Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.508 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.50819775 +0000 UTC m=+4.147804976 Peers:[] LocalAddrs:[{Addr:127.0.0.1:51993 Type:stun} {Addr:172.20.0.2:51993 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.51148228Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.047681075}}}\n"}
+{"Time":"2023-03-29T13:37:27.511561169Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.50828Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.511646171Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [64qRi] ...\n"}
+{"Time":"2023-03-29T13:37:27.512004191Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.50828Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.512026173Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.511 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.512131402Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.512284203Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [64qRi] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:27.512446305Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.512355Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.047681075}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.512694091Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.512355Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.047681075}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.512945734Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.512982501Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.512 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.513074208Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.513149912Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.513458516Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1241\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): no matching peer\n"}
+{"Time":"2023-03-29T13:37:27.513601111Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.513 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:27.514167854Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
+{"Time":"2023-03-29T13:37:27.514216037Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:58992 derp=1 derpdist=1v4:62ms\n"}
+{"Time":"2023-03-29T13:37:27.514270704Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.51443958Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:58992 (stun), 172.20.0.2:58992 (local)\n"}
+{"Time":"2023-03-29T13:37:27.514519618Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.514434952 +0000 UTC m=+4.154042177 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.514591409Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.514515Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.514811275Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.514515Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.514831215Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.514927532Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.514969002Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:27.51501Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.514 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:27.515094247Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.515008199 +0000 UTC m=+4.154615430 Peers:[] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.515144167Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.062405899}}}\n"}
+{"Time":"2023-03-29T13:37:27.515231182Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.515155Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.062405899}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.515477352Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.515155Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.062405899}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.5157436Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.515810118Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.515944355Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.51603017Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.515 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.516114674Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.516 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.520145614Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.520 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [64qRi] d:e6f05f1260bbd611 now using 172.20.0.2:45837\n"}
+{"Time":"2023-03-29T13:37:27.520236787Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.520 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [64qRi] ...\n"}
+{"Time":"2023-03-29T13:37:27.520381164Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.520 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:27.523889233Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.523 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [64qRi] ...\n"}
+{"Time":"2023-03-29T13:37:27.524071718Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.524 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:27.525270748Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] netcheck: measuring HTTPS latency of test (1): unexpected status code: 426 (426 Upgrade Required)\n"}
+{"Time":"2023-03-29T13:37:27.525326669Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest=false hair= portmap= v4a=127.0.0.1:34848 derp=1 derpdist=1v4:65ms\n"}
+{"Time":"2023-03-29T13:37:27.525374168Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1092\u003e\t(*Conn).setNearestDERP\tmagicsock: home is now derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.525530241Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2759\u003e\t(*Conn).logEndpointChange\tmagicsock: endpoints changed: 127.0.0.1:34848 (stun), 172.20.0.2:34848 (local)\n"}
+{"Time":"2023-03-29T13:37:27.525639565Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.525526859 +0000 UTC m=+4.165134074 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:0}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.525953664Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1480\u003e\t(*Conn).derpWriteChanOfAddr\tmagicsock: adding connection to derp-1 for home-keep-alive\n"}
+{"Time":"2023-03-29T13:37:27.525977204Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 1 active derp conns: derp-1=cr0s,wr0s\n"}
+{"Time":"2023-03-29T13:37:27.526067972Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.525 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.525972278 +0000 UTC m=+4.165579492 Peers:[] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.526107105Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": false, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.065420657}}}\n"}
+{"Time":"2023-03-29T13:37:27.526188976Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.5256Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.526396168Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:423\u003e\t(*Conn).UpdateNodes\tno preferred DERP, skipping node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.5256Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 0, \"derp_latency\": null, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.526422378Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.526517317Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.526687969Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/derp/derphttp/derphttp_client.go:401\u003e\t(*Client).connect\tderphttp.Client.Connect: connecting to derp-1 (test)\n"}
+{"Time":"2023-03-29T13:37:27.526875114Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.526 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.526766Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.065420657}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.527145691Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.526766Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.065420657}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.527368061Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.527462668Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.527579879Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 0/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.52765384Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.527 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.531819508Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.531 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:27.532053362Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.532237504Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:27.5322919Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:27.532342787Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:27.532383163Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:27.532430032Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:27.532468962Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - Starting\n"}
+{"Time":"2023-03-29T13:37:27.532522407Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.532 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - Sending handshake initiation\n"}
+{"Time":"2023-03-29T13:37:27.53317143Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.533 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:27.533306241Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.533 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:27.534331952Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [dv8u3] now active, reconfiguring WireGuard\n"}
+{"Time":"2023-03-29T13:37:27.534386848Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.53460606Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:27.534650003Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:27.53470262Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:27.534744335Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:27.534797239Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:27.534834361Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.534 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - Starting\n"}
+{"Time":"2023-03-29T13:37:27.535064975Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - Received handshake initiation\n"}
+{"Time":"2023-03-29T13:37:27.535101365Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - Sending handshake response\n"}
+{"Time":"2023-03-29T13:37:27.535486164Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [dv8u3] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:27.535646633Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.535517538 +0000 UTC m=+4.175124762 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:76ff2edcacaac78382de86ce14dcf7d1464d8bff76ab14412a1c18ef29aa9370}] LocalAddrs:[{Addr:127.0.0.1:45837 Type:stun} {Addr:172.20.0.2:45837 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.535973095Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.535 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:27.536642888Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.536 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - Received handshake response\n"}
+{"Time":"2023-03-29T13:37:27.536713027Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.536 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [dv8u3] d:17b5066de479f458 now using 172.20.0.2:59384\n"}
+{"Time":"2023-03-29T13:37:27.536917351Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.536 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.536783767 +0000 UTC m=+4.176390982 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.536635319 +0000 UTC NodeKey:nodekey:eb8a91888d02040ddaee61afa4ae8d03bd6c35ddf3f76edcaa5bde89743e5c24}] LocalAddrs:[{Addr:127.0.0.1:59384 Type:stun} {Addr:172.20.0.2:59384 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.53763406Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.537 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1705\u003e\t(*Conn).runDerpReader\tmagicsock: derp-1 connected; connGen=1\n"}
+{"Time":"2023-03-29T13:37:27.538314662Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.538 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4781:bb82:1540:3954:6a8): sending disco ping to [dv8u3] ...\n"}
+{"Time":"2023-03-29T13:37:27.542696821Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.542 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:35595 derp=1 derpdist=1v4:9ms\n"}
+{"Time":"2023-03-29T13:37:27.545658947Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.545 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:57709 derp=1 derpdist=1v4:15ms\n"}
+{"Time":"2023-03-29T13:37:27.546526665Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.546 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.014798468}}}\n"}
+{"Time":"2023-03-29T13:37:27.546624395Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.546 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.546513Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.014798468}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.546899956Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.546 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 4984932330222696591, \"as_of\": \"2023-03-29T13:37:27.546513Z\", \"key\": \"nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907\", \"disco\": \"discokey:cc502d2065d3910d659fc206b5c1b833cc8721e43ccd43ed245fc56e1d9d6219\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.014798468}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:57709\", \"172.20.0.2:57709\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.547131568Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.547 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.547206358Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.547 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.547317822Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.547 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.548159459Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
+{"Time":"2023-03-29T13:37:27.548281455Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
+{"Time":"2023-03-29T13:37:27.548330701Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:27.548397336Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.548439957Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:27.548494794Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:27.548600181Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.548645011Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.548694237Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [64qRi] - Stopping\n"}
+{"Time":"2023-03-29T13:37:27.548765909Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:27.548840457Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:27.548886886Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
+{"Time":"2023-03-29T13:37:27.548943901Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.548 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
+{"Time":"2023-03-29T13:37:27.549285716Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
+{"Time":"2023-03-29T13:37:27.549325844Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:27.549397271Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.549434623Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:27.549486154Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:27.549569105Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4781:bb82:1540:3954:6a8): sending disco ping to [dv8u3] ...\n"}
+{"Time":"2023-03-29T13:37:27.54967718Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.549720977Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.549763197Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [dv8u3] - Stopping\n"}
+{"Time":"2023-03-29T13:37:27.549846472Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" t.go:81: 2023-03-29 13:37:27.549 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:27.55012971Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":" stuntest.go:63: STUN server shutdown\n"}
+{"Time":"2023-03-29T13:37:27.55014656Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Output":"--- PASS: TestAgent_SessionExec (0.86s)\n"}
+{"Time":"2023-03-29T13:37:27.562749894Z","Action":"pass","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionExec","Elapsed":0.86}
+{"Time":"2023-03-29T13:37:27.562803065Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.562 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:51993 derp=1 derpdist=1v4:8ms\n"}
+{"Time":"2023-03-29T13:37:27.563392796Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.563 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [XHSZg] ...\n"}
+{"Time":"2023-03-29T13:37:27.563801502Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.563 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.008279639}}}\n"}
+{"Time":"2023-03-29T13:37:27.56407208Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.563 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.563776Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.008279639}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.564819316Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.564 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 3791438885238154126, \"as_of\": \"2023-03-29T13:37:27.563776Z\", \"key\": \"nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00\", \"disco\": \"discokey:9c9ea8075f682592f7a084f08ca03fcad41aea85a44de470f32d96f1067b8a60\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.008279639}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4743:8dab:c855:f633:532/128\"], \"endpoints\": [\"127.0.0.1:51993\", \"172.20.0.2:51993\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.565650461Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.565 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.565870529Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.565 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.566173208Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.566 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.567328308Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.567 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [V4gBN] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:27.567693064Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.567 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [XHSZg] d:cc502d2065d3910d now using 127.0.0.1:57709\n"}
+{"Time":"2023-03-29T13:37:27.567994501Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.567 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [XHSZg] ...\n"}
+{"Time":"2023-03-29T13:37:27.568226707Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.568 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [XHSZg] ...\n"}
+{"Time":"2023-03-29T13:37:27.56908417Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.568 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.569629095Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.569 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:27.569764065Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.569 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:27.569899377Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.569 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:27.570050145Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.569 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:27.57018252Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.570 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:27.570302009Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.570 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Starting\n"}
+{"Time":"2023-03-29T13:37:27.570580746Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.570 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Sending keepalive packet\n"}
+{"Time":"2023-03-29T13:37:27.570704044Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.570 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Sending handshake initiation\n"}
+{"Time":"2023-03-29T13:37:27.571723295Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.571 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [V4gBN] now active, reconfiguring WireGuard\n"}
+{"Time":"2023-03-29T13:37:27.571754224Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.571 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.571960017Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.571 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:27.5720063Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.571 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:27.572053253Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:27.572098018Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:27.572143378Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:27.572182651Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Starting\n"}
+{"Time":"2023-03-29T13:37:27.572412769Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Received handshake initiation\n"}
+{"Time":"2023-03-29T13:37:27.57244765Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Sending handshake response\n"}
+{"Time":"2023-03-29T13:37:27.572888396Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.572 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.57276895 +0000 UTC m=+4.212376174 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:57880137ed805a8c0fd7b29e835a3cd85d87c32c890d3b0f6ed3fc8620837c00}] LocalAddrs:[{Addr:127.0.0.1:57709 Type:stun} {Addr:172.20.0.2:57709 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.573422095Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.573 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Received handshake response\n"}
+{"Time":"2023-03-29T13:37:27.573505226Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.573 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [V4gBN] d:9c9ea8075f682592 now using 172.20.0.2:51993\n"}
+{"Time":"2023-03-29T13:37:27.573673421Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.573 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.573556739 +0000 UTC m=+4.213163954 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.573422752 +0000 UTC NodeKey:nodekey:5c74998353a1ae2dd2b8ee0de399386279c035b2b3d95bd245ba4820d0403907}] LocalAddrs:[{Addr:127.0.0.1:51993 Type:stun} {Addr:172.20.0.2:51993 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.574036177Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.573 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Receiving keepalive packet\n"}
+{"Time":"2023-03-29T13:37:27.578151189Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:58992 derp=1 derpdist=1v4:5ms\n"}
+{"Time":"2023-03-29T13:37:27.578464111Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:34848 derp=1 derpdist=1v4:7ms\n"}
+{"Time":"2023-03-29T13:37:27.578697845Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.005291379}}}\n"}
+{"Time":"2023-03-29T13:37:27.578788271Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.578687Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.005291379}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.579040964Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.578 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 6959219245254193963, \"as_of\": \"2023-03-29T13:37:27.578687Z\", \"key\": \"nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c\", \"disco\": \"discokey:049e454260a62aa19c35b82499dc35811a5aad44ef612f238808cae15d5c5b55\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.005291379}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d/128\"], \"endpoints\": [\"127.0.0.1:58992\", \"172.20.0.2:58992\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.579269912Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.579338921Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.579486192Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.579615554Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:246\u003e\tNewConn.func7\tnetinfo callback\t{\"netinfo\": {\"MappingVariesByDestIP\": null, \"HairPinning\": null, \"WorkingIPv6\": false, \"OSHasIPv6\": false, \"WorkingUDP\": true, \"WorkingICMPv4\": false, \"UPnP\": false, \"PMP\": false, \"PCP\": false, \"PreferredDERP\": 1, \"DERPLatency\": {\"1-v4\": 0.00675754}}}\n"}
+{"Time":"2023-03-29T13:37:27.579715409Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:642\u003e\t(*Conn).sendNode.func1\tsending node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.579606Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.00675754}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.580015856Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.579 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:426\u003e\t(*Conn).UpdateNodes\tadding node\t{\"node\": {\"id\": 2689903771435529409, \"as_of\": \"2023-03-29T13:37:27.579606Z\", \"key\": \"nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f\", \"disco\": \"discokey:34ff526bdd502e84533e42919465a676d8fa64abda3b4f5943a8c9aa6fd0253b\", \"preferred_derp\": 1, \"derp_latency\": {\"1-v4\": 0.00675754}, \"derp_forced_websockets\": {}, \"addresses\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"allowed_ips\": [\"fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4/128\"], \"endpoints\": [\"127.0.0.1:34848\", \"172.20.0.2:34848\"]}}\n"}
+{"Time":"2023-03-29T13:37:27.580200069Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:454\u003e\t(*Conn).UpdateNodes\tupdating network map\n"}
+{"Time":"2023-03-29T13:37:27.580263003Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2578\u003e\t(*Conn).SetNetworkMap\t[v1] magicsock: got updated network map; 1 peers\n"}
+{"Time":"2023-03-29T13:37:27.580357379Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.580 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:967\u003e\t(*userspaceEngine).Reconfig\t[v1] wgengine: Reconfig done\n"}
+{"Time":"2023-03-29T13:37:27.584912838Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.584 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:836\u003e\t(*agent).init.func2\tssh session returned\t{\"error\": \"exit status 127\"}\n"}
+{"Time":"2023-03-29T13:37:27.585085871Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.585 [WARN]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:1119\u003e\t(*agent).handleSSHSession.func2\tfailed to resize tty ...\n"}
+{"Time":"2023-03-29T13:37:27.58510059Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" \"error\": pty: closed:\n"}
+{"Time":"2023-03-29T13:37:27.585107445Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" github.com/coder/coder/pty.(*otherPty).Close\n"}
+{"Time":"2023-03-29T13:37:27.585113814Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" /home/mafredri/src/coder/coder/pty/pty_other.go:134\n"}
+{"Time":"2023-03-29T13:37:27.585512242Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:83: 2023-03-29 13:37:27.585: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:27.585526871Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:74: 2023-03-29 13:37:27.585: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:27.58556302Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:110: 2023-03-29 13:37:27.585: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:27.585579238Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:111: 2023-03-29 13:37:27.585: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:27.585599261Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:113: 2023-03-29 13:37:27.585: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:27.58566446Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:76: 2023-03-29 13:37:27.585: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:27.585677492Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:74: 2023-03-29 13:37:27.585: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:27.585682384Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:76: 2023-03-29 13:37:27.585: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:27.585699939Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:74: 2023-03-29 13:37:27.585: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:27.58570508Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:76: 2023-03-29 13:37:27.585: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:27.585722893Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" ptytest.go:102: 2023-03-29 13:37:27.585: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:27.585976831Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.585 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
+{"Time":"2023-03-29T13:37:27.586016625Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.585 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:27.586092293Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.586133054Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:27.586200024Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:27.586329617Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.586371206Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.586499602Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [XHSZg] - Stopping\n"}
+{"Time":"2023-03-29T13:37:27.586608393Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:27.58672256Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:27.586761915Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
+{"Time":"2023-03-29T13:37:27.586833422Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
+{"Time":"2023-03-29T13:37:27.5869144Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.586 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
+{"Time":"2023-03-29T13:37:27.58723746Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
+{"Time":"2023-03-29T13:37:27.58728708Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:27.587374552Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.587490464Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4743:8dab:c855:f633:532): sending disco ping to [V4gBN] ...\n"}
+{"Time":"2023-03-29T13:37:27.587617597Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.587 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.598290287Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/logger.go:98\u003e\tNewConn.func6\tnetcheck: [v1] report: udp=true v6=false v6os=false mapvarydest= hair= portmap= v4a=127.0.0.1:57709 derp=1 derpdist=1v4:3ms\n"}
+{"Time":"2023-03-29T13:37:27.598667071Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.598735879Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:27.598824991Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:27.598917706Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.598 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [V4gBN] - Stopping\n"}
+{"Time":"2023-03-29T13:37:27.599077451Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" t.go:81: 2023-03-29 13:37:27.599 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:27.599268425Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":" stuntest.go:63: STUN server shutdown\n"}
+{"Time":"2023-03-29T13:37:27.599297466Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Output":"--- PASS: TestAgent_SessionTTYExitCode (0.88s)\n"}
+{"Time":"2023-03-29T13:37:27.624884929Z","Action":"pass","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYExitCode","Elapsed":0.88}
+{"Time":"2023-03-29T13:37:27.624923874Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.624 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n"}
+{"Time":"2023-03-29T13:37:27.625357585Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.625 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [5WitN] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:27.626340882Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [5EOvJ] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:27.626577093Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5WitN] d:34ff526bdd502e84 now using 172.20.0.2:34848\n"}
+{"Time":"2023-03-29T13:37:27.626873494Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n"}
+{"Time":"2023-03-29T13:37:27.627094808Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.626 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [5WitN] ...\n"}
+{"Time":"2023-03-29T13:37:27.627846604Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.627 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.628133034Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:27.628178673Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:27.628231143Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:27.628273924Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:27.628326843Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:27.628365041Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Starting\n"}
+{"Time":"2023-03-29T13:37:27.628423532Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Sending handshake initiation\n"}
+{"Time":"2023-03-29T13:37:27.62897967Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [5EOvJ] now active, reconfiguring WireGuard\n"}
+{"Time":"2023-03-29T13:37:27.62904036Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.628 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.629335987Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:27.629378533Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:27.629437221Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:27.629484031Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:27.629528288Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:27.629564272Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Starting\n"}
+{"Time":"2023-03-29T13:37:27.629899772Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Received handshake initiation\n"}
+{"Time":"2023-03-29T13:37:27.629937538Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.629 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Sending handshake response\n"}
+{"Time":"2023-03-29T13:37:27.630550619Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.630 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.630405833 +0000 UTC m=+4.270013057 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:e443af25902d57edd4bf0b663849e6cb06390f7b80e6ab179dbd5deabea10e0c}] LocalAddrs:[{Addr:127.0.0.1:34848 Type:stun} {Addr:172.20.0.2:34848 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.631298672Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Received handshake response\n"}
+{"Time":"2023-03-29T13:37:27.631395745Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5EOvJ] d:049e454260a62aa1 now using 172.20.0.2:58992\n"}
+{"Time":"2023-03-29T13:37:27.631619667Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.631 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.631481797 +0000 UTC m=+4.271089021 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.631305354 +0000 UTC NodeKey:nodekey:e568ad36a49b4d60323fc0207eded97153e72170c491f74a8942ac38e9dd541f}] LocalAddrs:[{Addr:127.0.0.1:58992 Type:stun} {Addr:172.20.0.2:58992 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.632260898Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.632 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [5WitN] d:34ff526bdd502e84 now using 127.0.0.1:34848\n"}
+{"Time":"2023-03-29T13:37:27.637293337Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.637 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [Xlu3R] ...\n"}
+{"Time":"2023-03-29T13:37:27.637491883Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.637 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [Xlu3R] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:27.638607677Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.638 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:1599\u003e\t(*Conn).setPeerLastDerpLocked\t[v1] magicsock: derp route for [uWfac] set to derp-1 (shared home)\n"}
+{"Time":"2023-03-29T13:37:27.6387189Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.638 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [Xlu3R] d:59083cba13956f00 now using 172.20.0.2:35595\n"}
+{"Time":"2023-03-29T13:37:27.638794177Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.638 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [Xlu3R] ...\n"}
+{"Time":"2023-03-29T13:37:27.638932756Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.638 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4): sending disco ping to [Xlu3R] ...\n"}
+{"Time":"2023-03-29T13:37:27.639446274Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.639629272Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:27.639676449Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:27.639710523Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:27.639738112Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:27.639767671Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:27.639792041Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - Starting\n"}
+{"Time":"2023-03-29T13:37:27.639832214Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.639 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - Sending handshake initiation\n"}
+{"Time":"2023-03-29T13:37:27.640359836Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:584\u003e\t(*userspaceEngine).noteRecvActivity\twgengine: idle peer [uWfac] now active, reconfiguring WireGuard\n"}
+{"Time":"2023-03-29T13:37:27.640405214Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:706\u003e\t(*userspaceEngine).maybeReconfigWireguardLocked\twgengine: Reconfig: configuring userspace WireGuard config (with 1/1 peers)\n"}
+{"Time":"2023-03-29T13:37:27.640596752Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Created\n"}
+{"Time":"2023-03-29T13:37:27.64062514Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Updating endpoint\n"}
+{"Time":"2023-03-29T13:37:27.640660936Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Removing all allowedips\n"}
+{"Time":"2023-03-29T13:37:27.640688925Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Adding allowedip\n"}
+{"Time":"2023-03-29T13:37:27.640721293Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - UAPI: Updating persistent keepalive interval\n"}
+{"Time":"2023-03-29T13:37:27.640745599Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - Starting\n"}
+{"Time":"2023-03-29T13:37:27.640964666Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - Received handshake initiation\n"}
+{"Time":"2023-03-29T13:37:27.640987155Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.640 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - Sending handshake response\n"}
+{"Time":"2023-03-29T13:37:27.641427103Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.641 [DEBUG]\t(agent.tailnet)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.641327519 +0000 UTC m=+4.280934743 Peers:[{TxBytes:92 RxBytes:148 LastHandshake:1970-01-01 00:00:00 +0000 UTC NodeKey:nodekey:b967da7372e7aa1e4ed1fc6f032437dfe7a6e1a0d465cd04c9adf77d69ee2a1e}] LocalAddrs:[{Addr:127.0.0.1:35595 Type:stun} {Addr:172.20.0.2:35595 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.642337602Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.642 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - Received handshake response\n"}
+{"Time":"2023-03-29T13:37:27.642405776Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.642 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [uWfac] d:c7f1bea9d6ff269c now using 172.20.0.2:51685\n"}
+{"Time":"2023-03-29T13:37:27.642612703Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.642 [DEBUG]\t(client)\t\u003cgithub.com/coder/coder/tailnet/conn.go:225\u003e\tNewConn.func6\twireguard status\t{\"status\": \"\\u0026{AsOf:2023-03-29 13:37:27.64250087 +0000 UTC m=+4.282108085 Peers:[{TxBytes:148 RxBytes:92 LastHandshake:2023-03-29 13:37:27.642336966 +0000 UTC NodeKey:nodekey:5e5bb74471183bca142348628f8e5cb431c9b3367f0fe15605a03a1721343e56}] LocalAddrs:[{Addr:127.0.0.1:51685 Type:stun} {Addr:172.20.0.2:51685 Type:local}] DERPs:1}\", \"err\": null}\n"}
+{"Time":"2023-03-29T13:37:27.64326778Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.643 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:4387\u003e\t(*endpoint).handlePongConnLocked\tmagicsock: disco: node [Xlu3R] d:59083cba13956f00 now using 127.0.0.1:35595\n"}
+{"Time":"2023-03-29T13:37:27.648583243Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" agent_test.go:400: \n"}
+{"Time":"2023-03-29T13:37:27.648599647Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/agent/agent_test.go:400\n"}
+{"Time":"2023-03-29T13:37:27.648605577Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \t \t\t\t\t/home/mafredri/src/coder/coder/agent/agent_test.go:401\n"}
+{"Time":"2023-03-29T13:37:27.648610117Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \tError: \t\"\" does not contain \"wazzup\"\n"}
+{"Time":"2023-03-29T13:37:27.648616547Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \tTest: \tTestAgent_Session_TTY_FastCommandHasOutput\n"}
+{"Time":"2023-03-29T13:37:27.648621257Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" \tMessages: \tshould output greeting\n"}
+{"Time":"2023-03-29T13:37:27.648628226Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:83: 2023-03-29 13:37:27.648: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:27.648632598Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:27.648669381Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:110: 2023-03-29 13:37:27.648: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:27.648676209Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:111: 2023-03-29 13:37:27.648: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:27.64868565Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:113: 2023-03-29 13:37:27.648: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:27.648725455Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:27.648735417Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:27.648742887Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:27.648755628Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:74: 2023-03-29 13:37:27.648: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:27.648763111Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:76: 2023-03-29 13:37:27.648: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:27.648781723Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" ptytest.go:102: 2023-03-29 13:37:27.648: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:27.64898407Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
+{"Time":"2023-03-29T13:37:27.64901264Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.648 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:27.649065867Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.649092046Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:27.649138332Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:27.649234264Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.649272903Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.649300503Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5WitN] - Stopping\n"}
+{"Time":"2023-03-29T13:37:27.649378408Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:27.649434677Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
+{"Time":"2023-03-29T13:37:27.649514069Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:27.649551066Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
+{"Time":"2023-03-29T13:37:27.649591609Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
+{"Time":"2023-03-29T13:37:27.649899898Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
+{"Time":"2023-03-29T13:37:27.649921785Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:27.649976578Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.650005597Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.649 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:27.650053977Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:27.650112159Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4d67:9e5c:abb:531:b55d): sending disco ping to [5EOvJ] ...\n"}
+{"Time":"2023-03-29T13:37:27.650212974Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.650252149Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.650280078Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [5EOvJ] - Stopping\n"}
+{"Time":"2023-03-29T13:37:27.650357784Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" t.go:81: 2023-03-29 13:37:27.650 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:27.650618927Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":" stuntest.go:63: STUN server shutdown\n"}
+{"Time":"2023-03-29T13:37:27.650634125Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Output":"--- FAIL: TestAgent_Session_TTY_FastCommandHasOutput (0.95s)\n"}
+{"Time":"2023-03-29T13:37:27.674793681Z","Action":"fail","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_Session_TTY_FastCommandHasOutput","Elapsed":0.95}
+{"Time":"2023-03-29T13:37:27.674817479Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.674 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805): sending disco ping to [uWfac] ...\n"}
+{"Time":"2023-03-29T13:37:27.675734168Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" agent_test.go:235: 2023-03-29 13:37:27.675: cmd: peeked 1/1 bytes = \"$\"\n"}
+{"Time":"2023-03-29T13:37:27.675774216Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" agent_test.go:236: 2023-03-29 13:37:27.675: cmd: stdin: \"echo test\\r\"\n"}
+{"Time":"2023-03-29T13:37:27.676191344Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.676: cmd: \"$ echo test\"\n"}
+{"Time":"2023-03-29T13:37:27.676273026Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" agent_test.go:237: 2023-03-29 13:37:27.676: cmd: matched \"test\" = \"$ echo test\"\n"}
+{"Time":"2023-03-29T13:37:27.676308536Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" agent_test.go:238: 2023-03-29 13:37:27.676: cmd: stdin: \"exit\\r\"\n"}
+{"Time":"2023-03-29T13:37:27.676642013Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.676: cmd: \"exit\"\n"}
+{"Time":"2023-03-29T13:37:27.67773278Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.677: cmd: \"echo test\\r\"\n"}
+{"Time":"2023-03-29T13:37:27.677752958Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.677: cmd: \"exit\\r\"\n"}
+{"Time":"2023-03-29T13:37:27.67778936Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.677: cmd: \"test\\r\"\n"}
+{"Time":"2023-03-29T13:37:27.678126711Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:83: 2023-03-29 13:37:27.678: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:27.678143306Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:74: 2023-03-29 13:37:27.678: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:27.678200486Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:110: 2023-03-29 13:37:27.678: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:27.678223246Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:111: 2023-03-29 13:37:27.678: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:27.678238993Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:113: 2023-03-29 13:37:27.678: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:27.678305772Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:76: 2023-03-29 13:37:27.678: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:27.678359399Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:74: 2023-03-29 13:37:27.678: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:27.678373082Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:76: 2023-03-29 13:37:27.678: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:27.678387877Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:74: 2023-03-29 13:37:27.678: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:27.678398091Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:76: 2023-03-29 13:37:27.678: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:27.678444105Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:121: 2023-03-29 13:37:27.678: cmd: \"$ \"\n"}
+{"Time":"2023-03-29T13:37:27.67847139Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" ptytest.go:102: 2023-03-29 13:37:27.678: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:27.67876363Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.678 [INFO]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:201\u003e\t(*agent).runLoop\tdisconnected from coderd\n"}
+{"Time":"2023-03-29T13:37:27.678920913Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.678 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
+{"Time":"2023-03-29T13:37:27.678978615Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.678 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:27.679093854Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.67916374Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:27.679261738Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:27.679498576Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.679558488Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.679635937Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [Xlu3R] - Stopping\n"}
+{"Time":"2023-03-29T13:37:27.679772893Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(client.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:27.679951878Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"shutting_down\", \"last\": \"ready\"}\n"}
+{"Time":"2023-03-29T13:37:27.680029583Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.679 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:263\u003e\t(*agent).setLifecycle\tset lifecycle state\t{\"state\": \"off\", \"last\": \"shutting_down\"}\n"}
+{"Time":"2023-03-29T13:37:27.680147794Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent)\t\u003cgithub.com/coder/coder/v2/agent/agent.go:229\u003e\t(*agent).reportLifecycleLoop\treporting lifecycle state\t{\"state\": \"off\"}\n"}
+{"Time":"2023-03-29T13:37:27.680759898Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2736\u003e\t(*Conn).closeDerpLocked\tmagicsock: closing connection to derp-1 (conn-close), age 0s\n"}
+{"Time":"2023-03-29T13:37:27.68081749Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/magicsock/magicsock.go:2747\u003e\t(*Conn).logActiveDerpLocked\tmagicsock: 0 active derp conns\n"}
+{"Time":"2023-03-29T13:37:27.680933631Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/router/router_fake.go:31\u003e\tfakeRouter.Close\t[v1] warning: fakeRouter.Close: not implemented.\n"}
+{"Time":"2023-03-29T13:37:27.681009542Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.680 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closing\n"}
+{"Time":"2023-03-29T13:37:27.681106332Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming receiveDERP - stopped\n"}
+{"Time":"2023-03-29T13:37:27.681249372Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/userspace.go:1254\u003e\t(*userspaceEngine).Ping\tping(fd7a:115c:a1e0:4341:84c0:6b1c:81d1:5805): sending disco ping to [uWfac] ...\n"}
+{"Time":"2023-03-29T13:37:27.681454735Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v6 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.681540218Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Routine: receive incoming v4 - stopped\n"}
+{"Time":"2023-03-29T13:37:27.68161487Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] [uWfac] - Stopping\n"}
+{"Time":"2023-03-29T13:37:27.681781085Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" t.go:81: 2023-03-29 13:37:27.681 [DEBUG]\t(agent.tailnet.wgengine)\t\u003ctailscale.com/wgengine/wglog/wglog.go:81\u003e\tNewLogger.func1\twg: [v2] Device closed\n"}
+{"Time":"2023-03-29T13:37:27.682219467Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":" stuntest.go:63: STUN server shutdown\n"}
+{"Time":"2023-03-29T13:37:27.682247508Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Output":"--- PASS: TestAgent_SessionTTYShell (0.94s)\n"}
+{"Time":"2023-03-29T13:37:27.682263381Z","Action":"pass","Package":"github.com/coder/coder/v2/agent","Test":"TestAgent_SessionTTYShell","Elapsed":0.94}
+{"Time":"2023-03-29T13:37:27.682278577Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Output":"FAIL\n"}
+{"Time":"2023-03-29T13:37:27.696326667Z","Action":"output","Package":"github.com/coder/coder/v2/agent","Output":"FAIL\tgithub.com/coder/coder/v2/agent\t4.341s\n"}
+{"Time":"2023-03-29T13:37:27.696360103Z","Action":"fail","Package":"github.com/coder/coder/v2/agent","Elapsed":4.341}
+{"Time":"2023-03-29T13:37:32.643934624Z","Action":"start","Package":"github.com/coder/coder/v2/cli"}
+{"Time":"2023-03-29T13:37:32.790842698Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser"}
+{"Time":"2023-03-29T13:37:32.79088125Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser","Output":"=== RUN TestServerCreateAdminUser\n"}
+{"Time":"2023-03-29T13:37:32.792730073Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK"}
+{"Time":"2023-03-29T13:37:32.792739078Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":"=== RUN TestServerCreateAdminUser/OK\n"}
+{"Time":"2023-03-29T13:37:32.792745576Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":"=== PAUSE TestServerCreateAdminUser/OK\n"}
+{"Time":"2023-03-29T13:37:32.79274818Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK"}
+{"Time":"2023-03-29T13:37:32.79275173Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env"}
+{"Time":"2023-03-29T13:37:32.792754236Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":"=== RUN TestServerCreateAdminUser/Env\n"}
+{"Time":"2023-03-29T13:37:32.792759492Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":"=== PAUSE TestServerCreateAdminUser/Env\n"}
+{"Time":"2023-03-29T13:37:32.792763227Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env"}
+{"Time":"2023-03-29T13:37:32.792767605Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin"}
+{"Time":"2023-03-29T13:37:32.792772306Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":"=== RUN TestServerCreateAdminUser/Stdin\n"}
+{"Time":"2023-03-29T13:37:32.792778719Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":"=== PAUSE TestServerCreateAdminUser/Stdin\n"}
+{"Time":"2023-03-29T13:37:32.792781324Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin"}
+{"Time":"2023-03-29T13:37:32.792785642Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates"}
+{"Time":"2023-03-29T13:37:32.792788132Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":"=== RUN TestServerCreateAdminUser/Validates\n"}
+{"Time":"2023-03-29T13:37:32.792833072Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":"=== PAUSE TestServerCreateAdminUser/Validates\n"}
+{"Time":"2023-03-29T13:37:32.792839332Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates"}
+{"Time":"2023-03-29T13:37:32.792849881Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK"}
+{"Time":"2023-03-29T13:37:32.79285277Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":"=== CONT TestServerCreateAdminUser/OK\n"}
+{"Time":"2023-03-29T13:37:32.794003473Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" server_createadminuser_test.go:87: \n"}
+{"Time":"2023-03-29T13:37:32.794008803Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:87\n"}
+{"Time":"2023-03-29T13:37:32.794012119Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \tError: \tReceived unexpected error:\n"}
+{"Time":"2023-03-29T13:37:32.794014928Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \tcould not start resource:\n"}
+{"Time":"2023-03-29T13:37:32.794017745Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.794020968Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
+{"Time":"2023-03-29T13:37:32.794025025Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
+{"Time":"2023-03-29T13:37:32.794028396Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \n"}
+{"Time":"2023-03-29T13:37:32.794031694Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
+{"Time":"2023-03-29T13:37:32.794034996Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
+{"Time":"2023-03-29T13:37:32.794038934Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.794042353Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
+{"Time":"2023-03-29T13:37:32.794045324Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t github.com/coder/coder/v2/cli_test.TestServerCreateAdminUser.func2\n"}
+{"Time":"2023-03-29T13:37:32.794048191Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:86\n"}
+{"Time":"2023-03-29T13:37:32.794051013Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t testing.tRunner\n"}
+{"Time":"2023-03-29T13:37:32.794053771Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
+{"Time":"2023-03-29T13:37:32.794056664Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t runtime.goexit\n"}
+{"Time":"2023-03-29T13:37:32.794060193Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
+{"Time":"2023-03-29T13:37:32.79406303Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":" \tTest: \tTestServerCreateAdminUser/OK\n"}
+{"Time":"2023-03-29T13:37:32.79407275Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Output":"--- FAIL: TestServerCreateAdminUser/OK (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.794075922Z","Action":"fail","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/OK","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.794079842Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates"}
+{"Time":"2023-03-29T13:37:32.794082413Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":"=== CONT TestServerCreateAdminUser/Validates\n"}
+{"Time":"2023-03-29T13:37:32.795185256Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" server_createadminuser_test.go:227: \n"}
+{"Time":"2023-03-29T13:37:32.795189907Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:227\n"}
+{"Time":"2023-03-29T13:37:32.795192879Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \tError: \tReceived unexpected error:\n"}
+{"Time":"2023-03-29T13:37:32.795195707Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \tcould not start resource:\n"}
+{"Time":"2023-03-29T13:37:32.795198569Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.795203724Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
+{"Time":"2023-03-29T13:37:32.795206991Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
+{"Time":"2023-03-29T13:37:32.795209975Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \n"}
+{"Time":"2023-03-29T13:37:32.795212879Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
+{"Time":"2023-03-29T13:37:32.79521788Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
+{"Time":"2023-03-29T13:37:32.795223388Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.795228236Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
+{"Time":"2023-03-29T13:37:32.795231388Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t github.com/coder/coder/v2/cli_test.TestServerCreateAdminUser.func5\n"}
+{"Time":"2023-03-29T13:37:32.795234339Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:226\n"}
+{"Time":"2023-03-29T13:37:32.795237524Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t testing.tRunner\n"}
+{"Time":"2023-03-29T13:37:32.795240439Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
+{"Time":"2023-03-29T13:37:32.795243318Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t runtime.goexit\n"}
+{"Time":"2023-03-29T13:37:32.795246653Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
+{"Time":"2023-03-29T13:37:32.795249486Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":" \tTest: \tTestServerCreateAdminUser/Validates\n"}
+{"Time":"2023-03-29T13:37:32.795256993Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Output":"--- FAIL: TestServerCreateAdminUser/Validates (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.795260148Z","Action":"fail","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Validates","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.795262834Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin"}
+{"Time":"2023-03-29T13:37:32.795265403Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":"=== CONT TestServerCreateAdminUser/Stdin\n"}
+{"Time":"2023-03-29T13:37:32.795769577Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" server_createadminuser_test.go:187: \n"}
+{"Time":"2023-03-29T13:37:32.795774301Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:187\n"}
+{"Time":"2023-03-29T13:37:32.795777433Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \tError: \tReceived unexpected error:\n"}
+{"Time":"2023-03-29T13:37:32.795782206Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \tcould not start resource:\n"}
+{"Time":"2023-03-29T13:37:32.795787591Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.795793763Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
+{"Time":"2023-03-29T13:37:32.795796926Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
+{"Time":"2023-03-29T13:37:32.795799647Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \n"}
+{"Time":"2023-03-29T13:37:32.795802415Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
+{"Time":"2023-03-29T13:37:32.795805244Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
+{"Time":"2023-03-29T13:37:32.795808186Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.79581094Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
+{"Time":"2023-03-29T13:37:32.795813828Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t github.com/coder/coder/v2/cli_test.TestServerCreateAdminUser.func4\n"}
+{"Time":"2023-03-29T13:37:32.79581676Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:186\n"}
+{"Time":"2023-03-29T13:37:32.795819484Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t testing.tRunner\n"}
+{"Time":"2023-03-29T13:37:32.795822305Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
+{"Time":"2023-03-29T13:37:32.795826355Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t runtime.goexit\n"}
+{"Time":"2023-03-29T13:37:32.795829152Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
+{"Time":"2023-03-29T13:37:32.795832058Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":" \tTest: \tTestServerCreateAdminUser/Stdin\n"}
+{"Time":"2023-03-29T13:37:32.795846761Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Output":"--- FAIL: TestServerCreateAdminUser/Stdin (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.79585045Z","Action":"fail","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Stdin","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.795853001Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env"}
+{"Time":"2023-03-29T13:37:32.795855433Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":"=== CONT TestServerCreateAdminUser/Env\n"}
+{"Time":"2023-03-29T13:37:32.796339444Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" server_createadminuser_test.go:153: \n"}
+{"Time":"2023-03-29T13:37:32.796345738Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:153\n"}
+{"Time":"2023-03-29T13:37:32.796349118Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \tError: \tReceived unexpected error:\n"}
+{"Time":"2023-03-29T13:37:32.796351839Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \tcould not start resource:\n"}
+{"Time":"2023-03-29T13:37:32.796354772Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.796357683Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
+{"Time":"2023-03-29T13:37:32.796360546Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
+{"Time":"2023-03-29T13:37:32.79636323Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \n"}
+{"Time":"2023-03-29T13:37:32.796366079Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
+{"Time":"2023-03-29T13:37:32.796368987Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
+{"Time":"2023-03-29T13:37:32.79637254Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.79637535Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
+{"Time":"2023-03-29T13:37:32.796378207Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t github.com/coder/coder/v2/cli_test.TestServerCreateAdminUser.func3\n"}
+{"Time":"2023-03-29T13:37:32.796381015Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_createadminuser_test.go:152\n"}
+{"Time":"2023-03-29T13:37:32.796383831Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t testing.tRunner\n"}
+{"Time":"2023-03-29T13:37:32.796386544Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
+{"Time":"2023-03-29T13:37:32.796389783Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t runtime.goexit\n"}
+{"Time":"2023-03-29T13:37:32.796394885Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
+{"Time":"2023-03-29T13:37:32.796400461Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":" \tTest: \tTestServerCreateAdminUser/Env\n"}
+{"Time":"2023-03-29T13:37:32.796405793Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Output":"--- FAIL: TestServerCreateAdminUser/Env (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.796409018Z","Action":"fail","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser/Env","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.796411751Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser","Output":"--- FAIL: TestServerCreateAdminUser (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.796414761Z","Action":"fail","Package":"github.com/coder/coder/v2/cli","Test":"TestServerCreateAdminUser","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.796417599Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer"}
+{"Time":"2023-03-29T13:37:32.796420175Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer","Output":"=== RUN TestServer\n"}
+{"Time":"2023-03-29T13:37:32.796424448Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production"}
+{"Time":"2023-03-29T13:37:32.796426853Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":"=== RUN TestServer/Production\n"}
+{"Time":"2023-03-29T13:37:32.797198344Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" server_test.go:109: \n"}
+{"Time":"2023-03-29T13:37:32.797204437Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \tError Trace:\t/home/mafredri/src/coder/coder/cli/server_test.go:109\n"}
+{"Time":"2023-03-29T13:37:32.797207471Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \tError: \tReceived unexpected error:\n"}
+{"Time":"2023-03-29T13:37:32.797210203Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \tcould not start resource:\n"}
+{"Time":"2023-03-29T13:37:32.797213234Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.797216169Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t /home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:113\n"}
+{"Time":"2023-03-29T13:37:32.797219132Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t - dial unix /var/run/docker.sock: connect: no such file or directory\n"}
+{"Time":"2023-03-29T13:37:32.797221978Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t \n"}
+{"Time":"2023-03-29T13:37:32.797224749Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t github.com/ory/dockertest/v3.(*Pool).RunWithOptions\n"}
+{"Time":"2023-03-29T13:37:32.797227673Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t \t/home/mafredri/.local/go/pkg/mod/github.com/ory/dockertest/v3@v3.9.1/dockertest.go:413\n"}
+{"Time":"2023-03-29T13:37:32.797230597Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t github.com/coder/coder/v2/coderd/database/postgres.Open\n"}
+{"Time":"2023-03-29T13:37:32.797234931Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t \t/home/mafredri/src/coder/coder/coderd/database/postgres/postgres.go:77\n"}
+{"Time":"2023-03-29T13:37:32.797243511Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t github.com/coder/coder/v2/cli_test.TestServer.func1\n"}
+{"Time":"2023-03-29T13:37:32.797248163Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t \t/home/mafredri/src/coder/coder/cli/server_test.go:108\n"}
+{"Time":"2023-03-29T13:37:32.79725113Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t testing.tRunner\n"}
+{"Time":"2023-03-29T13:37:32.797253983Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t \t/usr/local/go/src/testing/testing.go:1576\n"}
+{"Time":"2023-03-29T13:37:32.797257318Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t runtime.goexit\n"}
+{"Time":"2023-03-29T13:37:32.797261252Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \t \t \t/usr/local/go/src/runtime/asm_amd64.s:1598\n"}
+{"Time":"2023-03-29T13:37:32.797266495Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":" \tTest: \tTestServer/Production\n"}
+{"Time":"2023-03-29T13:37:32.797274297Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Output":"--- FAIL: TestServer/Production (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.797277522Z","Action":"fail","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Production","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.797280535Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres"}
+{"Time":"2023-03-29T13:37:32.797283019Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":"=== RUN TestServer/BuiltinPostgres\n"}
+{"Time":"2023-03-29T13:37:32.797286137Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":"=== PAUSE TestServer/BuiltinPostgres\n"}
+{"Time":"2023-03-29T13:37:32.797288614Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres"}
+{"Time":"2023-03-29T13:37:32.797294343Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL"}
+{"Time":"2023-03-29T13:37:32.797296802Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":"=== RUN TestServer/BuiltinPostgresURL\n"}
+{"Time":"2023-03-29T13:37:32.797299815Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":"=== PAUSE TestServer/BuiltinPostgresURL\n"}
+{"Time":"2023-03-29T13:37:32.797302293Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL"}
+{"Time":"2023-03-29T13:37:32.797306699Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw"}
+{"Time":"2023-03-29T13:37:32.797309403Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":"=== RUN TestServer/BuiltinPostgresURLRaw\n"}
+{"Time":"2023-03-29T13:37:32.79731478Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":"=== PAUSE TestServer/BuiltinPostgresURLRaw\n"}
+{"Time":"2023-03-29T13:37:32.797319293Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw"}
+{"Time":"2023-03-29T13:37:32.797324667Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL"}
+{"Time":"2023-03-29T13:37:32.797328467Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":"=== RUN TestServer/LocalAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.797331431Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":"=== PAUSE TestServer/LocalAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.797333768Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL"}
+{"Time":"2023-03-29T13:37:32.797341584Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL"}
+{"Time":"2023-03-29T13:37:32.797345334Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":"=== RUN TestServer/RemoteAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.79737723Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":"=== PAUSE TestServer/RemoteAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.797380853Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL"}
+{"Time":"2023-03-29T13:37:32.797385247Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL"}
+{"Time":"2023-03-29T13:37:32.797387813Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":"=== RUN TestServer/NoWarningWithRemoteAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.797405636Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":"=== PAUSE TestServer/NoWarningWithRemoteAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.797408839Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL"}
+{"Time":"2023-03-29T13:37:32.797426981Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoSchemeAccessURL"}
+{"Time":"2023-03-29T13:37:32.797430439Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoSchemeAccessURL","Output":"=== RUN TestServer/NoSchemeAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.797434847Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoSchemeAccessURL","Output":"=== PAUSE TestServer/NoSchemeAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.797437343Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoSchemeAccessURL"}
+{"Time":"2023-03-29T13:37:32.797445885Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadVersion"}
+{"Time":"2023-03-29T13:37:32.797448325Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadVersion","Output":"=== RUN TestServer/TLSBadVersion\n"}
+{"Time":"2023-03-29T13:37:32.797466497Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadVersion","Output":"=== PAUSE TestServer/TLSBadVersion\n"}
+{"Time":"2023-03-29T13:37:32.797471662Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadVersion"}
+{"Time":"2023-03-29T13:37:32.797476114Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadClientAuth"}
+{"Time":"2023-03-29T13:37:32.797478506Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadClientAuth","Output":"=== RUN TestServer/TLSBadClientAuth\n"}
+{"Time":"2023-03-29T13:37:32.797495642Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadClientAuth","Output":"=== PAUSE TestServer/TLSBadClientAuth\n"}
+{"Time":"2023-03-29T13:37:32.79750134Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadClientAuth"}
+{"Time":"2023-03-29T13:37:32.797508036Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid"}
+{"Time":"2023-03-29T13:37:32.797510579Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid","Output":"=== RUN TestServer/TLSInvalid\n"}
+{"Time":"2023-03-29T13:37:32.797525872Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid","Output":"=== PAUSE TestServer/TLSInvalid\n"}
+{"Time":"2023-03-29T13:37:32.797528971Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid"}
+{"Time":"2023-03-29T13:37:32.797533231Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid"}
+{"Time":"2023-03-29T13:37:32.797535658Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":"=== RUN TestServer/TLSValid\n"}
+{"Time":"2023-03-29T13:37:32.797552494Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":"=== PAUSE TestServer/TLSValid\n"}
+{"Time":"2023-03-29T13:37:32.797556455Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid"}
+{"Time":"2023-03-29T13:37:32.797560606Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple"}
+{"Time":"2023-03-29T13:37:32.797563041Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":"=== RUN TestServer/TLSValidMultiple\n"}
+{"Time":"2023-03-29T13:37:32.797579028Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":"=== PAUSE TestServer/TLSValidMultiple\n"}
+{"Time":"2023-03-29T13:37:32.797582214Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple"}
+{"Time":"2023-03-29T13:37:32.797586334Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP"}
+{"Time":"2023-03-29T13:37:32.797588893Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":"=== RUN TestServer/TLSAndHTTP\n"}
+{"Time":"2023-03-29T13:37:32.797623711Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":"=== PAUSE TestServer/TLSAndHTTP\n"}
+{"Time":"2023-03-29T13:37:32.797626474Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP"}
+{"Time":"2023-03-29T13:37:32.797631971Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect"}
+{"Time":"2023-03-29T13:37:32.797634471Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect","Output":"=== RUN TestServer/TLSRedirect\n"}
+{"Time":"2023-03-29T13:37:32.797650491Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect","Output":"=== PAUSE TestServer/TLSRedirect\n"}
+{"Time":"2023-03-29T13:37:32.797654521Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect"}
+{"Time":"2023-03-29T13:37:32.797659191Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4"}
+{"Time":"2023-03-29T13:37:32.797661727Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":"=== RUN TestServer/CanListenUnspecifiedv4\n"}
+{"Time":"2023-03-29T13:37:32.797677081Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":"=== PAUSE TestServer/CanListenUnspecifiedv4\n"}
+{"Time":"2023-03-29T13:37:32.797679898Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4"}
+{"Time":"2023-03-29T13:37:32.797685398Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6"}
+{"Time":"2023-03-29T13:37:32.797688055Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":"=== RUN TestServer/CanListenUnspecifiedv6\n"}
+{"Time":"2023-03-29T13:37:32.797706704Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":"=== PAUSE TestServer/CanListenUnspecifiedv6\n"}
+{"Time":"2023-03-29T13:37:32.797710667Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6"}
+{"Time":"2023-03-29T13:37:32.797714823Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoAddress"}
+{"Time":"2023-03-29T13:37:32.797717235Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoAddress","Output":"=== RUN TestServer/NoAddress\n"}
+{"Time":"2023-03-29T13:37:32.797732629Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoAddress","Output":"=== PAUSE TestServer/NoAddress\n"}
+{"Time":"2023-03-29T13:37:32.797735715Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoAddress"}
+{"Time":"2023-03-29T13:37:32.797739744Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoTLSAddress"}
+{"Time":"2023-03-29T13:37:32.797742309Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoTLSAddress","Output":"=== RUN TestServer/NoTLSAddress\n"}
+{"Time":"2023-03-29T13:37:32.797754504Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoTLSAddress","Output":"=== PAUSE TestServer/NoTLSAddress\n"}
+{"Time":"2023-03-29T13:37:32.797757251Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoTLSAddress"}
+{"Time":"2023-03-29T13:37:32.797762513Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress"}
+{"Time":"2023-03-29T13:37:32.797764941Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress","Output":"=== RUN TestServer/DeprecatedAddress\n"}
+{"Time":"2023-03-29T13:37:32.797791112Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress","Output":"=== PAUSE TestServer/DeprecatedAddress\n"}
+{"Time":"2023-03-29T13:37:32.797794156Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress"}
+{"Time":"2023-03-29T13:37:32.797818478Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown"}
+{"Time":"2023-03-29T13:37:32.797821565Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":"=== RUN TestServer/Shutdown\n"}
+{"Time":"2023-03-29T13:37:32.799148069Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerShutdown1335635398/002 server --in-memory --http-address :0 --access-url http://example.com --provisioner-daemons 1 --cache-dir /tmp/TestServerShutdown1335635398/001\n"}
+{"Time":"2023-03-29T13:37:32.799996289Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:39611\n"}
+{"Time":"2023-03-29T13:37:32.803857037Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:32.820483862Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:32.840616097Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:32.840652908Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:32.840837548Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:32.840996757Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:32.841130055Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:32.843139909Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Output":"--- PASS: TestServer/Shutdown (0.05s)\n"}
+{"Time":"2023-03-29T13:37:32.843152696Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Shutdown","Elapsed":0.05}
+{"Time":"2023-03-29T13:37:32.843181686Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak"}
+{"Time":"2023-03-29T13:37:32.843185961Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":"=== RUN TestServer/TracerNoLeak\n"}
+{"Time":"2023-03-29T13:37:32.84324137Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":"=== PAUSE TestServer/TracerNoLeak\n"}
+{"Time":"2023-03-29T13:37:32.843248162Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak"}
+{"Time":"2023-03-29T13:37:32.84327344Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry"}
+{"Time":"2023-03-29T13:37:32.843276473Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":"=== RUN TestServer/Telemetry\n"}
+{"Time":"2023-03-29T13:37:32.843298273Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":"=== PAUSE TestServer/Telemetry\n"}
+{"Time":"2023-03-29T13:37:32.843301329Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry"}
+{"Time":"2023-03-29T13:37:32.843328619Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus"}
+{"Time":"2023-03-29T13:37:32.843332448Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":"=== RUN TestServer/Prometheus\n"}
+{"Time":"2023-03-29T13:37:32.843360436Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":"=== PAUSE TestServer/Prometheus\n"}
+{"Time":"2023-03-29T13:37:32.843363322Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus"}
+{"Time":"2023-03-29T13:37:32.843393432Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth"}
+{"Time":"2023-03-29T13:37:32.843398556Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":"=== RUN TestServer/GitHubOAuth\n"}
+{"Time":"2023-03-29T13:37:32.843457011Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":"=== PAUSE TestServer/GitHubOAuth\n"}
+{"Time":"2023-03-29T13:37:32.843461316Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth"}
+{"Time":"2023-03-29T13:37:32.843486876Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit"}
+{"Time":"2023-03-29T13:37:32.84349017Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit","Output":"=== RUN TestServer/RateLimit\n"}
+{"Time":"2023-03-29T13:37:32.843512128Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit","Output":"=== PAUSE TestServer/RateLimit\n"}
+{"Time":"2023-03-29T13:37:32.843515107Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit"}
+{"Time":"2023-03-29T13:37:32.843530012Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging"}
+{"Time":"2023-03-29T13:37:32.843532716Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging","Output":"=== RUN TestServer/Logging\n"}
+{"Time":"2023-03-29T13:37:32.843562048Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging","Output":"=== PAUSE TestServer/Logging\n"}
+{"Time":"2023-03-29T13:37:32.843565639Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging"}
+{"Time":"2023-03-29T13:37:32.843591297Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres"}
+{"Time":"2023-03-29T13:37:32.843594335Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":"=== CONT TestServer/BuiltinPostgres\n"}
+{"Time":"2023-03-29T13:37:32.845419328Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerBuiltinPostgres1969653008/002 server --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerBuiltinPostgres1969653008/001\n"}
+{"Time":"2023-03-29T13:37:32.846522439Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Using built-in PostgreSQL (/tmp/TestServerBuiltinPostgres1969653008/002/postgres)\n"}
+{"Time":"2023-03-29T13:37:32.847782539Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging"}
+{"Time":"2023-03-29T13:37:32.847787939Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging","Output":"=== CONT TestServer/Logging\n"}
+{"Time":"2023-03-29T13:37:32.847793104Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile"}
+{"Time":"2023-03-29T13:37:32.847797472Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":"=== RUN TestServer/Logging/CreatesFile\n"}
+{"Time":"2023-03-29T13:37:32.847809954Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":"=== PAUSE TestServer/Logging/CreatesFile\n"}
+{"Time":"2023-03-29T13:37:32.847814894Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile"}
+{"Time":"2023-03-29T13:37:32.847821007Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human"}
+{"Time":"2023-03-29T13:37:32.847823557Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":"=== RUN TestServer/Logging/Human\n"}
+{"Time":"2023-03-29T13:37:32.847827971Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":"=== PAUSE TestServer/Logging/Human\n"}
+{"Time":"2023-03-29T13:37:32.847830572Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human"}
+{"Time":"2023-03-29T13:37:32.847846314Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON"}
+{"Time":"2023-03-29T13:37:32.847849769Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":"=== RUN TestServer/Logging/JSON\n"}
+{"Time":"2023-03-29T13:37:32.847855516Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":"=== PAUSE TestServer/Logging/JSON\n"}
+{"Time":"2023-03-29T13:37:32.847858116Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON"}
+{"Time":"2023-03-29T13:37:32.847862399Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver"}
+{"Time":"2023-03-29T13:37:32.847864919Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":"=== RUN TestServer/Logging/Stackdriver\n"}
+{"Time":"2023-03-29T13:37:32.847879575Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":"=== PAUSE TestServer/Logging/Stackdriver\n"}
+{"Time":"2023-03-29T13:37:32.847884818Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver"}
+{"Time":"2023-03-29T13:37:32.847891759Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple"}
+{"Time":"2023-03-29T13:37:32.847894796Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":"=== RUN TestServer/Logging/Multiple\n"}
+{"Time":"2023-03-29T13:37:32.847919729Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":"=== PAUSE TestServer/Logging/Multiple\n"}
+{"Time":"2023-03-29T13:37:32.847922769Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple"}
+{"Time":"2023-03-29T13:37:32.847927067Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile"}
+{"Time":"2023-03-29T13:37:32.847929708Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":"=== CONT TestServer/Logging/CreatesFile\n"}
+{"Time":"2023-03-29T13:37:32.848797106Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingCreatesFile2700332728/002 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-human /tmp/TestServerLoggingCreatesFile2700332728/001/coder-logging-test-2490710733\n"}
+{"Time":"2023-03-29T13:37:32.849489309Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:46231\n"}
+{"Time":"2023-03-29T13:37:32.849644524Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit"}
+{"Time":"2023-03-29T13:37:32.849648931Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit","Output":"=== CONT TestServer/RateLimit\n"}
+{"Time":"2023-03-29T13:37:32.849654201Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default"}
+{"Time":"2023-03-29T13:37:32.849658963Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":"=== RUN TestServer/RateLimit/Default\n"}
+{"Time":"2023-03-29T13:37:32.849673813Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":"=== PAUSE TestServer/RateLimit/Default\n"}
+{"Time":"2023-03-29T13:37:32.849676949Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default"}
+{"Time":"2023-03-29T13:37:32.849690853Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed"}
+{"Time":"2023-03-29T13:37:32.849694907Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":"=== RUN TestServer/RateLimit/Changed\n"}
+{"Time":"2023-03-29T13:37:32.849700056Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":"=== PAUSE TestServer/RateLimit/Changed\n"}
+{"Time":"2023-03-29T13:37:32.849702697Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed"}
+{"Time":"2023-03-29T13:37:32.849706931Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled"}
+{"Time":"2023-03-29T13:37:32.849709323Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":"=== RUN TestServer/RateLimit/Disabled\n"}
+{"Time":"2023-03-29T13:37:32.849713561Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":"=== PAUSE TestServer/RateLimit/Disabled\n"}
+{"Time":"2023-03-29T13:37:32.849716093Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled"}
+{"Time":"2023-03-29T13:37:32.849720102Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default"}
+{"Time":"2023-03-29T13:37:32.849722411Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":"=== CONT TestServer/RateLimit/Default\n"}
+{"Time":"2023-03-29T13:37:32.850485755Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerRateLimitDefault1881118474/001 server --in-memory --http-address :0 --access-url http://example.com\n"}
+{"Time":"2023-03-29T13:37:32.851011889Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:38127\n"}
+{"Time":"2023-03-29T13:37:32.851175576Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth"}
+{"Time":"2023-03-29T13:37:32.851180015Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":"=== CONT TestServer/GitHubOAuth\n"}
+{"Time":"2023-03-29T13:37:32.851983967Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerGitHubOAuth724593823/001 server --in-memory --http-address :0 --access-url http://example.com --oauth2-github-allow-everyone --oauth2-github-client-id fake --oauth2-github-client-secret fake --oauth2-github-enterprise-base-url https://fake-url.com\n"}
+{"Time":"2023-03-29T13:37:32.852521551Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:33313\n"}
+{"Time":"2023-03-29T13:37:32.852663734Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus"}
+{"Time":"2023-03-29T13:37:32.852668353Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":"=== CONT TestServer/Prometheus\n"}
+{"Time":"2023-03-29T13:37:32.853556318Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerPrometheus2050744846/002 server --in-memory --http-address :0 --access-url http://example.com --provisioner-daemons 1 --prometheus-enable --prometheus-address :37569 --cache-dir /tmp/TestServerPrometheus2050744846/001\n"}
+{"Time":"2023-03-29T13:37:32.854050354Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:38381\n"}
+{"Time":"2023-03-29T13:37:32.854220704Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry"}
+{"Time":"2023-03-29T13:37:32.854225473Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":"=== CONT TestServer/Telemetry\n"}
+{"Time":"2023-03-29T13:37:32.855113604Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTelemetry4206954660/002 server --in-memory --http-address :0 --access-url http://example.com --telemetry --telemetry-url http://127.0.0.1:46805 --cache-dir /tmp/TestServerTelemetry4206954660/001\n"}
+{"Time":"2023-03-29T13:37:32.855607409Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:46851\n"}
+{"Time":"2023-03-29T13:37:32.855759635Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak"}
+{"Time":"2023-03-29T13:37:32.85576445Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":"=== CONT TestServer/TracerNoLeak\n"}
+{"Time":"2023-03-29T13:37:32.856575033Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTracerNoLeak127485117/002 server --in-memory --http-address :0 --access-url http://example.com --trace=true --cache-dir /tmp/TestServerTracerNoLeak127485117/001\n"}
+{"Time":"2023-03-29T13:37:32.859583574Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress"}
+{"Time":"2023-03-29T13:37:32.859590623Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress","Output":"=== CONT TestServer/DeprecatedAddress\n"}
+{"Time":"2023-03-29T13:37:32.859595635Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP"}
+{"Time":"2023-03-29T13:37:32.859598523Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":"=== RUN TestServer/DeprecatedAddress/HTTP\n"}
+{"Time":"2023-03-29T13:37:32.860822192Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoTLSAddress"}
+{"Time":"2023-03-29T13:37:32.860826569Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoTLSAddress","Output":"=== CONT TestServer/NoTLSAddress\n"}
+{"Time":"2023-03-29T13:37:32.861575027Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoTLSAddress","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerNoTLSAddress2369561878/001 server --in-memory --tls-enable=true --tls-address \n"}
+{"Time":"2023-03-29T13:37:32.861999568Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoTLSAddress","Output":"--- PASS: TestServer/NoTLSAddress (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.863329199Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoTLSAddress","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.863344025Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoAddress"}
+{"Time":"2023-03-29T13:37:32.863348554Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoAddress","Output":"=== CONT TestServer/NoAddress\n"}
+{"Time":"2023-03-29T13:37:32.864056037Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoAddress","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerNoAddress3728350894/001 server --in-memory --http-address :80 --tls-enable=false --tls-address \n"}
+{"Time":"2023-03-29T13:37:32.864465815Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoAddress","Output":"--- PASS: TestServer/NoAddress (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.865769258Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoAddress","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.865776672Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6"}
+{"Time":"2023-03-29T13:37:32.865779982Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":"=== CONT TestServer/CanListenUnspecifiedv6\n"}
+{"Time":"2023-03-29T13:37:32.866452947Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerCanListenUnspecifiedv63515544106/001 server --in-memory --http-address [::]:0 --access-url http://example.com\n"}
+{"Time":"2023-03-29T13:37:32.86788654Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4"}
+{"Time":"2023-03-29T13:37:32.867892077Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":"=== CONT TestServer/CanListenUnspecifiedv4\n"}
+{"Time":"2023-03-29T13:37:32.868594181Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerCanListenUnspecifiedv43698525072/001 server --in-memory --http-address 0.0.0.0:0 --access-url http://example.com\n"}
+{"Time":"2023-03-29T13:37:32.87981375Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect"}
+{"Time":"2023-03-29T13:37:32.879832688Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect","Output":"=== CONT TestServer/TLSRedirect\n"}
+{"Time":"2023-03-29T13:37:32.879840427Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK"}
+{"Time":"2023-03-29T13:37:32.879843579Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":"=== RUN TestServer/TLSRedirect/OK\n"}
+{"Time":"2023-03-29T13:37:32.879847493Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":"=== PAUSE TestServer/TLSRedirect/OK\n"}
+{"Time":"2023-03-29T13:37:32.879850007Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK"}
+{"Time":"2023-03-29T13:37:32.879858919Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect"}
+{"Time":"2023-03-29T13:37:32.87986153Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":"=== RUN TestServer/TLSRedirect/NoRedirect\n"}
+{"Time":"2023-03-29T13:37:32.879868217Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":"=== PAUSE TestServer/TLSRedirect/NoRedirect\n"}
+{"Time":"2023-03-29T13:37:32.879871031Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect"}
+{"Time":"2023-03-29T13:37:32.879882319Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard"}
+{"Time":"2023-03-29T13:37:32.879885302Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":"=== RUN TestServer/TLSRedirect/NoRedirectWithWildcard\n"}
+{"Time":"2023-03-29T13:37:32.879901278Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":"=== PAUSE TestServer/TLSRedirect/NoRedirectWithWildcard\n"}
+{"Time":"2023-03-29T13:37:32.879904339Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard"}
+{"Time":"2023-03-29T13:37:32.879917091Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener"}
+{"Time":"2023-03-29T13:37:32.879920112Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":"=== RUN TestServer/TLSRedirect/NoTLSListener\n"}
+{"Time":"2023-03-29T13:37:32.881385273Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP"}
+{"Time":"2023-03-29T13:37:32.881392419Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":"=== CONT TestServer/TLSAndHTTP\n"}
+{"Time":"2023-03-29T13:37:32.883317205Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSAndHTTP1565439063/003 server --in-memory --http-address :0 --access-url https://example.com --tls-enable --tls-redirect-http-to-https=false --tls-address :0 --tls-cert-file /tmp/TestServerTLSAndHTTP1565439063/001/1540336369 --tls-key-file /tmp/TestServerTLSAndHTTP1565439063/001/228555534 --cache-dir /tmp/TestServerTLSAndHTTP1565439063/002\n"}
+{"Time":"2023-03-29T13:37:32.886709195Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple"}
+{"Time":"2023-03-29T13:37:32.886720913Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":"=== CONT TestServer/TLSValidMultiple\n"}
+{"Time":"2023-03-29T13:37:32.888650035Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSValidMultiple3077156975/004 server --in-memory --http-address --access-url https://example.com --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSValidMultiple3077156975/001/3448842699 --tls-key-file /tmp/TestServerTLSValidMultiple3077156975/001/3382329005 --tls-cert-file /tmp/TestServerTLSValidMultiple3077156975/002/610618616 --tls-key-file /tmp/TestServerTLSValidMultiple3077156975/002/3728409390 --cache-dir /tmp/TestServerTLSValidMultiple3077156975/003\n"}
+{"Time":"2023-03-29T13:37:32.889032741Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid"}
+{"Time":"2023-03-29T13:37:32.889040091Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":"=== CONT TestServer/TLSValid\n"}
+{"Time":"2023-03-29T13:37:32.890094104Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSValid1911968885/003 server --in-memory --http-address --access-url https://example.com --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSValid1911968885/001/91528180 --tls-key-file /tmp/TestServerTLSValid1911968885/001/1223943395 --cache-dir /tmp/TestServerTLSValid1911968885/002\n"}
+{"Time":"2023-03-29T13:37:32.890711151Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Started TLS/HTTPS listener at https://[::]:38747\n"}
+{"Time":"2023-03-29T13:37:32.893432414Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid"}
+{"Time":"2023-03-29T13:37:32.893439577Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid","Output":"=== CONT TestServer/TLSInvalid\n"}
+{"Time":"2023-03-29T13:37:32.895534213Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert"}
+{"Time":"2023-03-29T13:37:32.895540534Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert","Output":"=== RUN TestServer/TLSInvalid/NoCert\n"}
+{"Time":"2023-03-29T13:37:32.895545749Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert","Output":"=== PAUSE TestServer/TLSInvalid/NoCert\n"}
+{"Time":"2023-03-29T13:37:32.895548511Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert"}
+{"Time":"2023-03-29T13:37:32.89555469Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey"}
+{"Time":"2023-03-29T13:37:32.895559989Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey","Output":"=== RUN TestServer/TLSInvalid/NoKey\n"}
+{"Time":"2023-03-29T13:37:32.895564748Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey","Output":"=== PAUSE TestServer/TLSInvalid/NoKey\n"}
+{"Time":"2023-03-29T13:37:32.895567562Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey"}
+{"Time":"2023-03-29T13:37:32.895571832Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount"}
+{"Time":"2023-03-29T13:37:32.895574371Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":"=== RUN TestServer/TLSInvalid/MismatchedCount\n"}
+{"Time":"2023-03-29T13:37:32.895579082Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":"=== PAUSE TestServer/TLSInvalid/MismatchedCount\n"}
+{"Time":"2023-03-29T13:37:32.895581688Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount"}
+{"Time":"2023-03-29T13:37:32.895586121Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey"}
+{"Time":"2023-03-29T13:37:32.895588766Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":"=== RUN TestServer/TLSInvalid/MismatchedCertAndKey\n"}
+{"Time":"2023-03-29T13:37:32.895593302Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":"=== PAUSE TestServer/TLSInvalid/MismatchedCertAndKey\n"}
+{"Time":"2023-03-29T13:37:32.895595797Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey"}
+{"Time":"2023-03-29T13:37:32.895598709Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert"}
+{"Time":"2023-03-29T13:37:32.895601189Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert","Output":"=== CONT TestServer/TLSInvalid/NoCert\n"}
+{"Time":"2023-03-29T13:37:32.896383938Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSInvalidNoCert155343146/002 server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidNoCert155343146/001 --tls-enable --tls-key-file /tmp/TestServerTLSInvalid1610620518/001/1816833089\n"}
+{"Time":"2023-03-29T13:37:32.896712838Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:41521\n"}
+{"Time":"2023-03-29T13:37:32.896777738Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert","Output":" server_test.go:344: args: [server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidNoCert155343146/001 --tls-enable --tls-key-file /tmp/TestServerTLSInvalid1610620518/001/1816833089]\n"}
+{"Time":"2023-03-29T13:37:32.896926023Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert","Output":"--- PASS: TestServer/TLSInvalid/NoCert (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.896932574Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoCert","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.896936656Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadClientAuth"}
+{"Time":"2023-03-29T13:37:32.896939204Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadClientAuth","Output":"=== CONT TestServer/TLSBadClientAuth\n"}
+{"Time":"2023-03-29T13:37:32.897600801Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadClientAuth","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSBadClientAuth2976087810/002 server --in-memory --http-address --access-url http://example.com --tls-enable --tls-address :0 --tls-client-auth something --cache-dir /tmp/TestServerTLSBadClientAuth2976087810/001\n"}
+{"Time":"2023-03-29T13:37:32.897994004Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadClientAuth","Output":"--- PASS: TestServer/TLSBadClientAuth (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.898002196Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadClientAuth","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.89800515Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadVersion"}
+{"Time":"2023-03-29T13:37:32.898008134Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadVersion","Output":"=== CONT TestServer/TLSBadVersion\n"}
+{"Time":"2023-03-29T13:37:32.898635976Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadVersion","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSBadVersion2460276843/002 server --in-memory --http-address --access-url http://example.com --tls-enable --tls-address :0 --tls-min-version tls9 --cache-dir /tmp/TestServerTLSBadVersion2460276843/001\n"}
+{"Time":"2023-03-29T13:37:32.899017393Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadVersion","Output":"--- PASS: TestServer/TLSBadVersion (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.899025249Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSBadVersion","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.899028489Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoSchemeAccessURL"}
+{"Time":"2023-03-29T13:37:32.899031117Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoSchemeAccessURL","Output":"=== CONT TestServer/NoSchemeAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.899735022Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoSchemeAccessURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerNoSchemeAccessURL3227162288/002 server --in-memory --http-address :0 --access-url google.com --cache-dir /tmp/TestServerNoSchemeAccessURL3227162288/001\n"}
+{"Time":"2023-03-29T13:37:32.900078052Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoSchemeAccessURL","Output":"--- PASS: TestServer/NoSchemeAccessURL (0.00s)\n"}
+{"Time":"2023-03-29T13:37:32.900133291Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoSchemeAccessURL","Elapsed":0}
+{"Time":"2023-03-29T13:37:32.900136928Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL"}
+{"Time":"2023-03-29T13:37:32.900139387Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":"=== CONT TestServer/NoWarningWithRemoteAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.900802695Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerNoWarningWithRemoteAccessURL2261451002/002 server --in-memory --http-address :0 --access-url https://google.com --cache-dir /tmp/TestServerNoWarningWithRemoteAccessURL2261451002/001\n"}
+{"Time":"2023-03-29T13:37:32.901428258Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL"}
+{"Time":"2023-03-29T13:37:32.901434866Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":"=== CONT TestServer/RemoteAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.902088159Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerRemoteAccessURL917985260/002 server --in-memory --http-address :0 --access-url https://foobarbaz.mydomain --cache-dir /tmp/TestServerRemoteAccessURL917985260/001\n"}
+{"Time":"2023-03-29T13:37:32.904875871Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL"}
+{"Time":"2023-03-29T13:37:32.90488445Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":"=== CONT TestServer/LocalAccessURL\n"}
+{"Time":"2023-03-29T13:37:32.905523425Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLocalAccessURL3554694382/002 server --in-memory --http-address :0 --access-url http://localhost:3000/ --cache-dir /tmp/TestServerLocalAccessURL3554694382/001\n"}
+{"Time":"2023-03-29T13:37:32.909203431Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw"}
+{"Time":"2023-03-29T13:37:32.909214782Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":"=== CONT TestServer/BuiltinPostgresURLRaw\n"}
+{"Time":"2023-03-29T13:37:32.90987112Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerBuiltinPostgresURLRaw2301128244/001 server postgres-builtin-url --raw-url\n"}
+{"Time":"2023-03-29T13:37:32.910387857Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL"}
+{"Time":"2023-03-29T13:37:32.910393909Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":"=== CONT TestServer/BuiltinPostgresURL\n"}
+{"Time":"2023-03-29T13:37:32.911031385Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerBuiltinPostgresURL2022412164/001 server postgres-builtin-url\n"}
+{"Time":"2023-03-29T13:37:32.911696996Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple"}
+{"Time":"2023-03-29T13:37:32.911705024Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":"=== CONT TestServer/Logging/Multiple\n"}
+{"Time":"2023-03-29T13:37:32.912435053Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingMultiple1018156314/004 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-human /tmp/TestServerLoggingMultiple1018156314/001/coder-logging-test-2240709984 --log-json /tmp/TestServerLoggingMultiple1018156314/002/coder-logging-test-2164710923 --log-stackdriver /tmp/TestServerLoggingMultiple1018156314/003/coder-logging-test-3557853095\n"}
+{"Time":"2023-03-29T13:37:32.912514796Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver"}
+{"Time":"2023-03-29T13:37:32.912520309Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":"=== CONT TestServer/Logging/Stackdriver\n"}
+{"Time":"2023-03-29T13:37:32.913204422Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingStackdriver1654522233/002 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-stackdriver /tmp/TestServerLoggingStackdriver1654522233/001/coder-logging-test-3531177805\n"}
+{"Time":"2023-03-29T13:37:32.913286393Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON"}
+{"Time":"2023-03-29T13:37:32.913292549Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":"=== CONT TestServer/Logging/JSON\n"}
+{"Time":"2023-03-29T13:37:32.913937207Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingJSON1509288451/002 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-json /tmp/TestServerLoggingJSON1509288451/001/coder-logging-test-115410787\n"}
+{"Time":"2023-03-29T13:37:32.913982029Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human"}
+{"Time":"2023-03-29T13:37:32.913985447Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":"=== CONT TestServer/Logging/Human\n"}
+{"Time":"2023-03-29T13:37:32.914664441Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerLoggingHuman1910502850/002 server --verbose --in-memory --http-address :0 --access-url http://example.com --log-human /tmp/TestServerLoggingHuman1910502850/001/coder-logging-test-2040592756\n"}
+{"Time":"2023-03-29T13:37:32.915201663Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:42891\n"}
+{"Time":"2023-03-29T13:37:32.915334373Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled"}
+{"Time":"2023-03-29T13:37:32.91533902Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":"=== CONT TestServer/RateLimit/Disabled\n"}
+{"Time":"2023-03-29T13:37:32.915962046Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerRateLimitDisabled1912095220/001 server --in-memory --http-address :0 --access-url http://example.com --api-rate-limit -1\n"}
+{"Time":"2023-03-29T13:37:32.916390927Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:35065\n"}
+{"Time":"2023-03-29T13:37:32.917528951Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stderr: 2023-03-29 13:37:32.917 [WARN]\t\u003cgithub.com/coder/coder/v2/cli/server.go:310\u003e\t(*RootCmd).Server.func1\tstart telemetry exporter ...\n"}
+{"Time":"2023-03-29T13:37:32.917535354Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" \"error\": default exporter:\n"}
+{"Time":"2023-03-29T13:37:32.917538568Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" github.com/coder/coder/v2/coderd/tracing.TracerProvider\n"}
+{"Time":"2023-03-29T13:37:32.917543903Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" /home/mafredri/src/coder/coder/coderd/tracing/exporter.go:51\n"}
+{"Time":"2023-03-29T13:37:32.917547458Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" - create otlp exporter:\n"}
+{"Time":"2023-03-29T13:37:32.917552386Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" github.com/coder/coder/v2/coderd/tracing.DefaultExporter\n"}
+{"Time":"2023-03-29T13:37:32.917557328Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" /home/mafredri/src/coder/coder/coderd/tracing/exporter.go:109\n"}
+{"Time":"2023-03-29T13:37:32.917562236Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" - context canceled\n"}
+{"Time":"2023-03-29T13:37:32.917576569Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:42093\n"}
+{"Time":"2023-03-29T13:37:32.917619611Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stderr: \u001b[1;mWARN: \u001b[0mThe access URL \u001b[;mhttp://example.com\u001b[0m could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n"}
+{"Time":"2023-03-29T13:37:32.917642615Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:32.920130044Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:32.920155751Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:32.920178993Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:32.920200709Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:32.920249847Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":"=== PAUSE TestServer/DeprecatedAddress/HTTP\n"}
+{"Time":"2023-03-29T13:37:32.92025399Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP"}
+{"Time":"2023-03-29T13:37:32.920269801Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS"}
+{"Time":"2023-03-29T13:37:32.920274896Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":"=== RUN TestServer/DeprecatedAddress/TLS\n"}
+{"Time":"2023-03-29T13:37:32.920279669Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":"=== PAUSE TestServer/DeprecatedAddress/TLS\n"}
+{"Time":"2023-03-29T13:37:32.920282197Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS"}
+{"Time":"2023-03-29T13:37:32.92570047Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":"=== PAUSE TestServer/TLSRedirect/NoTLSListener\n"}
+{"Time":"2023-03-29T13:37:32.92570807Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener"}
+{"Time":"2023-03-29T13:37:32.925713393Z","Action":"run","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener"}
+{"Time":"2023-03-29T13:37:32.92571595Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":"=== RUN TestServer/TLSRedirect/NoHTTPListener\n"}
+{"Time":"2023-03-29T13:37:32.925720434Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":"=== PAUSE TestServer/TLSRedirect/NoHTTPListener\n"}
+{"Time":"2023-03-29T13:37:32.925722986Z","Action":"pause","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener"}
+{"Time":"2023-03-29T13:37:32.92724642Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.927: cmd: \"Started HTTP listener at http://[::]:39671\"\n"}
+{"Time":"2023-03-29T13:37:32.927965116Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.927: cmd: \"Started HTTP listener at http://[::]:42445\"\n"}
+{"Time":"2023-03-29T13:37:32.928006932Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.927: cmd: \"WARN: The access URL http://localhost:3000/ isn't externally reachable, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
+{"Time":"2023-03-29T13:37:32.928023919Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:32.92802928Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:32.928042009Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \"View the Web UI: http://localhost:3000/\\r\"\n"}
+{"Time":"2023-03-29T13:37:32.928061275Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:32.928077795Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:32.928: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:32.973554406Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" server_test.go:173: 2023-03-29 13:37:32.973: cmd: matched newline = \"postgres://coder@localhost:43211/coder?sslmode=disable\u0026password=Xha7Pt7Mcuv0IlkT\"\n"}
+{"Time":"2023-03-29T13:37:32.973585539Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:83: 2023-03-29 13:37:32.973: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:32.973602051Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:74: 2023-03-29 13:37:32.973: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:32.973653232Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:110: 2023-03-29 13:37:32.973: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:32.973673734Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:111: 2023-03-29 13:37:32.973: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:32.973681952Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:113: 2023-03-29 13:37:32.973: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:32.973745487Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:76: 2023-03-29 13:37:32.973: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:32.973755467Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:74: 2023-03-29 13:37:32.973: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:32.973767632Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:76: 2023-03-29 13:37:32.973: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:32.973787295Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:74: 2023-03-29 13:37:32.973: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:32.973794335Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:76: 2023-03-29 13:37:32.973: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:32.973897477Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" server_test.go:161: 2023-03-29 13:37:32.973: cmd: matched \"psql\" = \" psql\"\n"}
+{"Time":"2023-03-29T13:37:32.973911816Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:83: 2023-03-29 13:37:32.973: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:32.97395369Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:74: 2023-03-29 13:37:32.973: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:32.973977688Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:110: 2023-03-29 13:37:32.973: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:32.973985059Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:111: 2023-03-29 13:37:32.973: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:32.974011895Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:113: 2023-03-29 13:37:32.973: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:32.974059281Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:76: 2023-03-29 13:37:32.974: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:32.974066822Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:74: 2023-03-29 13:37:32.974: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:32.974073474Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:76: 2023-03-29 13:37:32.974: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:32.974079891Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:74: 2023-03-29 13:37:32.974: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:32.974111815Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:76: 2023-03-29 13:37:32.974: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:32.976190183Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:32.976 [DEBUG]\t\u003cgithub.com/coder/coder/v2/cli/server.go:260\u003e\t(*RootCmd).Server.func1\tstarted debug logging\n"}
+{"Time":"2023-03-29T13:37:32.976499482Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:45725\n"}
+{"Time":"2023-03-29T13:37:32.977583615Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:32.977594677Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed"}
+{"Time":"2023-03-29T13:37:32.977598319Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":"=== CONT TestServer/RateLimit/Changed\n"}
+{"Time":"2023-03-29T13:37:32.979536902Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerRateLimitChanged2140102987/001 server --in-memory --http-address :0 --access-url http://example.com --api-rate-limit 100\n"}
+{"Time":"2023-03-29T13:37:32.981248381Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:45133\n"}
+{"Time":"2023-03-29T13:37:32.982290561Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:32.982: cmd: \"Started HTTP listener at http://0.0.0.0:37181\"\n"}
+{"Time":"2023-03-29T13:37:32.982961159Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" server_test.go:718: 2023-03-29 13:37:32.982: cmd: matched \"Started HTTP listener\" = \"Started HTTP listener\"\n"}
+{"Time":"2023-03-29T13:37:32.983105329Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" server_test.go:719: 2023-03-29 13:37:32.983: cmd: matched \"http://0.0.0.0:\" = \" at http://0.0.0.0:\"\n"}
+{"Time":"2023-03-29T13:37:32.983250515Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:32.983: cmd: \"Started HTTP listener at http://[::]:33561\"\n"}
+{"Time":"2023-03-29T13:37:32.984671213Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" server_test.go:738: 2023-03-29 13:37:32.983: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
+{"Time":"2023-03-29T13:37:32.984712571Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" server_test.go:739: 2023-03-29 13:37:32.983: cmd: matched \"http://[::]:\" = \" http://[::]:\"\n"}
+{"Time":"2023-03-29T13:37:32.984733017Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:32.996108891Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:32.996314264Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:33.00125555Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:33.001437292Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:33.002347544Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:33.00471704Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:33.004816922Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stderr: \u001b[1;mWARN: \u001b[0mThe access URL \u001b[;mhttp://example.com\u001b[0m could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n"}
+{"Time":"2023-03-29T13:37:33.004828798Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:33.008304441Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:33.008334151Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:33.008344183Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:33.008351448Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:33.008401366Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 3...\n"}
+{"Time":"2023-03-29T13:37:33.00852168Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 3\n"}
+{"Time":"2023-03-29T13:37:33.008558749Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP"}
+{"Time":"2023-03-29T13:37:33.008572474Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":"=== CONT TestServer/DeprecatedAddress/HTTP\n"}
+{"Time":"2023-03-29T13:37:33.009297224Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerDeprecatedAddressHTTP1174595269/002 server --in-memory --address :0 --access-url http://example.com --cache-dir /tmp/TestServerDeprecatedAddressHTTP1174595269/001\n"}
+{"Time":"2023-03-29T13:37:33.011200842Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" server_test.go:196: 2023-03-29 13:37:33.011: cmd: matched \"this may cause unexpected problems when creating workspaces\" = \"Started HTTP listener at http://[::]:42445\\r\\nWARN: The access URL http://localhost:3000/ isn't externally reachable, this may cause unexpected problems when creating workspaces\"\n"}
+{"Time":"2023-03-29T13:37:33.011256139Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" server_test.go:197: 2023-03-29 13:37:33.011: cmd: matched \"View the Web UI: http://localhost:3000/\" = \". Generate a unique *.try.coder.app URL by not specifying an access URL.\\r\\n \\r\\r\\n \\r\\nView the Web UI: http://localhost:3000/\"\n"}
+{"Time":"2023-03-29T13:37:33.011375838Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: \u001b[1;mWARN: \u001b[0mThe access URL \u001b[;mhttp://example.com\u001b[0m could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n"}
+{"Time":"2023-03-29T13:37:33.01139606Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:33.013674142Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:33.013696745Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:33.013708678Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:33.013717271Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:33.013756177Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 3...\n"}
+{"Time":"2023-03-29T13:37:33.013807024Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.013 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\t(*Server).closeWithError\tclosing server with error\t{\"error\": null}\n"}
+{"Time":"2023-03-29T13:37:33.013861318Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 3\n"}
+{"Time":"2023-03-29T13:37:33.014354405Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:121: 2023-03-29 13:37:33.014: cmd: \"postgres://coder@localhost:43211/coder?sslmode=disable\u0026password=Xha7Pt7Mcuv0IlkT\"\n"}
+{"Time":"2023-03-29T13:37:33.014367303Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":" ptytest.go:102: 2023-03-29 13:37:33.014: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:33.01451535Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Output":"--- PASS: TestServer/BuiltinPostgresURLRaw (0.11s)\n"}
+{"Time":"2023-03-29T13:37:33.014525519Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURLRaw","Elapsed":0.11}
+{"Time":"2023-03-29T13:37:33.014534922Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK"}
+{"Time":"2023-03-29T13:37:33.014543883Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":"=== CONT TestServer/TLSRedirect/OK\n"}
+{"Time":"2023-03-29T13:37:33.01571179Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectOK4195649967/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectOK4195649967/002 --http-address :0 --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSRedirectOK4195649967/001/1898764516 --tls-key-file /tmp/TestServerTLSRedirectOK4195649967/001/3656697468 --wildcard-access-url *.example.com --access-url https://example.com --redirect-to-access-url\n"}
+{"Time":"2023-03-29T13:37:33.015887089Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.015: cmd: \" psql \\\"postgres://coder@localhost:43265/coder?sslmode=disable\u0026password=qZX0YVm9trLHmHzY\\\" \"\n"}
+{"Time":"2023-03-29T13:37:33.015903823Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":" ptytest.go:102: 2023-03-29 13:37:33.015: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:33.016045291Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Output":"--- PASS: TestServer/BuiltinPostgresURL (0.11s)\n"}
+{"Time":"2023-03-29T13:37:33.01632566Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgresURL","Elapsed":0.11}
+{"Time":"2023-03-29T13:37:33.016345189Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.022419375Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:33.099478164Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:33.138846249Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.137: cmd: \"Started TLS/HTTPS listener at https://[::]:46747\"\n"}
+{"Time":"2023-03-29T13:37:33.138887737Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 1...\n"}
+{"Time":"2023-03-29T13:37:33.138894754Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 1\n"}
+{"Time":"2023-03-29T13:37:33.138900199Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 2...\n"}
+{"Time":"2023-03-29T13:37:33.138905607Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 2\n"}
+{"Time":"2023-03-29T13:37:33.138911333Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:33.141820899Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.140 [DEBUG]\t(coderd.metrics_cache)\t\u003cgithub.com/coder/coder/v2/coderd/metricscache/metricscache.go:272\u003e\t(*Cache).run\tdeployment stats metrics refreshed\t{\"took\": \"23.786µs\", \"interval\": \"30s\"}\n"}
+{"Time":"2023-03-29T13:37:33.141862791Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.141 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\t(*Server).connect\tconnected\n"}
+{"Time":"2023-03-29T13:37:33.141871378Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.141 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\t(*Server).connect\tconnected\n"}
+{"Time":"2023-03-29T13:37:33.14187713Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 1...\n"}
+{"Time":"2023-03-29T13:37:33.141883292Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.141 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\t(*Server).closeWithError\tclosing server with error\t{\"error\": null}\n"}
+{"Time":"2023-03-29T13:37:33.141893432Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 1\n"}
+{"Time":"2023-03-29T13:37:33.141898548Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 2...\n"}
+{"Time":"2023-03-29T13:37:33.141959656Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stderr: 2023-03-29 13:37:33.141 [DEBUG]\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\t(*Server).closeWithError\tclosing server with error\t{\"error\": null}\n"}
+{"Time":"2023-03-29T13:37:33.142091685Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 2\n"}
+{"Time":"2023-03-29T13:37:33.142184362Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:33.142408919Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stderr: \u001b[1;mWARN: \u001b[0mThe access URL \u001b[;mhttp://example.com\u001b[0m could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n"}
+{"Time":"2023-03-29T13:37:33.142501883Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:33.147218812Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:33.147251166Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:33.14725864Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:33.147298554Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:33.147355456Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 3...\n"}
+{"Time":"2023-03-29T13:37:33.14745485Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 3\n"}
+{"Time":"2023-03-29T13:37:33.148482198Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey"}
+{"Time":"2023-03-29T13:37:33.148502985Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":"=== CONT TestServer/TLSInvalid/MismatchedCertAndKey\n"}
+{"Time":"2023-03-29T13:37:33.149452866Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSInvalidMismatchedCertAndKey978167281/002 server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidMismatchedCertAndKey978167281/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081 --tls-key-file /tmp/TestServerTLSInvalid1610620518/002/2775674587\n"}
+{"Time":"2023-03-29T13:37:33.149984351Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:38873\n"}
+{"Time":"2023-03-29T13:37:33.150363114Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":" server_test.go:344: args: [server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidMismatchedCertAndKey978167281/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081 --tls-key-file /tmp/TestServerTLSInvalid1610620518/002/2775674587]\n"}
+{"Time":"2023-03-29T13:37:33.150581402Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Output":"--- PASS: TestServer/TLSInvalid/MismatchedCertAndKey (0.00s)\n"}
+{"Time":"2023-03-29T13:37:33.15059771Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCertAndKey","Elapsed":0}
+{"Time":"2023-03-29T13:37:33.150605526Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount"}
+{"Time":"2023-03-29T13:37:33.150609996Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":"=== CONT TestServer/TLSInvalid/MismatchedCount\n"}
+{"Time":"2023-03-29T13:37:33.151854633Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSInvalidMismatchedCount803880522/002 server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidMismatchedCount803880522/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081 --tls-key-file /tmp/TestServerTLSInvalid1610620518/001/1816833089 --tls-cert-file /tmp/TestServerTLSInvalid1610620518/002/3269350839\n"}
+{"Time":"2023-03-29T13:37:33.152410162Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:37839\n"}
+{"Time":"2023-03-29T13:37:33.15252017Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":" server_test.go:344: args: [server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidMismatchedCount803880522/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081 --tls-key-file /tmp/TestServerTLSInvalid1610620518/001/1816833089 --tls-cert-file /tmp/TestServerTLSInvalid1610620518/002/3269350839]\n"}
+{"Time":"2023-03-29T13:37:33.152732054Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Output":"--- PASS: TestServer/TLSInvalid/MismatchedCount (0.00s)\n"}
+{"Time":"2023-03-29T13:37:33.152746399Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/MismatchedCount","Elapsed":0}
+{"Time":"2023-03-29T13:37:33.152754347Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey"}
+{"Time":"2023-03-29T13:37:33.152758935Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey","Output":"=== CONT TestServer/TLSInvalid/NoKey\n"}
+{"Time":"2023-03-29T13:37:33.153794977Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSInvalidNoKey281486761/002 server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidNoKey281486761/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081\n"}
+{"Time":"2023-03-29T13:37:33.154207106Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:43607\n"}
+{"Time":"2023-03-29T13:37:33.15427222Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey","Output":" server_test.go:344: args: [server --in-memory --http-address :0 --access-url http://example.com --cache-dir /tmp/TestServerTLSInvalidNoKey281486761/001 --tls-enable --tls-cert-file /tmp/TestServerTLSInvalid1610620518/001/3088514081]\n"}
+{"Time":"2023-03-29T13:37:33.154468443Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey","Output":"--- PASS: TestServer/TLSInvalid/NoKey (0.00s)\n"}
+{"Time":"2023-03-29T13:37:33.154623549Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid/NoKey","Elapsed":0}
+{"Time":"2023-03-29T13:37:33.154633176Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid","Output":"--- PASS: TestServer/TLSInvalid (0.00s)\n"}
+{"Time":"2023-03-29T13:37:33.154761857Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSInvalid","Elapsed":0}
+{"Time":"2023-03-29T13:37:33.154770493Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.154: cmd: \"WARN: The access URL http://example.com could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
+{"Time":"2023-03-29T13:37:33.439719632Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.43976961Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.439791036Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"View the Web UI: http://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.439807711Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.43982432Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.439851264Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:33.439869598Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:33.439886994Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.439905518Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:33.439924649Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.439: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.439941905Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:83: 2023-03-29 13:37:33.439: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:33.4399588Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:74: 2023-03-29 13:37:33.439: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:33.440230934Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.440: cmd: \"Started HTTP listener at http://[::]:45645\"\n"}
+{"Time":"2023-03-29T13:37:33.440246712Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.440: cmd: \"Started TLS/HTTPS listener at https://[::]:33779\"\n"}
+{"Time":"2023-03-29T13:37:33.440251896Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:634: 2023-03-29 13:37:33.440: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
+{"Time":"2023-03-29T13:37:33.440278535Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:635: 2023-03-29 13:37:33.440: cmd: ReadLine ctx has no deadline, using 10s\n"}
+{"Time":"2023-03-29T13:37:33.440295931Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:635: 2023-03-29 13:37:33.440: cmd: matched newline = \" http://[::]:45645\"\n"}
+{"Time":"2023-03-29T13:37:33.440327613Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:641: 2023-03-29 13:37:33.440: cmd: matched \"Started TLS/HTTPS listener at\" = \"Started TLS/HTTPS listener at\"\n"}
+{"Time":"2023-03-29T13:37:33.440339171Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:642: 2023-03-29 13:37:33.440: cmd: ReadLine ctx has no deadline, using 10s\n"}
+{"Time":"2023-03-29T13:37:33.440353544Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" server_test.go:642: 2023-03-29 13:37:33.440: cmd: matched newline = \" https://[::]:33779\"\n"}
+{"Time":"2023-03-29T13:37:33.462318671Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.155: cmd: \"WARN: Redirect HTTP to HTTPS is deprecated, please use Redirect to Access URL instead.\"\n"}
+{"Time":"2023-03-29T13:37:33.462349606Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.462356521Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"Started HTTP listener at http://[::]:38281\"\n"}
+{"Time":"2023-03-29T13:37:33.462372929Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"WARN: --tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.462395287Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"Started TLS/HTTPS listener at https://[::]:37895\"\n"}
+{"Time":"2023-03-29T13:37:33.46242925Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.462438148Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"View the Web UI: https://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.462469497Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.462484536Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.462637037Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:76: 2023-03-29 13:37:33.462: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.462649797Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:33.462659463Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.462670357Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:33.462694313Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:121: 2023-03-29 13:37:33.462: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.462718576Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:110: 2023-03-29 13:37:33.462: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.46272687Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:111: 2023-03-29 13:37:33.462: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:33.462736639Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:113: 2023-03-29 13:37:33.462: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.462758116Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:74: 2023-03-29 13:37:33.462: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:33.462768617Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:76: 2023-03-29 13:37:33.462: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.462788026Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:74: 2023-03-29 13:37:33.462: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:33.462795774Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:76: 2023-03-29 13:37:33.462: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.462816156Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":" ptytest.go:102: 2023-03-29 13:37:33.462: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:33.462931778Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Output":"--- PASS: TestServer/CanListenUnspecifiedv4 (0.60s)\n"}
+{"Time":"2023-03-29T13:37:33.462940935Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv4","Elapsed":0.6}
+{"Time":"2023-03-29T13:37:33.462948149Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener"}
+{"Time":"2023-03-29T13:37:33.462952114Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":"=== CONT TestServer/TLSRedirect/NoHTTPListener\n"}
+{"Time":"2023-03-29T13:37:33.464080755Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectNoHTTPListener857312755/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectNoHTTPListener857312755/002 --http-address --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSRedirectNoHTTPListener857312755/001/774178655 --tls-key-file /tmp/TestServerTLSRedirectNoHTTPListener857312755/001/2109956146 --wildcard-access-url *.example.com --access-url https://example.com\n"}
+{"Time":"2023-03-29T13:37:33.464227163Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.464: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.464240384Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.464: cmd: \"View the Web UI: https://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.464252515Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.464: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.46428589Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:33.464: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.472563896Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.155: cmd: \"WARN: The access URL http://example.com could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
+{"Time":"2023-03-29T13:37:33.472612391Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.47263698Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.472648847Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"View the Web UI: http://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.472703374Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.472714667Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.473028818Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:33.473041801Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:33.473048085Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.473053454Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:33.47305943Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.473064898Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:33.473070019Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.473075028Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:33.473080237Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:121: 2023-03-29 13:37:33.472: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.47308794Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:488: 2023-03-29 13:37:33.472: cmd: matched \"Started HTTP listener at\" = \"WARN: Redirect HTTP to HTTPS is deprecated, please use Redirect to Access URL instead.\\r\\n \\r\\r\\nStarted HTTP listener at\"\n"}
+{"Time":"2023-03-29T13:37:33.473094939Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:489: 2023-03-29 13:37:33.472: cmd: ReadLine ctx has no deadline, using 10s\n"}
+{"Time":"2023-03-29T13:37:33.473099786Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:489: 2023-03-29 13:37:33.472: cmd: matched newline = \" http://[::]:38281\"\n"}
+{"Time":"2023-03-29T13:37:33.473628418Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:493: 2023-03-29 13:37:33.473: cmd: matched \"Started TLS/HTTPS listener at \" = \"WARN: --tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead\\r\\r\\nStarted TLS/HTTPS listener at \"\n"}
+{"Time":"2023-03-29T13:37:33.473644808Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:494: 2023-03-29 13:37:33.473: cmd: ReadLine ctx has no deadline, using 10s\n"}
+{"Time":"2023-03-29T13:37:33.47365119Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" server_test.go:494: 2023-03-29 13:37:33.473: cmd: matched newline = \"https://[::]:37895\"\n"}
+{"Time":"2023-03-29T13:37:33.475250161Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:33.475281933Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:33.490892952Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:33.492145943Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:83: 2023-03-29 13:37:33.491: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:33.492183748Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:74: 2023-03-29 13:37:33.491: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:33.492192673Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:110: 2023-03-29 13:37:33.492: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.492199945Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:111: 2023-03-29 13:37:33.492: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:33.492206118Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:113: 2023-03-29 13:37:33.492: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.492296422Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:76: 2023-03-29 13:37:33.492: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.492316694Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:74: 2023-03-29 13:37:33.492: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:33.49232562Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:76: 2023-03-29 13:37:33.492: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.492355439Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:74: 2023-03-29 13:37:33.492: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:33.492364574Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:76: 2023-03-29 13:37:33.492: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.49240481Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":" ptytest.go:102: 2023-03-29 13:37:33.492: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:33.495511552Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Output":"--- PASS: TestServer/CanListenUnspecifiedv6 (0.63s)\n"}
+{"Time":"2023-03-29T13:37:33.495557907Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/CanListenUnspecifiedv6","Elapsed":0.63}
+{"Time":"2023-03-29T13:37:33.495569713Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener"}
+{"Time":"2023-03-29T13:37:33.495575692Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":"=== CONT TestServer/TLSRedirect/NoTLSListener\n"}
+{"Time":"2023-03-29T13:37:33.495582515Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectNoTLSListener2050465310/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectNoTLSListener2050465310/002 --http-address :0 --access-url https://example.com\n"}
+{"Time":"2023-03-29T13:37:33.514163923Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:33.51646695Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: View the Web UI: https://example.com\n"}
+{"Time":"2023-03-29T13:37:33.532093856Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.532820236Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:33.533254024Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.533887464Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Output":"--- PASS: TestServer/Logging/CreatesFile (0.69s)\n"}
+{"Time":"2023-03-29T13:37:33.580391994Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/CreatesFile","Elapsed":0.69}
+{"Time":"2023-03-29T13:37:33.580437708Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.194: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:33.580458446Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:33.580471806Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.58048911Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:33.580504524Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.580522972Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:33.5805328Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.580555757Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:33.580587561Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.580: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.580846451Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Output":"--- PASS: TestServer/Logging/JSON (0.67s)\n"}
+{"Time":"2023-03-29T13:37:33.580862096Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/JSON","Elapsed":0.67}
+{"Time":"2023-03-29T13:37:33.580868926Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect"}
+{"Time":"2023-03-29T13:37:33.580873653Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":"=== CONT TestServer/TLSRedirect/NoRedirect\n"}
+{"Time":"2023-03-29T13:37:33.582048889Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectNoRedirect2668740580/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectNoRedirect2668740580/002 --http-address :0 --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSRedirectNoRedirect2668740580/001/1517171254 --tls-key-file /tmp/TestServerTLSRedirectNoRedirect2668740580/001/1491051903 --wildcard-access-url *.example.com --access-url https://example.com\n"}
+{"Time":"2023-03-29T13:37:33.582187252Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard"}
+{"Time":"2023-03-29T13:37:33.582197387Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":"=== CONT TestServer/TLSRedirect/NoRedirectWithWildcard\n"}
+{"Time":"2023-03-29T13:37:33.583144917Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerTLSRedirectNoRedirectWithWildcard3241718280/003 server --in-memory --cache-dir /tmp/TestServerTLSRedirectNoRedirectWithWildcard3241718280/002 --http-address --tls-enable --tls-address :0 --tls-cert-file /tmp/TestServerTLSRedirectNoRedirectWithWildcard3241718280/001/2297363344 --tls-key-file /tmp/TestServerTLSRedirectNoRedirectWithWildcard3241718280/001/3868867528 --wildcard-access-url *.example.com --access-url https://example.com --redirect-to-access-url\n"}
+{"Time":"2023-03-29T13:37:33.609797366Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Output":"--- PASS: TestServer/TracerNoLeak (0.75s)\n"}
+{"Time":"2023-03-29T13:37:33.610858133Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TracerNoLeak","Elapsed":0.75}
+{"Time":"2023-03-29T13:37:33.6108818Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.202: cmd: \"WARN: Address is deprecated, please use HTTP Address and TLS Address instead.\"\n"}
+{"Time":"2023-03-29T13:37:33.610892881Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.610900074Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \"Started HTTP listener at http://[::]:32777\"\n"}
+{"Time":"2023-03-29T13:37:33.610935322Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.610944309Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \"View the Web UI: http://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.610972945Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.610991889Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.610: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.61104088Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:83: 2023-03-29 13:37:33.611: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:33.611050128Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.611: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:33.611098802Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:110: 2023-03-29 13:37:33.611: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.611114878Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:111: 2023-03-29 13:37:33.611: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:33.611120793Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:113: 2023-03-29 13:37:33.611: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.611180989Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.611: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.611193294Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.611: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:33.611200866Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.611: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.611204553Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.611: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:33.611207462Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.611: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.611215565Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":" ptytest.go:102: 2023-03-29 13:37:33.611: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:33.611336071Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Output":"--- PASS: TestServer/LocalAccessURL (0.71s)\n"}
+{"Time":"2023-03-29T13:37:33.613044416Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/LocalAccessURL","Elapsed":0.71}
+{"Time":"2023-03-29T13:37:33.613054355Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 1...\n"}
+{"Time":"2023-03-29T13:37:33.613144221Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 1\n"}
+{"Time":"2023-03-29T13:37:33.613176406Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" server_test.go:799: 2023-03-29 13:37:33.613: cmd: matched \"is deprecated\" = \"WARN: Address is deprecated\"\n"}
+{"Time":"2023-03-29T13:37:33.613666866Z","Action":"cont","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS"}
+{"Time":"2023-03-29T13:37:33.613682333Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":"=== CONT TestServer/DeprecatedAddress/TLS\n"}
+{"Time":"2023-03-29T13:37:33.614665673Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" clitest.go:67: invoking command: coder --global-config /tmp/TestServerDeprecatedAddressTLS1194868532/003 server --in-memory --address :0 --access-url https://example.com --tls-enable --tls-cert-file /tmp/TestServerDeprecatedAddressTLS1194868532/001/3357512070 --tls-key-file /tmp/TestServerDeprecatedAddressTLS1194868532/001/2886385591 --cache-dir /tmp/TestServerDeprecatedAddressTLS1194868532/002\n"}
+{"Time":"2023-03-29T13:37:33.614774341Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Shutting down provisioner daemon 2...\n"}
+{"Time":"2023-03-29T13:37:33.614835489Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Gracefully shut down provisioner daemon 2\n"}
+{"Time":"2023-03-29T13:37:33.614847064Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:33.615151651Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.251: cmd: \"Started HTTP listener at http://[::]:46789\"\n"}
+{"Time":"2023-03-29T13:37:33.615187666Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \"WARN: The access URL https://foobarbaz.mydomain could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
+{"Time":"2023-03-29T13:37:33.615200461Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.615214043Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.615230151Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \"View the Web UI: https://foobarbaz.mydomain\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.615243576Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.61525094Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.615: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.616015109Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.617106937Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.251: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.617129585Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \"View the Web UI: https://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.617140564Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.617159456Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.617293213Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" server_test.go:219: 2023-03-29 13:37:33.617: cmd: matched \"this may cause unexpected problems when creating workspaces\" = \"Started HTTP listener at http://[::]:46789\\r\\nWARN: The access URL https://foobarbaz.mydomain could not be resolved, this may cause unexpected problems when creating workspaces\"\n"}
+{"Time":"2023-03-29T13:37:33.617419153Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" server_test.go:220: 2023-03-29 13:37:33.617: cmd: matched \"View the Web UI: https://foobarbaz.mydomain\" = \". Generate a unique *.try.coder.app URL by not specifying an access URL.\\r\\n \\r\\r\\n \\r\\nView the Web UI: https://foobarbaz.mydomain\"\n"}
+{"Time":"2023-03-29T13:37:33.617702539Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Output":"--- PASS: TestServer/Logging/Human (0.70s)\n"}
+{"Time":"2023-03-29T13:37:33.617717314Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Human","Elapsed":0.7}
+{"Time":"2023-03-29T13:37:33.617728397Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.346: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.617738582Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \"View the Web UI: https://google.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.617745792Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.617755568Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.617: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.617776141Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" server_test.go:238: 2023-03-29 13:37:33.617: cmd: matched \"View the Web UI: https://google.com\" = \"Started HTTP listener at http://[::]:39671\\r\\n \\r\\nView the Web UI: https://google.com\"\n"}
+{"Time":"2023-03-29T13:37:33.618700109Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:83: 2023-03-29 13:37:33.618: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:33.618717448Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.618: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:33.618729015Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:110: 2023-03-29 13:37:33.618: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.618738942Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:111: 2023-03-29 13:37:33.618: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:33.618748331Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:113: 2023-03-29 13:37:33.618: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.618809556Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.618: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.618823389Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.618: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:33.618833326Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.618: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.618840112Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.618: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:33.61884905Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.618: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.618860551Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":" ptytest.go:102: 2023-03-29 13:37:33.618: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:33.618993236Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Output":"--- PASS: TestServer/NoWarningWithRemoteAccessURL (0.72s)\n"}
+{"Time":"2023-03-29T13:37:33.620193889Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/NoWarningWithRemoteAccessURL","Elapsed":0.72}
+{"Time":"2023-03-29T13:37:33.620216262Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:33.620558208Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.620: cmd: \"Started TLS/HTTPS listener at https://[::]:40599\"\n"}
+{"Time":"2023-03-29T13:37:33.62057844Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.620: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.620586258Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.620: cmd: \"View the Web UI: https://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.620613734Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" server_test.go:641: 2023-03-29 13:37:33.620: cmd: matched \"Started TLS/HTTPS listener at\" = \"Started TLS/HTTPS listener at\"\n"}
+{"Time":"2023-03-29T13:37:33.620652863Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" server_test.go:642: 2023-03-29 13:37:33.620: cmd: ReadLine ctx has no deadline, using 10s\n"}
+{"Time":"2023-03-29T13:37:33.620669386Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" server_test.go:642: 2023-03-29 13:37:33.620: cmd: matched newline = \" https://[::]:40599\"\n"}
+{"Time":"2023-03-29T13:37:33.623941782Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:33.634109465Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.634: cmd: \"Started HTTP listener at http://[::]:44615\"\n"}
+{"Time":"2023-03-29T13:37:33.6341392Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" server_test.go:634: 2023-03-29 13:37:33.634: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
+{"Time":"2023-03-29T13:37:33.634159059Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" server_test.go:635: 2023-03-29 13:37:33.634: cmd: ReadLine ctx has no deadline, using 10s\n"}
+{"Time":"2023-03-29T13:37:33.634168084Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" server_test.go:635: 2023-03-29 13:37:33.634: cmd: matched newline = \" http://[::]:44615\"\n"}
+{"Time":"2023-03-29T13:37:33.639592274Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.639: cmd: \"WARN: Address is deprecated, please use HTTP Address and TLS Address instead.\"\n"}
+{"Time":"2023-03-29T13:37:33.639622715Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.639: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.639633384Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.639: cmd: \"Started TLS/HTTPS listener at https://[::]:44869\"\n"}
+{"Time":"2023-03-29T13:37:33.642427153Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.642: cmd: \"Started TLS/HTTPS listener at https://[::]:43889\"\n"}
+{"Time":"2023-03-29T13:37:33.642448651Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.642: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.642456925Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.642: cmd: \"View the Web UI: https://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.642468331Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" server_test.go:641: 2023-03-29 13:37:33.642: cmd: matched \"Started TLS/HTTPS listener at\" = \"Started TLS/HTTPS listener at\"\n"}
+{"Time":"2023-03-29T13:37:33.642477804Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" server_test.go:642: 2023-03-29 13:37:33.642: cmd: ReadLine ctx has no deadline, using 10s\n"}
+{"Time":"2023-03-29T13:37:33.642487898Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" server_test.go:642: 2023-03-29 13:37:33.642: cmd: matched newline = \" https://[::]:43889\"\n"}
+{"Time":"2023-03-29T13:37:33.646526946Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.646: cmd: \"Started HTTP listener at http://[::]:33323\"\n"}
+{"Time":"2023-03-29T13:37:33.646552176Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.646: cmd: \"Started TLS/HTTPS listener at https://[::]:36951\"\n"}
+{"Time":"2023-03-29T13:37:33.64656429Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.646: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.646572476Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.646: cmd: \"View the Web UI: https://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.646580011Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:634: 2023-03-29 13:37:33.646: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
+{"Time":"2023-03-29T13:37:33.646591361Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:635: 2023-03-29 13:37:33.646: cmd: ReadLine ctx has no deadline, using 10s\n"}
+{"Time":"2023-03-29T13:37:33.646616475Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:635: 2023-03-29 13:37:33.646: cmd: matched newline = \" http://[::]:33323\"\n"}
+{"Time":"2023-03-29T13:37:33.646662584Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:641: 2023-03-29 13:37:33.646: cmd: matched \"Started TLS/HTTPS listener at\" = \"Started TLS/HTTPS listener at\"\n"}
+{"Time":"2023-03-29T13:37:33.646681881Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:642: 2023-03-29 13:37:33.646: cmd: ReadLine ctx has no deadline, using 10s\n"}
+{"Time":"2023-03-29T13:37:33.646699362Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" server_test.go:642: 2023-03-29 13:37:33.646: cmd: matched newline = \" https://[::]:36951\"\n"}
+{"Time":"2023-03-29T13:37:33.648933788Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_api_active_users_duration_hour The number of users that have been active within the last hour.\n"}
+{"Time":"2023-03-29T13:37:33.648967031Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_api_active_users_duration_hour gauge\n"}
+{"Time":"2023-03-29T13:37:33.648980231Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_api_concurrent_requests The number of concurrent API requests.\n"}
+{"Time":"2023-03-29T13:37:33.649053919Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_api_concurrent_requests gauge\n"}
+{"Time":"2023-03-29T13:37:33.649069592Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_api_concurrent_requests 0\n"}
+{"Time":"2023-03-29T13:37:33.649078578Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_api_concurrent_websockets The total number of concurrent API websockets.\n"}
+{"Time":"2023-03-29T13:37:33.64908451Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_api_concurrent_websockets gauge\n"}
+{"Time":"2023-03-29T13:37:33.64908947Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_api_concurrent_websockets 0\n"}
+{"Time":"2023-03-29T13:37:33.649096688Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_api_workspace_latest_build_total The latest workspace builds with a status.\n"}
+{"Time":"2023-03-29T13:37:33.649103792Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_api_workspace_latest_build_total gauge\n"}
+{"Time":"2023-03-29T13:37:33.649110998Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_authz_authorize_duration_seconds Duration of the 'Authorize' call in seconds. Only counts calls that succeed.\n"}
+{"Time":"2023-03-29T13:37:33.649125082Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_authz_authorize_duration_seconds histogram\n"}
+{"Time":"2023-03-29T13:37:33.649136372Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.0005\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649158836Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.001\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649167411Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.002\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649174438Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.003\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649184378Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.005\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649205265Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.01\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649213251Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.02\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649222908Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.035\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649235415Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.05\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649253778Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.075\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649261752Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.1\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649271274Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.25\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649292314Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"0.75\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649300333Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"1\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649310057Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_bucket{allowed=\"true\",le=\"+Inf\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649320729Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_sum{allowed=\"true\"} 0.002876051\n"}
+{"Time":"2023-03-29T13:37:33.649345041Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_authorize_duration_seconds_count{allowed=\"true\"} 2\n"}
+{"Time":"2023-03-29T13:37:33.649356377Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP coderd_authz_prepare_authorize_duration_seconds Duration of the 'PrepareAuthorize' call in seconds.\n"}
+{"Time":"2023-03-29T13:37:33.649363746Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE coderd_authz_prepare_authorize_duration_seconds histogram\n"}
+{"Time":"2023-03-29T13:37:33.649370497Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.0005\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649391953Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.001\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649399779Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.002\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649410888Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.003\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649428841Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.005\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649438526Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.01\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649445746Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.02\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.64947999Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.035\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649486523Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.05\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649643599Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.075\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649652811Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.1\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649659776Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.25\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649677202Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"0.75\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.649684812Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"1\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.64970109Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_bucket{le=\"+Inf\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.64971819Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_sum 0\n"}
+{"Time":"2023-03-29T13:37:33.649725513Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned coderd_authz_prepare_authorize_duration_seconds_count 0\n"}
+{"Time":"2023-03-29T13:37:33.649741363Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"}
+{"Time":"2023-03-29T13:37:33.649751815Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_gc_duration_seconds summary\n"}
+{"Time":"2023-03-29T13:37:33.649770366Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"0\"} 1.6651e-05\n"}
+{"Time":"2023-03-29T13:37:33.649779436Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"0.25\"} 3.0073e-05\n"}
+{"Time":"2023-03-29T13:37:33.649795479Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"0.5\"} 3.3851e-05\n"}
+{"Time":"2023-03-29T13:37:33.649803042Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"0.75\"} 5.0874e-05\n"}
+{"Time":"2023-03-29T13:37:33.649822842Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds{quantile=\"1\"} 0.000164674\n"}
+{"Time":"2023-03-29T13:37:33.649830516Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds_sum 0.000461803\n"}
+{"Time":"2023-03-29T13:37:33.64984149Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_gc_duration_seconds_count 9\n"}
+{"Time":"2023-03-29T13:37:33.649859074Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_goroutines Number of goroutines that currently exist.\n"}
+{"Time":"2023-03-29T13:37:33.649876364Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_goroutines gauge\n"}
+{"Time":"2023-03-29T13:37:33.649883909Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_goroutines 400\n"}
+{"Time":"2023-03-29T13:37:33.649893852Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_info Information about the Go environment.\n"}
+{"Time":"2023-03-29T13:37:33.649915539Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_info gauge\n"}
+{"Time":"2023-03-29T13:37:33.649923065Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_info{version=\"go1.20\"} 1\n"}
+{"Time":"2023-03-29T13:37:33.649934513Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.\n"}
+{"Time":"2023-03-29T13:37:33.64994537Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_alloc_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.649964236Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_alloc_bytes 4.4139992e+07\n"}
+{"Time":"2023-03-29T13:37:33.649974501Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.\n"}
+{"Time":"2023-03-29T13:37:33.649985399Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_alloc_bytes_total counter\n"}
+{"Time":"2023-03-29T13:37:33.650002195Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_alloc_bytes_total 1.07033304e+08\n"}
+{"Time":"2023-03-29T13:37:33.650019066Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.\n"}
+{"Time":"2023-03-29T13:37:33.650026662Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_buck_hash_sys_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650037208Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_buck_hash_sys_bytes 1.487772e+06\n"}
+{"Time":"2023-03-29T13:37:33.650056379Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_frees_total Total number of frees.\n"}
+{"Time":"2023-03-29T13:37:33.650063678Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_frees_total counter\n"}
+{"Time":"2023-03-29T13:37:33.650079718Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_frees_total 341416\n"}
+{"Time":"2023-03-29T13:37:33.650087192Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.\n"}
+{"Time":"2023-03-29T13:37:33.65010648Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_gc_sys_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650113893Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_gc_sys_bytes 9.376592e+06\n"}
+{"Time":"2023-03-29T13:37:33.650132561Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and still in use.\n"}
+{"Time":"2023-03-29T13:37:33.650140278Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_alloc_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650152649Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_alloc_bytes 4.4139992e+07\n"}
+{"Time":"2023-03-29T13:37:33.650314526Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used.\n"}
+{"Time":"2023-03-29T13:37:33.650323426Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_idle_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650332952Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_idle_bytes 4.481024e+06\n"}
+{"Time":"2023-03-29T13:37:33.65035073Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use.\n"}
+{"Time":"2023-03-29T13:37:33.650358078Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_inuse_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650374519Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_inuse_bytes 4.6473216e+07\n"}
+{"Time":"2023-03-29T13:37:33.650392405Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_objects Number of allocated objects.\n"}
+{"Time":"2023-03-29T13:37:33.650400167Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_objects gauge\n"}
+{"Time":"2023-03-29T13:37:33.650411Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_objects 198877\n"}
+{"Time":"2023-03-29T13:37:33.650427198Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_released_bytes Number of heap bytes released to OS.\n"}
+{"Time":"2023-03-29T13:37:33.650443875Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_released_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.6504512Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_released_bytes 172032\n"}
+{"Time":"2023-03-29T13:37:33.650466769Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system.\n"}
+{"Time":"2023-03-29T13:37:33.650476593Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_heap_sys_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650493555Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_heap_sys_bytes 5.095424e+07\n"}
+{"Time":"2023-03-29T13:37:33.650501064Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection.\n"}
+{"Time":"2023-03-29T13:37:33.650517983Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_last_gc_time_seconds gauge\n"}
+{"Time":"2023-03-29T13:37:33.650530998Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_last_gc_time_seconds 1.680097053302355e+09\n"}
+{"Time":"2023-03-29T13:37:33.65054228Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_lookups_total Total number of pointer lookups.\n"}
+{"Time":"2023-03-29T13:37:33.650552934Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_lookups_total counter\n"}
+{"Time":"2023-03-29T13:37:33.650563901Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_lookups_total 0\n"}
+{"Time":"2023-03-29T13:37:33.650584173Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mallocs_total Total number of mallocs.\n"}
+{"Time":"2023-03-29T13:37:33.650591833Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mallocs_total counter\n"}
+{"Time":"2023-03-29T13:37:33.650607986Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mallocs_total 540293\n"}
+{"Time":"2023-03-29T13:37:33.650615529Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures.\n"}
+{"Time":"2023-03-29T13:37:33.650641182Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mcache_inuse_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650649185Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mcache_inuse_bytes 1200\n"}
+{"Time":"2023-03-29T13:37:33.650655792Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system.\n"}
+{"Time":"2023-03-29T13:37:33.650675584Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mcache_sys_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650682964Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mcache_sys_bytes 15600\n"}
+{"Time":"2023-03-29T13:37:33.650699492Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures.\n"}
+{"Time":"2023-03-29T13:37:33.65070695Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mspan_inuse_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650725284Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mspan_inuse_bytes 415680\n"}
+{"Time":"2023-03-29T13:37:33.650732919Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system.\n"}
+{"Time":"2023-03-29T13:37:33.650742233Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_mspan_sys_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650753796Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_mspan_sys_bytes 440640\n"}
+{"Time":"2023-03-29T13:37:33.650773086Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place.\n"}
+{"Time":"2023-03-29T13:37:33.650780844Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_next_gc_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650791619Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_next_gc_bytes 5.744056e+07\n"}
+{"Time":"2023-03-29T13:37:33.650939729Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations.\n"}
+{"Time":"2023-03-29T13:37:33.650948462Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_other_sys_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650960125Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_other_sys_bytes 407532\n"}
+{"Time":"2023-03-29T13:37:33.65097098Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_stack_inuse_bytes Number of bytes in use by the stack allocator.\n"}
+{"Time":"2023-03-29T13:37:33.650989112Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_stack_inuse_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.650999328Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_stack_inuse_bytes 3.506176e+06\n"}
+{"Time":"2023-03-29T13:37:33.651015975Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator.\n"}
+{"Time":"2023-03-29T13:37:33.651023745Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_stack_sys_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.651042622Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_stack_sys_bytes 3.506176e+06\n"}
+{"Time":"2023-03-29T13:37:33.651050141Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_memstats_sys_bytes Number of bytes obtained from system.\n"}
+{"Time":"2023-03-29T13:37:33.651066559Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_memstats_sys_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.65107676Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_memstats_sys_bytes 6.6188552e+07\n"}
+{"Time":"2023-03-29T13:37:33.651096334Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP go_threads Number of OS threads created.\n"}
+{"Time":"2023-03-29T13:37:33.651104041Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE go_threads gauge\n"}
+{"Time":"2023-03-29T13:37:33.651114838Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned go_threads 11\n"}
+{"Time":"2023-03-29T13:37:33.651133818Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.\n"}
+{"Time":"2023-03-29T13:37:33.651141836Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_cpu_seconds_total counter\n"}
+{"Time":"2023-03-29T13:37:33.651152615Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_cpu_seconds_total 0.47\n"}
+{"Time":"2023-03-29T13:37:33.651169215Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_max_fds Maximum number of open file descriptors.\n"}
+{"Time":"2023-03-29T13:37:33.651188139Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_max_fds gauge\n"}
+{"Time":"2023-03-29T13:37:33.651196288Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_max_fds 1.048576e+06\n"}
+{"Time":"2023-03-29T13:37:33.651207299Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_open_fds Number of open file descriptors.\n"}
+{"Time":"2023-03-29T13:37:33.651218057Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_open_fds gauge\n"}
+{"Time":"2023-03-29T13:37:33.651236861Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_open_fds 144\n"}
+{"Time":"2023-03-29T13:37:33.651248494Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_resident_memory_bytes Resident memory size in bytes.\n"}
+{"Time":"2023-03-29T13:37:33.651257364Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_resident_memory_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.651280196Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_resident_memory_bytes 1.14364416e+08\n"}
+{"Time":"2023-03-29T13:37:33.65128834Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_start_time_seconds Start time of the process since unix epoch in seconds.\n"}
+{"Time":"2023-03-29T13:37:33.651296824Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_start_time_seconds gauge\n"}
+{"Time":"2023-03-29T13:37:33.651307674Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_start_time_seconds 1.68009705209e+09\n"}
+{"Time":"2023-03-29T13:37:33.651326547Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_virtual_memory_bytes Virtual memory size in bytes.\n"}
+{"Time":"2023-03-29T13:37:33.651334348Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_virtual_memory_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.651345305Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_virtual_memory_bytes 1.4835712e+09\n"}
+{"Time":"2023-03-29T13:37:33.651361504Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP process_virtual_memory_max_bytes Maximum amount of virtual memory available in bytes.\n"}
+{"Time":"2023-03-29T13:37:33.651378854Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE process_virtual_memory_max_bytes gauge\n"}
+{"Time":"2023-03-29T13:37:33.651386339Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned process_virtual_memory_max_bytes 1.8446744073709552e+19\n"}
+{"Time":"2023-03-29T13:37:33.651397458Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.\n"}
+{"Time":"2023-03-29T13:37:33.65149956Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE promhttp_metric_handler_requests_in_flight gauge\n"}
+{"Time":"2023-03-29T13:37:33.65150712Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned promhttp_metric_handler_requests_in_flight 1\n"}
+{"Time":"2023-03-29T13:37:33.651512875Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.\n"}
+{"Time":"2023-03-29T13:37:33.651517405Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned # TYPE promhttp_metric_handler_requests_total counter\n"}
+{"Time":"2023-03-29T13:37:33.651608377Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned promhttp_metric_handler_requests_total{code=\"200\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.651617771Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned promhttp_metric_handler_requests_total{code=\"500\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.651624722Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" server_test.go:981: scanned promhttp_metric_handler_requests_total{code=\"503\"} 0\n"}
+{"Time":"2023-03-29T13:37:33.653302095Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:33.687811194Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.687: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.68784324Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.687: cmd: \"View the Web UI: https://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.687847743Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.687: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.687859388Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.687: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.69314768Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" server_test.go:829: 2023-03-29 13:37:33.690: cmd: matched \"is deprecated\" = \"WARN: Address is deprecated\"\n"}
+{"Time":"2023-03-29T13:37:33.699837388Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:33.700125946Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:33.700416667Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.701833349Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Output":"--- PASS: TestServer/RateLimit/Default (0.85s)\n"}
+{"Time":"2023-03-29T13:37:33.792395824Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Default","Elapsed":0.85}
+{"Time":"2023-03-29T13:37:33.792432021Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:33.792443805Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:33.796165422Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:83: 2023-03-29 13:37:33.795: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:33.796198633Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.796: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:33.797173408Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.797427041Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:33.79757722Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:33.797913033Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:33.797928952Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:33.803483718Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.803533703Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:33.803541349Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:33.803549183Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.80355496Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:33.803560722Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.803565907Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:33.803571719Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.803577128Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:33.803582995Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:121: 2023-03-29 13:37:33.799: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.803588238Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:110: 2023-03-29 13:37:33.799: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.803593211Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:111: 2023-03-29 13:37:33.799: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:33.803598107Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:113: 2023-03-29 13:37:33.799: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.807382663Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.807: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.807425699Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:33.807: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.812449918Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.812: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.812479278Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:33.812: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.81249279Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.812: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.812540743Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.812: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.81937956Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.819: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.819501503Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.819: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:33.819519256Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.819: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.819527309Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:74: 2023-03-29 13:37:33.819: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:33.819534022Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:76: 2023-03-29 13:37:33.819: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:33.819605916Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":" ptytest.go:102: 2023-03-29 13:37:33.819: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:33.819682321Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Output":"--- PASS: TestServer/RemoteAccessURL (0.92s)\n"}
+{"Time":"2023-03-29T13:37:33.819757496Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RemoteAccessURL","Elapsed":0.92}
+{"Time":"2023-03-29T13:37:33.819772079Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.819: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.819781749Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.819: cmd: \"View the Web UI: https://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.819797518Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.819: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:33.819813792Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:33.819: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:33.851914311Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:33.85194979Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:33.852148613Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Output":"--- PASS: TestServer/Prometheus (1.00s)\n"}
+{"Time":"2023-03-29T13:37:33.855235903Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Prometheus","Elapsed":1}
+{"Time":"2023-03-29T13:37:33.855263447Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:33.855284404Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:33.855351311Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.85648797Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Output":"--- PASS: TestServer/GitHubOAuth (1.01s)\n"}
+{"Time":"2023-03-29T13:37:33.898898347Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/GitHubOAuth","Elapsed":1.01}
+{"Time":"2023-03-29T13:37:33.898943644Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:33.898966965Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:33.899910542Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:33.899951915Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:33.899979591Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:33.931576217Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:33.931632315Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.932359012Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Output":"--- PASS: TestServer/RateLimit/Disabled (1.02s)\n"}
+{"Time":"2023-03-29T13:37:33.932605839Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Disabled","Elapsed":1.02}
+{"Time":"2023-03-29T13:37:33.932638521Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:33.944423479Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:33.944456273Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:33.944471028Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.944486505Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:33.944506728Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.944: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.948894392Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:33.949085454Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Output":"--- PASS: TestServer/Telemetry (1.09s)\n"}
+{"Time":"2023-03-29T13:37:33.973592223Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Telemetry","Elapsed":1.09}
+{"Time":"2023-03-29T13:37:33.973627583Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:33.982109361Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Output":"--- PASS: TestServer/TLSValid (1.09s)\n"}
+{"Time":"2023-03-29T13:37:33.982145968Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValid","Elapsed":1.09}
+{"Time":"2023-03-29T13:37:33.982166462Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:83: 2023-03-29 13:37:33.982: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:33.982185903Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:74: 2023-03-29 13:37:33.982: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:33.983908041Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.983: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:33.983932298Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.983: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.983950164Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.983: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:33.983971383Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:121: 2023-03-29 13:37:33.983: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.983987252Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:110: 2023-03-29 13:37:33.983: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.984001811Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:111: 2023-03-29 13:37:33.983: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:33.984029981Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:113: 2023-03-29 13:37:33.983: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:33.999767444Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:33.999792066Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:33.999806103Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:33.999825199Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:33.999838052Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:33.999: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.000132942Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:34.000152725Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:34.013522575Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.013: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.013539719Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.013: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:34.013545396Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.013: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.013549166Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.013: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:34.013552799Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.013: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.013557098Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":" ptytest.go:102: 2023-03-29 13:37:34.013: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:34.013685528Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Output":"--- PASS: TestServer/DeprecatedAddress/HTTP (1.01s)\n"}
+{"Time":"2023-03-29T13:37:34.024907745Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/HTTP","Elapsed":1.01}
+{"Time":"2023-03-29T13:37:34.024925201Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:83: 2023-03-29 13:37:34.024: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:34.024929754Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:74: 2023-03-29 13:37:34.024: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:34.037436741Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:34.046649783Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:34.046: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:34.04666228Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:34.046: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.046671065Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:34.046: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:34.046674499Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:121: 2023-03-29 13:37:34.046: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.046678741Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:110: 2023-03-29 13:37:34.046: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.046682976Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:111: 2023-03-29 13:37:34.046: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:34.046687631Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:113: 2023-03-29 13:37:34.046: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.048234545Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:34.048465929Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:76: 2023-03-29 13:37:34.048: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.048473751Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:74: 2023-03-29 13:37:34.048: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:34.04848652Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:76: 2023-03-29 13:37:34.048: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.048494431Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:74: 2023-03-29 13:37:34.048: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:34.0485018Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:76: 2023-03-29 13:37:34.048: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.048515549Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":" ptytest.go:102: 2023-03-29 13:37:34.048: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:34.048685756Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Output":"--- PASS: TestServer/TLSRedirect/NoRedirectWithWildcard (0.47s)\n"}
+{"Time":"2023-03-29T13:37:34.049232577Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirectWithWildcard","Elapsed":0.47}
+{"Time":"2023-03-29T13:37:34.049241005Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:34.049926527Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Output":"--- PASS: TestServer/RateLimit/Changed (1.07s)\n"}
+{"Time":"2023-03-29T13:37:34.049934686Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit/Changed","Elapsed":1.07}
+{"Time":"2023-03-29T13:37:34.049940803Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit","Output":"--- PASS: TestServer/RateLimit (0.00s)\n"}
+{"Time":"2023-03-29T13:37:34.050192839Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/RateLimit","Elapsed":0}
+{"Time":"2023-03-29T13:37:34.05020025Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:83: 2023-03-29 13:37:34.050: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:34.050204931Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.050: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:34.050237731Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:83: 2023-03-29 13:37:34.050: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:34.050243867Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.050: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:34.05027121Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:83: 2023-03-29 13:37:34.050: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:34.050278957Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:74: 2023-03-29 13:37:34.050: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:34.051736931Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:34.05174611Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:34.051751453Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.051758881Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:34.051780431Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.05178826Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:34.051870799Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.051881957Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:34.051887542Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.05189248Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:110: 2023-03-29 13:37:34.051: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.05189708Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:111: 2023-03-29 13:37:34.051: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:34.05190198Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:113: 2023-03-29 13:37:34.051: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.051911515Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:34.051922383Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:34.051927984Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.051938866Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:34.051946554Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.051962503Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:34.051971415Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.0519782Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:34.051987362Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.051: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.052007059Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:110: 2023-03-29 13:37:34.051: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.052015346Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:111: 2023-03-29 13:37:34.052: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:34.052021784Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:113: 2023-03-29 13:37:34.052: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.052400141Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:34.052410137Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:34.052420866Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.052427625Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:34.052440372Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.052448298Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:34.052457892Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.052465595Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:34.052483713Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:121: 2023-03-29 13:37:34.052: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.052491345Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:110: 2023-03-29 13:37:34.052: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.052498064Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:111: 2023-03-29 13:37:34.052: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:34.052506921Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:113: 2023-03-29 13:37:34.052: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.078632301Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.07865445Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:74: 2023-03-29 13:37:34.078: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:34.078658395Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.078664194Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:74: 2023-03-29 13:37:34.078: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:34.078667573Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.078670569Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":" ptytest.go:102: 2023-03-29 13:37:34.078: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:34.078838607Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Output":"--- PASS: TestServer/TLSRedirect/OK (1.06s)\n"}
+{"Time":"2023-03-29T13:37:34.078885902Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/OK","Elapsed":1.06}
+{"Time":"2023-03-29T13:37:34.078892321Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.078897167Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.078: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:34.078901364Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.078912845Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.078: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:34.078917725Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.078: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.07894293Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":" ptytest.go:102: 2023-03-29 13:37:34.078: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:34.079092731Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Output":"--- PASS: TestServer/TLSRedirect/NoHTTPListener (0.62s)\n"}
+{"Time":"2023-03-29T13:37:34.079144047Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoHTTPListener","Elapsed":0.62}
+{"Time":"2023-03-29T13:37:34.079152217Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.079: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.079158662Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.079: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:34.079162915Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.079: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.079168988Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:74: 2023-03-29 13:37:34.079: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:34.079178829Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:76: 2023-03-29 13:37:34.079: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.079196269Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":" ptytest.go:102: 2023-03-29 13:37:34.079: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:34.079336643Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Output":"--- PASS: TestServer/TLSRedirect/NoTLSListener (0.59s)\n"}
+{"Time":"2023-03-29T13:37:34.091089368Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoTLSListener","Elapsed":0.59}
+{"Time":"2023-03-29T13:37:34.091112345Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:83: 2023-03-29 13:37:34.091: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:34.091119428Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.091: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:34.115772917Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:34.115801947Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:34.11581297Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.115819506Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:34.115824758Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.115830066Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:34.11583724Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.115844144Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:34.115860103Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.115888103Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:110: 2023-03-29 13:37:34.115: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.115903904Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:111: 2023-03-29 13:37:34.115: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:34.115919206Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:113: 2023-03-29 13:37:34.115: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.115976431Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:34.115986313Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:34.115997881Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.11602101Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.115: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:34.116029108Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:121: 2023-03-29 13:37:34.116: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.116476692Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.116: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.116491796Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.116: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:34.116497774Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.116: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.116504918Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:74: 2023-03-29 13:37:34.116: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:34.1165175Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:76: 2023-03-29 13:37:34.116: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.116539123Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":" ptytest.go:102: 2023-03-29 13:37:34.116: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:34.116717402Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Output":"--- PASS: TestServer/TLSAndHTTP (1.24s)\n"}
+{"Time":"2023-03-29T13:37:34.117404728Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSAndHTTP","Elapsed":1.24}
+{"Time":"2023-03-29T13:37:34.117418357Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:83: 2023-03-29 13:37:34.117: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:34.117441857Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:74: 2023-03-29 13:37:34.117: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:34.117475322Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:110: 2023-03-29 13:37:34.117: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.117484444Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:111: 2023-03-29 13:37:34.117: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:34.117494423Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:113: 2023-03-29 13:37:34.117: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.11754208Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:76: 2023-03-29 13:37:34.117: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.117551844Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:74: 2023-03-29 13:37:34.117: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:34.117570608Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:76: 2023-03-29 13:37:34.117: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.117578465Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:74: 2023-03-29 13:37:34.117: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:34.117597311Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:76: 2023-03-29 13:37:34.117: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.117616074Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":" ptytest.go:102: 2023-03-29 13:37:34.117: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:34.11776518Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Output":"--- PASS: TestServer/DeprecatedAddress/TLS (0.50s)\n"}
+{"Time":"2023-03-29T13:37:34.117776019Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress/TLS","Elapsed":0.5}
+{"Time":"2023-03-29T13:37:34.117782763Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress","Output":"--- PASS: TestServer/DeprecatedAddress (0.06s)\n"}
+{"Time":"2023-03-29T13:37:34.117841805Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/DeprecatedAddress","Elapsed":0.06}
+{"Time":"2023-03-29T13:37:34.117849931Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:83: 2023-03-29 13:37:34.117: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:34.117859828Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:74: 2023-03-29 13:37:34.117: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:34.118127485Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:83: 2023-03-29 13:37:34.118: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:34.118138813Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:74: 2023-03-29 13:37:34.118: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:34.118170526Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:110: 2023-03-29 13:37:34.118: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.11817918Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:111: 2023-03-29 13:37:34.118: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:34.118194579Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:113: 2023-03-29 13:37:34.118: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.11825734Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:76: 2023-03-29 13:37:34.118: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.118277979Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:74: 2023-03-29 13:37:34.118: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:34.118286381Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:76: 2023-03-29 13:37:34.118: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.118291513Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:74: 2023-03-29 13:37:34.118: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:34.118295899Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:76: 2023-03-29 13:37:34.118: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.11830286Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":" ptytest.go:102: 2023-03-29 13:37:34.118: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:34.118448813Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Output":"--- PASS: TestServer/TLSValidMultiple (1.23s)\n"}
+{"Time":"2023-03-29T13:37:34.118485143Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSValidMultiple","Elapsed":1.23}
+{"Time":"2023-03-29T13:37:34.118492964Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:34.118500337Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:34.118505085Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.118511668Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:34.118536832Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.118542137Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:34.118548654Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.118562305Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:34.118568736Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:121: 2023-03-29 13:37:34.118: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:34.1185733Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:110: 2023-03-29 13:37:34.118: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.118577677Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:111: 2023-03-29 13:37:34.118: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:34.118583972Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:113: 2023-03-29 13:37:34.118: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:34.347394026Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:76: 2023-03-29 13:37:34.347: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.347481519Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:74: 2023-03-29 13:37:34.347: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:34.347503045Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:76: 2023-03-29 13:37:34.347: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.347521875Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:74: 2023-03-29 13:37:34.347: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:34.347538956Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:76: 2023-03-29 13:37:34.347: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:34.347563965Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":" ptytest.go:102: 2023-03-29 13:37:34.347: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:34.347597239Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Output":"--- PASS: TestServer/TLSRedirect/NoRedirect (0.77s)\n"}
+{"Time":"2023-03-29T13:37:34.347618369Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect/NoRedirect","Elapsed":0.77}
+{"Time":"2023-03-29T13:37:34.347639094Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect","Output":"--- PASS: TestServer/TLSRedirect (0.05s)\n"}
+{"Time":"2023-03-29T13:37:37.495996215Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/TLSRedirect","Elapsed":0.05}
+{"Time":"2023-03-29T13:37:37.496041088Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Started HTTP listener at http://[::]:42403\n"}
+{"Time":"2023-03-29T13:37:37.497561163Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: View the Web UI: http://example.com\n"}
+{"Time":"2023-03-29T13:37:37.854158017Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: ==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\n"}
+{"Time":"2023-03-29T13:37:37.871207973Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: \u001b[1mInterrupt caught, gracefully exiting. Use ctrl+\\ to force quit\u001b[0m\n"}
+{"Time":"2023-03-29T13:37:37.871239014Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Shutting down API server...\n"}
+{"Time":"2023-03-29T13:37:37.871296925Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Gracefully shut down API server\n"}
+{"Time":"2023-03-29T13:37:37.871384307Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Waiting for WebSocket connections to close...\n"}
+{"Time":"2023-03-29T13:37:37.871872118Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Done waiting for WebSocket connections\n"}
+{"Time":"2023-03-29T13:37:37.872659674Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Stopping built-in PostgreSQL...\n"}
+{"Time":"2023-03-29T13:37:37.974547475Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":" clitest.go:50: stdout: Stopped built-in PostgreSQL\n"}
+{"Time":"2023-03-29T13:37:38.01812179Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Output":"--- PASS: TestServer/BuiltinPostgres (5.17s)\n"}
+{"Time":"2023-03-29T13:37:46.072262917Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/BuiltinPostgres","Elapsed":5.17}
+{"Time":"2023-03-29T13:37:46.072311252Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \"Started HTTP listener at http://[::]:35645\"\n"}
+{"Time":"2023-03-29T13:37:46.072335279Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \"WARN: The access URL http://example.com could not be resolved, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\"\n"}
+{"Time":"2023-03-29T13:37:46.07241558Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.072459905Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:46.07251014Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.072: cmd: \"View the Web UI: http://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:46.072679873Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" server_test.go:1240: 2023-03-29 13:37:46.072: cmd: matched \"Started HTTP listener at\" = \"Started HTTP listener at\"\n"}
+{"Time":"2023-03-29T13:37:46.078543671Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:46.078573522Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:46.07859364Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:46.07861453Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"ERROR: Unexpected error, shutting down server: context deadline exceeded\"\n"}
+{"Time":"2023-03-29T13:37:46.078631014Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.078647037Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:46.078658003Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.078668373Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:46.078678662Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.078702364Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Shutting down provisioner daemon 3...\"\n"}
+{"Time":"2023-03-29T13:37:46.078719655Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.078730465Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Gracefully shut down provisioner daemon 3\"\n"}
+{"Time":"2023-03-29T13:37:46.078744589Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.078763734Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Shutting down provisioner daemon 1...\"\n"}
+{"Time":"2023-03-29T13:37:46.07878276Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.078801865Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Gracefully shut down provisioner daemon 1\"\n"}
+{"Time":"2023-03-29T13:37:46.07881665Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.078826759Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Shutting down provisioner daemon 2...\"\n"}
+{"Time":"2023-03-29T13:37:46.078840612Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.07885439Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \"Gracefully shut down provisioner daemon 2\"\n"}
+{"Time":"2023-03-29T13:37:46.07887537Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.078: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.081392321Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:46.081408574Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.081421854Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:46.081431192Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:46.081449657Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:121: 2023-03-29 13:37:46.081: cmd: \"WARN: Graceful shutdown timed out\\r\"\n"}
+{"Time":"2023-03-29T13:37:46.14910839Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:83: 2023-03-29 13:37:46.149: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:46.149142371Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:74: 2023-03-29 13:37:46.149: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:46.149157567Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:110: 2023-03-29 13:37:46.149: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:46.149166448Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:111: 2023-03-29 13:37:46.149: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:46.149175898Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:113: 2023-03-29 13:37:46.149: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:46.149187205Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:76: 2023-03-29 13:37:46.149: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:46.14919623Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:74: 2023-03-29 13:37:46.149: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:46.149207373Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:76: 2023-03-29 13:37:46.149: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:46.149215685Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:74: 2023-03-29 13:37:46.149: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:46.149223347Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:76: 2023-03-29 13:37:46.149: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:46.149234369Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":" ptytest.go:102: 2023-03-29 13:37:46.149: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:46.149426722Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Output":"--- PASS: TestServer/Logging/Multiple (13.24s)\n"}
+{"Time":"2023-03-29T13:37:59.1117031Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Multiple","Elapsed":13.24}
+{"Time":"2023-03-29T13:37:59.111755068Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.111: cmd: \"2023-03-29 13:37:59.110 [DEBUG]\\t\u003cgithub.com/coder/coder/v2/cli/server.go:260\u003e\\t(*RootCmd).Server.func1\\tstarted debug logging\"\n"}
+{"Time":"2023-03-29T13:37:59.111775176Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.111: cmd: \"Started HTTP listener at http://[::]:40007\"\n"}
+{"Time":"2023-03-29T13:37:59.111795675Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" server_test.go:1204: 2023-03-29 13:37:59.111: cmd: matched \"Started HTTP listener at\" = \"2023-03-29 13:37:59.110 [DEBUG]\\t\u003cgithub.com/coder/coder/v2/cli/server.go:260\u003e\\t(*RootCmd).Server.func1\\tstarted debug logging\\r\\nStarted HTTP listener at\"\n"}
+{"Time":"2023-03-29T13:37:59.123488003Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.123: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:59.123536759Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.123: cmd: \"View the Web UI: http://example.com\\r\"\n"}
+{"Time":"2023-03-29T13:37:59.123550652Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.123: cmd: \" \"\n"}
+{"Time":"2023-03-29T13:37:59.123559998Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.123: cmd: \"==\u003e Logs will stream in below (press ctrl+c to gracefully exit):\\r\"\n"}
+{"Time":"2023-03-29T13:37:59.144165093Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.123 [DEBUG]\\t(coderd.metrics_cache)\\t\u003cgithub.com/coder/coder/v2/coderd/metricscache/metricscache.go:272\u003e\\t(*Cache).run\\tdeployment stats metrics refreshed\\t{\\\"took\\\": \\\"20.37µs\\\", \\\"interval\\\": \\\"30s\\\"}\"\n"}
+{"Time":"2023-03-29T13:37:59.144212142Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.139 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\\t(*Server).connect\\tconnected\"\n"}
+{"Time":"2023-03-29T13:37:59.1442281Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.139 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\\t(*Server).connect\\tconnected\"\n"}
+{"Time":"2023-03-29T13:37:59.14427036Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.139 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:200\u003e\\t(*Server).connect\\tconnected\"\n"}
+{"Time":"2023-03-29T13:37:59.14428344Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.143 [DEBUG]\\t(coderd.metrics_cache)\\t\u003cgithub.com/coder/coder/v2/coderd/metricscache/metricscache.go:272\u003e\\t(*Cache).run\\ttemplate daus metrics refreshed\\t{\\\"took\\\": \\\"3.61474ms\\\", \\\"interval\\\": \\\"1h0m0s\\\"}\"\n"}
+{"Time":"2023-03-29T13:37:59.14429578Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Interrupt caught, gracefully exiting. Use ctrl+\\\\ to force quit\"\n"}
+{"Time":"2023-03-29T13:37:59.14430588Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Shutting down API server...\"\n"}
+{"Time":"2023-03-29T13:37:59.144315009Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.144324164Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Gracefully shut down API server\"\n"}
+{"Time":"2023-03-29T13:37:59.144339556Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.144624294Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Shutting down provisioner daemon 1...\"\n"}
+{"Time":"2023-03-29T13:37:59.144639861Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.14465833Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.144 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\\t(*Server).closeWithError\\tclosing server with error\\t{\\\"error\\\": null}\"\n"}
+{"Time":"2023-03-29T13:37:59.14468626Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Gracefully shut down provisioner daemon 1\"\n"}
+{"Time":"2023-03-29T13:37:59.144695878Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.144705145Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Shutting down provisioner daemon 2...\"\n"}
+{"Time":"2023-03-29T13:37:59.144718072Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.144727731Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.144 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\\t(*Server).closeWithError\\tclosing server with error\\t{\\\"error\\\": null}\"\n"}
+{"Time":"2023-03-29T13:37:59.144750279Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Gracefully shut down provisioner daemon 2\"\n"}
+{"Time":"2023-03-29T13:37:59.144764838Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.14477793Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Shutting down provisioner daemon 3...\"\n"}
+{"Time":"2023-03-29T13:37:59.144787313Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.144797136Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"2023-03-29 13:37:59.144 [DEBUG]\\t\u003cgithub.com/coder/coder/provisionerd/provisionerd.go:553\u003e\\t(*Server).closeWithError\\tclosing server with error\\t{\\\"error\\\": null}\"\n"}
+{"Time":"2023-03-29T13:37:59.144810893Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Gracefully shut down provisioner daemon 3\"\n"}
+{"Time":"2023-03-29T13:37:59.144821417Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.144842079Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Waiting for WebSocket connections to close...\"\n"}
+{"Time":"2023-03-29T13:37:59.144855916Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.144867744Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \"Done waiting for WebSocket connections\"\n"}
+{"Time":"2023-03-29T13:37:59.14488065Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:121: 2023-03-29 13:37:59.144: cmd: \" \\r\"\n"}
+{"Time":"2023-03-29T13:37:59.146012139Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:83: 2023-03-29 13:37:59.145: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:37:59.146025303Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:74: 2023-03-29 13:37:59.145: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:37:59.14606956Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:110: 2023-03-29 13:37:59.146: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:59.146083103Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:111: 2023-03-29 13:37:59.146: cmd: closing out\n"}
+{"Time":"2023-03-29T13:37:59.146095911Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:113: 2023-03-29 13:37:59.146: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:37:59.146143558Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:76: 2023-03-29 13:37:59.146: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:59.146156843Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:74: 2023-03-29 13:37:59.146: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:37:59.146169893Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:76: 2023-03-29 13:37:59.146: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:59.146182763Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:74: 2023-03-29 13:37:59.146: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:37:59.146191645Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:76: 2023-03-29 13:37:59.146: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:37:59.146205122Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":" ptytest.go:102: 2023-03-29 13:37:59.146: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:37:59.146368404Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Output":"--- PASS: TestServer/Logging/Stackdriver (26.23s)\n"}
+{"Time":"2023-03-29T13:37:59.146391543Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging/Stackdriver","Elapsed":26.23}
+{"Time":"2023-03-29T13:37:59.146412558Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging","Output":"--- PASS: TestServer/Logging (0.00s)\n"}
+{"Time":"2023-03-29T13:37:59.146438143Z","Action":"pass","Package":"github.com/coder/coder/v2/cli","Test":"TestServer/Logging","Elapsed":0}
+{"Time":"2023-03-29T13:37:59.1464552Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Test":"TestServer","Output":"--- FAIL: TestServer (0.05s)\n"}
+{"Time":"2023-03-29T13:37:59.146474821Z","Action":"fail","Package":"github.com/coder/coder/v2/cli","Test":"TestServer","Elapsed":0.05}
+{"Time":"2023-03-29T13:37:59.146491309Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Output":"FAIL\n"}
+{"Time":"2023-03-29T13:37:59.158021068Z","Action":"output","Package":"github.com/coder/coder/v2/cli","Output":"FAIL\tgithub.com/coder/coder/v2/cli\t26.514s\n"}
+{"Time":"2023-03-29T13:37:59.158054855Z","Action":"fail","Package":"github.com/coder/coder/v2/cli","Elapsed":26.514}
+{"Time":"2023-03-29T13:38:02.724238056Z","Action":"start","Package":"github.com/coder/coder/v2/cli/cliui"}
+{"Time":"2023-03-29T13:38:02.754440648Z","Action":"run","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth"}
+{"Time":"2023-03-29T13:38:02.75448054Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":"=== RUN TestGitAuth\n"}
+{"Time":"2023-03-29T13:38:02.754486705Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":"=== PAUSE TestGitAuth\n"}
+{"Time":"2023-03-29T13:38:02.754490044Z","Action":"pause","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth"}
+{"Time":"2023-03-29T13:38:02.754493443Z","Action":"run","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt"}
+{"Time":"2023-03-29T13:38:02.754496272Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt","Output":"=== RUN TestPrompt\n"}
+{"Time":"2023-03-29T13:38:02.754504892Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt","Output":"=== PAUSE TestPrompt\n"}
+{"Time":"2023-03-29T13:38:02.754507539Z","Action":"pause","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt"}
+{"Time":"2023-03-29T13:38:02.754510534Z","Action":"cont","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth"}
+{"Time":"2023-03-29T13:38:02.754514471Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":"=== CONT TestGitAuth\n"}
+{"Time":"2023-03-29T13:38:02.754642422Z","Action":"cont","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt"}
+{"Time":"2023-03-29T13:38:02.754653067Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt","Output":"=== CONT TestPrompt\n"}
+{"Time":"2023-03-29T13:38:02.754658206Z","Action":"run","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success"}
+{"Time":"2023-03-29T13:38:02.754660941Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":"=== RUN TestPrompt/Success\n"}
+{"Time":"2023-03-29T13:38:02.754664503Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":"=== PAUSE TestPrompt/Success\n"}
+{"Time":"2023-03-29T13:38:02.754666941Z","Action":"pause","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success"}
+{"Time":"2023-03-29T13:38:02.754671476Z","Action":"run","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm"}
+{"Time":"2023-03-29T13:38:02.754673908Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":"=== RUN TestPrompt/Confirm\n"}
+{"Time":"2023-03-29T13:38:02.754676919Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":"=== PAUSE TestPrompt/Confirm\n"}
+{"Time":"2023-03-29T13:38:02.754683157Z","Action":"pause","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm"}
+{"Time":"2023-03-29T13:38:02.754688172Z","Action":"run","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip"}
+{"Time":"2023-03-29T13:38:02.754690617Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":"=== RUN TestPrompt/Skip\n"}
+{"Time":"2023-03-29T13:38:02.754693754Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":"=== PAUSE TestPrompt/Skip\n"}
+{"Time":"2023-03-29T13:38:02.754696128Z","Action":"pause","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip"}
+{"Time":"2023-03-29T13:38:02.754700672Z","Action":"run","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON"}
+{"Time":"2023-03-29T13:38:02.754703008Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":"=== RUN TestPrompt/JSON\n"}
+{"Time":"2023-03-29T13:38:02.754718094Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":"=== PAUSE TestPrompt/JSON\n"}
+{"Time":"2023-03-29T13:38:02.754723349Z","Action":"pause","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON"}
+{"Time":"2023-03-29T13:38:02.754728958Z","Action":"run","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON"}
+{"Time":"2023-03-29T13:38:02.754731492Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":"=== RUN TestPrompt/BadJSON\n"}
+{"Time":"2023-03-29T13:38:02.754734435Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":"=== PAUSE TestPrompt/BadJSON\n"}
+{"Time":"2023-03-29T13:38:02.754736902Z","Action":"pause","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON"}
+{"Time":"2023-03-29T13:38:02.754748953Z","Action":"run","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON"}
+{"Time":"2023-03-29T13:38:02.754751439Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":"=== RUN TestPrompt/MultilineJSON\n"}
+{"Time":"2023-03-29T13:38:02.754755982Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":"=== PAUSE TestPrompt/MultilineJSON\n"}
+{"Time":"2023-03-29T13:38:02.754760728Z","Action":"pause","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON"}
+{"Time":"2023-03-29T13:38:02.754764892Z","Action":"cont","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success"}
+{"Time":"2023-03-29T13:38:02.754767252Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":"=== CONT TestPrompt/Success\n"}
+{"Time":"2023-03-29T13:38:02.754976869Z","Action":"cont","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON"}
+{"Time":"2023-03-29T13:38:02.754982653Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":"=== CONT TestPrompt/MultilineJSON\n"}
+{"Time":"2023-03-29T13:38:02.755108229Z","Action":"cont","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON"}
+{"Time":"2023-03-29T13:38:02.755113844Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":"=== CONT TestPrompt/BadJSON\n"}
+{"Time":"2023-03-29T13:38:02.755212757Z","Action":"cont","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON"}
+{"Time":"2023-03-29T13:38:02.755218041Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":"=== CONT TestPrompt/JSON\n"}
+{"Time":"2023-03-29T13:38:02.755315155Z","Action":"cont","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip"}
+{"Time":"2023-03-29T13:38:02.755318778Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":"=== CONT TestPrompt/Skip\n"}
+{"Time":"2023-03-29T13:38:02.755513491Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:83: 2023-03-29 13:38:02.755: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:38:02.755529621Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.755: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:38:02.755596722Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.755: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.755601928Z","Action":"cont","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm"}
+{"Time":"2023-03-29T13:38:02.755604522Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":"=== CONT TestPrompt/Confirm\n"}
+{"Time":"2023-03-29T13:38:02.756154509Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:110: 2023-03-29 13:38:02.756: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.756161274Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:111: 2023-03-29 13:38:02.756: cmd: closing out\n"}
+{"Time":"2023-03-29T13:38:02.756179269Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:113: 2023-03-29 13:38:02.756: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.756195571Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:38:02.75620977Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.756222494Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:38:02.756250235Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.756263435Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:102: 2023-03-29 13:38:02.756: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:38:02.75631973Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:83: 2023-03-29 13:38:02.756: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:38:02.756334455Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:38:02.756398184Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed pty: pty: closed\n"}
+{"Time":"2023-03-29T13:38:02.75641208Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:38:02.756425561Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.756442572Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:74: 2023-03-29 13:38:02.756: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:38:02.756457245Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:76: 2023-03-29 13:38:02.756: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.756473084Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":" ptytest.go:102: 2023-03-29 13:38:02.756: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:38:02.756487964Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Output":"--- PASS: TestPrompt/Skip (0.00s)\n"}
+{"Time":"2023-03-29T13:38:02.756674486Z","Action":"pass","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Skip","Elapsed":0}
+{"Time":"2023-03-29T13:38:02.756683362Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" prompt_test.go:51: 2023-03-29 13:38:02.756: cmd: matched \"Example\" = \"\u003e Example\"\n"}
+{"Time":"2023-03-29T13:38:02.756700414Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" prompt_test.go:52: 2023-03-29 13:38:02.756: cmd: stdin: \"yes\\r\"\n"}
+{"Time":"2023-03-29T13:38:02.756759525Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" prompt_test.go:108: 2023-03-29 13:38:02.756: cmd: matched \"Example\" = \"\u003e Example\"\n"}
+{"Time":"2023-03-29T13:38:02.75678767Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" prompt_test.go:109: 2023-03-29 13:38:02.756: cmd: stdin: \"{}\\r\"\n"}
+{"Time":"2023-03-29T13:38:02.756838147Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" prompt_test.go:124: 2023-03-29 13:38:02.756: cmd: matched \"Example\" = \"\u003e Example\"\n"}
+{"Time":"2023-03-29T13:38:02.756862647Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" prompt_test.go:125: 2023-03-29 13:38:02.756: cmd: stdin: \"{a\\r\"\n"}
+{"Time":"2023-03-29T13:38:02.756932142Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" prompt_test.go:140: 2023-03-29 13:38:02.756: cmd: matched \"Example\" = \"\u003e Example\"\n"}
+{"Time":"2023-03-29T13:38:02.756958031Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" prompt_test.go:141: 2023-03-29 13:38:02.756: cmd: stdin: \"{\\n\\\"test\\\": \\\"wow\\\"\\n}\\r\"\n"}
+{"Time":"2023-03-29T13:38:02.757013878Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.756: cmd: \"You must authenticate with GitHub to create a workspace with this template. Visit:\"\n"}
+{"Time":"2023-03-29T13:38:02.757025551Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\"\n"}
+{"Time":"2023-03-29T13:38:02.757047114Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\\thttps://example.com/gitauth/github?redirect=%2Fgitauth%3Fnotify\"\n"}
+{"Time":"2023-03-29T13:38:02.757065197Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\"\n"}
+{"Time":"2023-03-29T13:38:02.757096029Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\\r\\r⠈⠁ Waiting for Git authentication...\\rSuccessfully authenticated with GitHub!\"\n"}
+{"Time":"2023-03-29T13:38:02.757108294Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\"\n"}
+{"Time":"2023-03-29T13:38:02.757140128Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" gitauth_test.go:53: 2023-03-29 13:38:02.757: cmd: matched \"You must authenticate with\" = \"You must authenticate with\"\n"}
+{"Time":"2023-03-29T13:38:02.757190596Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" gitauth_test.go:54: 2023-03-29 13:38:02.757: cmd: matched \"https://example.com/gitauth/github\" = \" GitHub to create a workspace with this template. Visit:\\r\\n\\r\\n\\thttps://example.com/gitauth/github\"\n"}
+{"Time":"2023-03-29T13:38:02.757264811Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" gitauth_test.go:55: 2023-03-29 13:38:02.757: cmd: matched \"Successfully authenticated with GitHub\" = \"?redirect=%2Fgitauth%3Fnotify\\r\\n\\r\\n\\r\\r⠈⠁ Waiting for Git authentication...\\rSuccessfully authenticated with GitHub\"\n"}
+{"Time":"2023-03-29T13:38:02.757294284Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:83: 2023-03-29 13:38:02.757: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:38:02.757307619Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:38:02.757350699Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:110: 2023-03-29 13:38:02.757: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.757369646Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:111: 2023-03-29 13:38:02.757: cmd: closing out\n"}
+{"Time":"2023-03-29T13:38:02.757388269Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:113: 2023-03-29 13:38:02.757: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.757437516Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.7574545Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:38:02.757468757Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.757483649Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:38:02.757496041Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.757513203Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":" ptytest.go:102: 2023-03-29 13:38:02.757: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:38:02.757518992Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Output":"--- PASS: TestGitAuth (0.00s)\n"}
+{"Time":"2023-03-29T13:38:02.75757314Z","Action":"pass","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestGitAuth","Elapsed":0}
+{"Time":"2023-03-29T13:38:02.757576987Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" prompt_test.go:34: 2023-03-29 13:38:02.757: cmd: matched \"Example\" = \"\u003e Example\"\n"}
+{"Time":"2023-03-29T13:38:02.757605985Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" prompt_test.go:35: 2023-03-29 13:38:02.757: cmd: stdin: \"hello\\r\"\n"}
+{"Time":"2023-03-29T13:38:02.757679461Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\u003e Example hello\"\n"}
+{"Time":"2023-03-29T13:38:02.757731096Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:83: 2023-03-29 13:38:02.757: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:38:02.757745488Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:38:02.757786052Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:110: 2023-03-29 13:38:02.757: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.757793961Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:111: 2023-03-29 13:38:02.757: cmd: closing out\n"}
+{"Time":"2023-03-29T13:38:02.757809024Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:113: 2023-03-29 13:38:02.757: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.757853587Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.757869751Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:38:02.757888281Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.757896143Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:74: 2023-03-29 13:38:02.757: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:38:02.757912615Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:76: 2023-03-29 13:38:02.757: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.757929309Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":" ptytest.go:102: 2023-03-29 13:38:02.757: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:38:02.757934794Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Output":"--- PASS: TestPrompt/Success (0.00s)\n"}
+{"Time":"2023-03-29T13:38:02.757963355Z","Action":"pass","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Success","Elapsed":0}
+{"Time":"2023-03-29T13:38:02.757966707Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\u003e Example {\"\n"}
+{"Time":"2023-03-29T13:38:02.757981822Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"\\\"test\\\": \\\"wow\\\"\"\n"}
+{"Time":"2023-03-29T13:38:02.757994456Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.757: cmd: \"}\"\n"}
+{"Time":"2023-03-29T13:38:02.75807554Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:83: 2023-03-29 13:38:02.758: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:38:02.758091845Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:38:02.758122927Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:110: 2023-03-29 13:38:02.758: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.758135875Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:111: 2023-03-29 13:38:02.758: cmd: closing out\n"}
+{"Time":"2023-03-29T13:38:02.758148587Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:113: 2023-03-29 13:38:02.758: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.758193703Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.758208689Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:38:02.75822251Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.758236906Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:38:02.758250264Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.758266392Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":" ptytest.go:102: 2023-03-29 13:38:02.758: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:38:02.758271916Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Output":"--- PASS: TestPrompt/MultilineJSON (0.00s)\n"}
+{"Time":"2023-03-29T13:38:02.758311206Z","Action":"pass","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/MultilineJSON","Elapsed":0}
+{"Time":"2023-03-29T13:38:02.758314623Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.758: cmd: \"\u003e Example {a\"\n"}
+{"Time":"2023-03-29T13:38:02.75836333Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:83: 2023-03-29 13:38:02.758: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:38:02.758378803Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:38:02.758418274Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:110: 2023-03-29 13:38:02.758: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.758425803Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:111: 2023-03-29 13:38:02.758: cmd: closing out\n"}
+{"Time":"2023-03-29T13:38:02.75844102Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:113: 2023-03-29 13:38:02.758: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.758482492Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.758498572Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:38:02.758513045Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.758526569Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:38:02.75853994Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.758557098Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":" ptytest.go:102: 2023-03-29 13:38:02.758: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:38:02.758562364Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Output":"--- PASS: TestPrompt/BadJSON (0.00s)\n"}
+{"Time":"2023-03-29T13:38:02.758589722Z","Action":"pass","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/BadJSON","Elapsed":0}
+{"Time":"2023-03-29T13:38:02.758593068Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:121: 2023-03-29 13:38:02.758: cmd: \"\u003e Example {}\"\n"}
+{"Time":"2023-03-29T13:38:02.758640512Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:83: 2023-03-29 13:38:02.758: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:38:02.75865573Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:38:02.75868581Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:110: 2023-03-29 13:38:02.758: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.758697594Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:111: 2023-03-29 13:38:02.758: cmd: closing out\n"}
+{"Time":"2023-03-29T13:38:02.758712004Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:113: 2023-03-29 13:38:02.758: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.758753059Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.758789238Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:38:02.75880265Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.758821412Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:38:02.758835398Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:76: 2023-03-29 13:38:02.758: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.758851842Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":" ptytest.go:102: 2023-03-29 13:38:02.758: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:38:02.758857116Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Output":"--- PASS: TestPrompt/JSON (0.00s)\n"}
+{"Time":"2023-03-29T13:38:02.758897163Z","Action":"pass","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/JSON","Elapsed":0}
+{"Time":"2023-03-29T13:38:02.758900525Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:121: 2023-03-29 13:38:02.758: cmd: \"\u003e Example (yes/no) yes\"\n"}
+{"Time":"2023-03-29T13:38:02.758967291Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:83: 2023-03-29 13:38:02.758: cmd: closing tpty: close\n"}
+{"Time":"2023-03-29T13:38:02.758981151Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:74: 2023-03-29 13:38:02.758: cmd: closing pty\n"}
+{"Time":"2023-03-29T13:38:02.759023005Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:110: 2023-03-29 13:38:02.758: cmd: copy done: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.759035878Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:111: 2023-03-29 13:38:02.759: cmd: closing out\n"}
+{"Time":"2023-03-29T13:38:02.759048644Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:113: 2023-03-29 13:38:02.759: cmd: closed out: read /dev/ptmx: file already closed\n"}
+{"Time":"2023-03-29T13:38:02.759094323Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:76: 2023-03-29 13:38:02.759: cmd: closed pty: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.759115364Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:74: 2023-03-29 13:38:02.759: cmd: closing logw\n"}
+{"Time":"2023-03-29T13:38:02.75913071Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:76: 2023-03-29 13:38:02.759: cmd: closed logw: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.759144071Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:74: 2023-03-29 13:38:02.759: cmd: closing logr\n"}
+{"Time":"2023-03-29T13:38:02.75915741Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:76: 2023-03-29 13:38:02.759: cmd: closed logr: \u003cnil\u003e\n"}
+{"Time":"2023-03-29T13:38:02.759174685Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":" ptytest.go:102: 2023-03-29 13:38:02.759: cmd: closed tpty\n"}
+{"Time":"2023-03-29T13:38:02.759180019Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Output":"--- PASS: TestPrompt/Confirm (0.00s)\n"}
+{"Time":"2023-03-29T13:38:02.759187539Z","Action":"pass","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt/Confirm","Elapsed":0}
+{"Time":"2023-03-29T13:38:02.75919072Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt","Output":"--- PASS: TestPrompt (0.00s)\n"}
+{"Time":"2023-03-29T13:38:02.759194742Z","Action":"pass","Package":"github.com/coder/coder/v2/cli/cliui","Test":"TestPrompt","Elapsed":0}
+{"Time":"2023-03-29T13:38:02.759198362Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Output":"PASS\n"}
+{"Time":"2023-03-29T13:38:02.761021961Z","Action":"output","Package":"github.com/coder/coder/v2/cli/cliui","Output":"ok \tgithub.com/coder/coder/v2/cli/cliui\t0.037s\n"}
+{"Time":"2023-03-29T13:38:02.761046557Z","Action":"pass","Package":"github.com/coder/coder/v2/cli/cliui","Elapsed":0.037}
diff --git a/scripts/clidocgen/gen.go b/scripts/clidocgen/gen.go
index ac50b511ca134..98fdb388c23ea 100644
--- a/scripts/clidocgen/gen.go
+++ b/scripts/clidocgen/gen.go
@@ -12,8 +12,8 @@ import (
"github.com/acarl005/stripansi"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/cli/clibase"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/cli/clibase"
"github.com/coder/flog"
)
diff --git a/scripts/clidocgen/main.go b/scripts/clidocgen/main.go
index 7d6f6d9c7f7b2..961797b02d0ab 100644
--- a/scripts/clidocgen/main.go
+++ b/scripts/clidocgen/main.go
@@ -7,8 +7,8 @@ import (
"sort"
"strings"
- "github.com/coder/coder/cli/clibase"
- "github.com/coder/coder/enterprise/cli"
+ "github.com/coder/coder/v2/cli/clibase"
+ "github.com/coder/coder/v2/enterprise/cli"
"github.com/coder/flog"
)
diff --git a/scripts/dbgen/main.go b/scripts/dbgen/main.go
index 8a7976e32154b..dfc4778e30b34 100644
--- a/scripts/dbgen/main.go
+++ b/scripts/dbgen/main.go
@@ -526,7 +526,7 @@ func loadInterfaceFuncs(f *dst.File, interfaceName string) ([]querierFunction, e
if !ident.IsExported() {
continue
}
- ident.Path = "github.com/coder/coder/coderd/database"
+ ident.Path = "github.com/coder/coder/v2/coderd/database"
}
}
diff --git a/scripts/deploy-pr.sh b/scripts/deploy-pr.sh
index 84ee6ed6266f4..e852368fd8bbe 100755
--- a/scripts/deploy-pr.sh
+++ b/scripts/deploy-pr.sh
@@ -1,21 +1,26 @@
#!/usr/bin/env bash
-# Usage: ./deploy-pr.sh [--skip-build -s] [--dry-run -n] [--yes -y]
+# Usage: ./deploy-pr.sh [--dry-run -n] [--yes -y] [--experiments -e ] [--build -b] [--deploy -d]
# deploys the current branch to a PR environment and posts login credentials to
# [#pr-deployments](https://codercom.slack.com/archives/C05DNE982E8) Slack channel
set -euo pipefail
# default settings
-skipBuild=false
dryRun=false
confirm=true
+build=false
+deploy=false
experiments=""
# parse arguments
while (("$#")); do
case "$1" in
- -s | --skip-build)
- skipBuild=true
+ -b | --build)
+ build=true
+ shift
+ ;;
+ -d | --deploy)
+ deploy=true
shift
;;
-n | --dry-run)
@@ -63,30 +68,20 @@ fi
branchName=$(gh pr view --json headRefName | jq -r .headRefName)
prNumber=$(gh pr view --json number | jq -r .number)
-if $skipBuild; then
- #check if the image exists
- foundTag=$(curl -fsSL https://github.com/coder/coder/pkgs/container/coder-preview | grep -o "$prNumber" | head -n 1) || true
- echo "foundTag is: '${foundTag}'"
- if [[ -z "${foundTag}" ]]; then
- echo "Image not found"
- echo "${prNumber} tag not found in ghcr.io/coder/coder-preview"
- echo "Please remove --skip-build and try again"
- exit 1
- fi
-fi
-
-if $dryRun; then
+if [[ "$dryRun" = true ]]; then
echo "dry run"
echo "branchName: ${branchName}"
echo "prNumber: ${prNumber}"
- echo "skipBuild: ${skipBuild}"
echo "experiments: ${experiments}"
+ echo "build: ${build}"
+ echo "deploy: ${deploy}"
exit 0
fi
echo "branchName: ${branchName}"
echo "prNumber: ${prNumber}"
-echo "skipBuild: ${skipBuild}"
echo "experiments: ${experiments}"
+echo "build: ${build}"
+echo "deploy: ${deploy}"
-gh workflow run pr-deploy.yaml --ref "${branchName}" -f "pr_number=${prNumber}" -f "skip_build=${skipBuild}" -f "experiments=${experiments}"
+gh workflow run pr-deploy.yaml --ref "${branchName}" -f "pr_number=${prNumber}" -f "experiments=${experiments}" -f "build=${build}" -f "deploy=${deploy}"
diff --git a/scripts/dev-oidc.sh b/scripts/dev-oidc.sh
new file mode 100755
index 0000000000000..017c7f07c646d
--- /dev/null
+++ b/scripts/dev-oidc.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
+# shellcheck source=scripts/lib.sh
+source "${SCRIPT_DIR}/lib.sh"
+
+# Allow toggling verbose output
+[[ -n ${VERBOSE:-} ]] && set -x
+set -euo pipefail
+
+KEYCLOAK_VERSION="${KEYCLOAK_VERSION:-22.0}"
+
+cat </tmp/example-realm.json
+{
+ "realm": "coder",
+ "enabled": true,
+ "sslRequired": "none",
+ "registrationAllowed": true,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": ["password"],
+ "users": [
+ {
+ "username": "oidcuser",
+ "email": "oidcuser@coder.com",
+ "emailVerified": true,
+ "enabled": true,
+ "credentials": [
+ {
+ "type": "password",
+ "value": "password"
+ }
+ ],
+ "clientRoles": {
+ "realm-management": ["realm-admin"],
+ "account": ["manage-account"]
+ }
+ }
+ ],
+ "clients": [
+ {
+ "clientId": "coder",
+ "directAccessGrantsEnabled": true,
+ "enabled": true,
+ "fullScopeAllowed": true,
+ "baseUrl": "/coder",
+ "redirectUris": ["*"],
+ "secret": "coder"
+ }
+ ]
+}
+EOF
+
+echo '== Starting Keycloak'
+docker rm -f keycloak || true
+# Start Keycloak
+docker run --rm -d \
+ --name keycloak \
+ -p 9080:8080 \
+ -e KEYCLOAK_ADMIN=admin \
+ -e KEYCLOAK_ADMIN_PASSWORD=password \
+ -v /tmp/example-realm.json:/opt/keycloak/data/import/example-realm.json \
+ "quay.io/keycloak/keycloak:${KEYCLOAK_VERSION}" \
+ start-dev \
+ --import-realm
+
+echo '== Waiting for keycloak to become ready'
+# Start the timeout in the background so interrupting this script
+# doesn't hang for 60s.
+timeout 60s bash -c 'until curl -s --fail http://localhost:9080/realms/coder/.well-known/openid-configuration > /dev/null 2>&1; do sleep 0.5; done' ||
+ fatal 'Keycloak did not become ready in time' &
+wait $!
+
+echo '== Starting Coder'
+hostname=$(hostname -f)
+export CODER_OIDC_ISSUER_URL="http://${hostname}:9080/realms/coder"
+export CODER_OIDC_CLIENT_ID=coder
+export CODER_OIDC_CLIENT_SECRET=coder
+export CODER_DEV_ACCESS_URL="http://${hostname}:8080"
+
+exec "${SCRIPT_DIR}/develop.sh" "$@"
diff --git a/scripts/develop.sh b/scripts/develop.sh
index 671c46a0bd5cc..39f81c2951bc4 100755
--- a/scripts/develop.sh
+++ b/scripts/develop.sh
@@ -13,14 +13,19 @@ source "${SCRIPT_DIR}/lib.sh"
[[ -n ${VERBOSE:-} ]] && set -x
set -euo pipefail
+CODER_DEV_ACCESS_URL="${CODER_DEV_ACCESS_URL:-http://127.0.0.1:3000}"
DEFAULT_PASSWORD="SomeSecurePassword!"
password="${CODER_DEV_ADMIN_PASSWORD:-${DEFAULT_PASSWORD}}"
use_proxy=0
-args="$(getopt -o "" -l use-proxy,agpl,password: -- "$@")"
+args="$(getopt -o "" -l access-url:,use-proxy,agpl,password: -- "$@")"
eval set -- "$args"
while true; do
case "$1" in
+ --access-url)
+ CODER_DEV_ACCESS_URL="$2"
+ shift 2
+ ;;
--agpl)
export CODER_BUILD_AGPL=1
shift
@@ -131,7 +136,7 @@ fatal() {
trap 'fatal "Script encountered an error"' ERR
cdroot
- start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "http://127.0.0.1:3000" --dangerous-allow-cors-requests=true --experiments "*,moons" "$@"
+ start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable --access-url "${CODER_DEV_ACCESS_URL}" --dangerous-allow-cors-requests=true "$@"
echo '== Waiting for Coder to become ready'
# Start the timeout in the background so interrupting this script
diff --git a/scripts/helm.sh b/scripts/helm.sh
index 33b556f0100a1..fa3283b1ac4bb 100755
--- a/scripts/helm.sh
+++ b/scripts/helm.sh
@@ -4,15 +4,14 @@
# .tgz file at the specified path, and may optionally push it to the Coder OSS
# repo.
#
-# ./helm.sh [--version 1.2.3] [--output path/to/coder.tgz] [--push]
+# ./helm.sh [--version 1.2.3] [--chart coder|provisioner] [--output path/to/coder.tgz]
#
# If no version is specified, defaults to the version from ./version.sh.
#
-# If no output path is specified, defaults to
-# "$repo_root/build/coder_helm_$version.tgz".
+# If no chart is specified, defaults to 'coder'
#
-# If the --push parameter is specified, the resulting artifact will be published
-# to the Coder OSS repo. This requires `gsutil` to be installed and configured.
+# If no output path is specified, defaults to
+# "$repo_root/build/$chart_helm_$version.tgz".
set -euo pipefail
# shellcheck source=scripts/lib.sh
@@ -20,9 +19,9 @@ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
version=""
output_path=""
-push=0
+chart=""
-args="$(getopt -o "" -l version:,output:,push -- "$@")"
+args="$(getopt -o "" -l version:,chart:,output:,push -- "$@")"
eval set -- "$args"
while true; do
case "$1" in
@@ -30,14 +29,14 @@ while true; do
version="$2"
shift 2
;;
+ --chart)
+ chart="$2"
+ shift 2
+ ;;
--output)
output_path="$(realpath "$2")"
shift 2
;;
- --push)
- push="1"
- shift
- ;;
--)
shift
break
@@ -54,10 +53,17 @@ if [[ "$version" == "" ]]; then
version="$(execrelative ./version.sh)"
fi
+if [[ "$chart" == "" ]]; then
+ chart="coder"
+fi
+if ! [[ "$chart" =~ ^(coder|provisioner)$ ]]; then
+ error "--chart value must be one of (coder, provisioner)"
+fi
+
if [[ "$output_path" == "" ]]; then
cdroot
mkdir -p build
- output_path="$(realpath "build/coder_helm_$version.tgz")"
+ output_path="$(realpath "build/${chart}_helm_${version}.tgz")"
fi
# Check dependencies
@@ -69,8 +75,10 @@ cdroot
temp_dir="$(mktemp -d)"
cdroot
-cd ./helm
-log "--- Packaging helm chart for version $version ($output_path)"
+cd "./helm/${chart}"
+log "--- Updating dependencies"
+helm dependency update .
+log "--- Packaging helm chart $chart for version $version ($output_path)"
helm package \
--version "$version" \
--app-version "$version" \
@@ -80,8 +88,3 @@ helm package \
log "Moving helm chart to $output_path"
cp "$temp_dir"/*.tgz "$output_path"
rm -rf "$temp_dir"
-
-if [[ "$push" == 1 ]]; then
- log "--- Publishing helm chart..."
- # TODO: figure out how/where we want to publish the helm chart
-fi
diff --git a/scripts/linux-pkg/coder.service b/scripts/linux-pkg/coder.service
index 4ff2cc260a9bf..32246491880d4 100644
--- a/scripts/linux-pkg/coder.service
+++ b/scripts/linux-pkg/coder.service
@@ -4,7 +4,7 @@ Documentation=https://coder.com/docs/coder-oss
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=/etc/coder.d/coder.env
-StartLimitIntervalSec=60
+StartLimitIntervalSec=10
StartLimitBurst=3
[Service]
diff --git a/scripts/metricsdocgen/main.go b/scripts/metricsdocgen/main.go
index fbeb148715c54..8589653172005 100644
--- a/scripts/metricsdocgen/main.go
+++ b/scripts/metricsdocgen/main.go
@@ -56,13 +56,13 @@ func main() {
}
}
-func readMetrics() ([]dto.MetricFamily, error) {
+func readMetrics() ([]*dto.MetricFamily, error) {
f, err := os.Open(metricsFile)
if err != nil {
return nil, xerrors.New("can't open metrics file")
}
- var metrics []dto.MetricFamily
+ var metrics []*dto.MetricFamily
decoder := expfmt.NewDecoder(f, expfmt.FmtProtoText)
for {
@@ -73,7 +73,7 @@ func readMetrics() ([]dto.MetricFamily, error) {
} else if err != nil {
return nil, err
}
- metrics = append(metrics, m)
+ metrics = append(metrics, &m)
}
sort.Slice(metrics, func(i, j int) bool {
@@ -90,7 +90,7 @@ func readPrometheusDoc() ([]byte, error) {
return doc, nil
}
-func updatePrometheusDoc(doc []byte, metricFamilies []dto.MetricFamily) ([]byte, error) {
+func updatePrometheusDoc(doc []byte, metricFamilies []*dto.MetricFamily) ([]byte, error) {
i := bytes.Index(doc, generatorPrefix)
if i < 0 {
return nil, xerrors.New("generator prefix tag not found")
diff --git a/scripts/migrate-ci/main.go b/scripts/migrate-ci/main.go
index 636067cf8dd9e..918fee8176e1f 100644
--- a/scripts/migrate-ci/main.go
+++ b/scripts/migrate-ci/main.go
@@ -4,8 +4,8 @@ import (
"database/sql"
"fmt"
- "github.com/coder/coder/coderd/database/migrations"
- "github.com/coder/coder/cryptorand"
+ "github.com/coder/coder/v2/coderd/database/migrations"
+ "github.com/coder/coder/v2/cryptorand"
)
func main() {
diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go
index ee06a49f21c31..d237227f693dc 100644
--- a/scripts/rbacgen/main.go
+++ b/scripts/rbacgen/main.go
@@ -81,7 +81,7 @@ func allResources(pkg *packages.Package) []string {
names := pkg.Types.Scope().Names()
for _, name := range names {
obj, ok := pkg.Types.Scope().Lookup(name).(*types.Var)
- if ok && obj.Type().String() == "github.com/coder/coder/coderd/rbac.Object" {
+ if ok && obj.Type().String() == "github.com/coder/coder/v2/coderd/rbac.Object" {
resources = append(resources, obj.Name())
}
}
diff --git a/scripts/release/check_commit_metadata.sh b/scripts/release/check_commit_metadata.sh
index 7ad78992d0e0c..02a39525365d4 100755
--- a/scripts/release/check_commit_metadata.sh
+++ b/scripts/release/check_commit_metadata.sh
@@ -82,16 +82,20 @@ main() {
--json mergeCommit,labels,author \
--jq '.[] | "\( .mergeCommit.oid ) author:\( .author.login ) labels:\(["label:\( .labels[].name )"] | join(" "))"'
)"
- mapfile -t pr_metadata_raw <<<"$pr_list_out"
+
declare -A authors labels
- for entry in "${pr_metadata_raw[@]}"; do
- commit_sha_long=${entry%% *}
- commit_author=${entry#* author:}
- commit_author=${commit_author%% *}
- authors[$commit_sha_long]=$commit_author
- all_labels=${entry#* labels:}
- labels[$commit_sha_long]=$all_labels
- done
+ if [[ -n $pr_list_out ]]; then
+ mapfile -t pr_metadata_raw <<<"$pr_list_out"
+
+ for entry in "${pr_metadata_raw[@]}"; do
+ commit_sha_long=${entry%% *}
+ commit_author=${entry#* author:}
+ commit_author=${commit_author%% *}
+ authors[$commit_sha_long]=$commit_author
+ all_labels=${entry#* labels:}
+ labels[$commit_sha_long]=$all_labels
+ done
+ fi
for commit in "${commits[@]}"; do
mapfile -d ' ' -t parts <<<"$commit"
diff --git a/scripts/rules.go b/scripts/rules.go
index 20d0c43f7b883..2154cc828a21a 100644
--- a/scripts/rules.go
+++ b/scripts/rules.go
@@ -29,7 +29,7 @@ import (
// explaining why it's ok and a nolint.
func dbauthzAuthorizationContext(m dsl.Matcher) {
m.Import("context")
- m.Import("github.com/coder/coder/coderd/database/dbauthz")
+ m.Import("github.com/coder/coder/v2/coderd/database/dbauthz")
m.Match(
`dbauthz.$f($c)`,
@@ -51,8 +51,12 @@ func xerrors(m dsl.Matcher) {
m.Import("fmt")
m.Import("golang.org/x/xerrors")
- m.Match("fmt.Errorf($*args)").
- Suggest("xerrors.New($args)").
+ m.Match("fmt.Errorf($arg)").
+ Suggest("xerrors.New($arg)").
+ Report("Use xerrors to provide additional stacktrace information!")
+
+ m.Match("fmt.Errorf($arg1, $*args)").
+ Suggest("xerrors.Errorf($arg1, $args)").
Report("Use xerrors to provide additional stacktrace information!")
m.Match("errors.$_($msg)").
@@ -65,10 +69,10 @@ func xerrors(m dsl.Matcher) {
//
//nolint:unused,deadcode,varnamelen
func databaseImport(m dsl.Matcher) {
- m.Import("github.com/coder/coder/coderd/database")
+ m.Import("github.com/coder/coder/v2/coderd/database")
m.Match("database.$_").
Report("Do not import any database types into codersdk").
- Where(m.File().PkgPath.Matches("github.com/coder/coder/codersdk"))
+ Where(m.File().PkgPath.Matches("github.com/coder/coder/v2/codersdk"))
}
// doNotCallTFailNowInsideGoroutine enforces not calling t.FailNow or
@@ -118,7 +122,7 @@ func doNotCallTFailNowInsideGoroutine(m dsl.Matcher) {
func useStandardTimeoutsAndDelaysInTests(m dsl.Matcher) {
m.Import("github.com/stretchr/testify/require")
m.Import("github.com/stretchr/testify/assert")
- m.Import("github.com/coder/coder/testutil")
+ m.Import("github.com/coder/coder/v2/testutil")
m.Match(`context.WithTimeout($ctx, $duration)`).
Where(m.File().Imports("testing") && !m.File().PkgPath.Matches("testutil$") && !m["duration"].Text.Matches("^testutil\\.")).
@@ -199,7 +203,7 @@ func InTx(m dsl.Matcher) {
// and ends with punctuation.
// There are ways around the linter, but this should work in the common cases.
func HttpAPIErrorMessage(m dsl.Matcher) {
- m.Import("github.com/coder/coder/coderd/httpapi")
+ m.Import("github.com/coder/coder/v2/coderd/httpapi")
isNotProperError := func(v dsl.Var) bool {
return v.Type.Is("string") &&
@@ -232,7 +236,7 @@ func HttpAPIErrorMessage(m dsl.Matcher) {
// HttpAPIReturn will report a linter violation if the http function is not
// returned after writing a response to the client.
func HttpAPIReturn(m dsl.Matcher) {
- m.Import("github.com/coder/coder/coderd/httpapi")
+ m.Import("github.com/coder/coder/v2/coderd/httpapi")
// Manually enumerate the httpapi function rather then a 'Where' condition
// as this is a bit more efficient.
diff --git a/site/.eslintignore b/site/.eslintignore
index 46023d091348a..71c636297172e 100644
--- a/site/.eslintignore
+++ b/site/.eslintignore
@@ -64,10 +64,13 @@ stats/
.././scaletest/terraform/.terraform.lock.hcl
../scaletest/terraform/secrets.tfvars
.terraform.tfstate.*
+
+# Nix
+result
# .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier.
-../helm/templates/*.yaml
+../helm/**/templates/*.yaml
# Terraform state files used in tests, these are automatically generated.
# Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json
diff --git a/site/.prettierignore b/site/.prettierignore
index 46023d091348a..71c636297172e 100644
--- a/site/.prettierignore
+++ b/site/.prettierignore
@@ -64,10 +64,13 @@ stats/
.././scaletest/terraform/.terraform.lock.hcl
../scaletest/terraform/secrets.tfvars
.terraform.tfstate.*
+
+# Nix
+result
# .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier.
-../helm/templates/*.yaml
+../helm/**/templates/*.yaml
# Terraform state files used in tests, these are automatically generated.
# Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json
diff --git a/site/.prettierrc.yaml b/site/.prettierrc.yaml
index 00ada8c6db07c..6188061f976fa 100644
--- a/site/.prettierrc.yaml
+++ b/site/.prettierrc.yaml
@@ -4,6 +4,7 @@
# formatting for prettier-supported files. See `.editorconfig` and
# `site/.editorconfig`for whitespace formatting options.
printWidth: 80
+proseWrap: always
semi: false
trailingComma: all
useTabs: false
@@ -11,10 +12,9 @@ tabWidth: 2
overrides:
- files:
- ../README.md
+ - ../docs/api/**/*.md
+ - ../docs/cli/**/*.md
+ - ../.github/**/*.{yaml,yml,toml}
+ - ../scripts/**/*.{yaml,yml,toml}
options:
proseWrap: preserve
- - files:
- - ./**/*.yaml
- - ./**/*.yml
- options:
- proseWrap: always
diff --git a/site/.storybook/main.js b/site/.storybook/main.js
index f22249ddbfe83..6fed6b2997d30 100644
--- a/site/.storybook/main.js
+++ b/site/.storybook/main.js
@@ -1,3 +1,6 @@
+import turbosnap from "vite-plugin-turbosnap"
+import { mergeConfig } from "vite"
+
module.exports = {
stories: ["../src/**/*.stories.tsx"],
addons: [
@@ -11,4 +14,17 @@ module.exports = {
name: "@storybook/react-vite",
options: {},
},
+ async viteFinal(config, { configType }) {
+ config.plugins = config.plugins || []
+ // return the customized config
+ if (configType === "PRODUCTION") {
+ // ignore @ts-ignore because it's not in the vite types yet
+ config.plugins.push(
+ turbosnap({
+ rootDir: config.root || "",
+ }),
+ )
+ }
+ return config
+ },
}
diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts
index dfbd5f99896a2..756c307ff59c9 100644
--- a/site/e2e/helpers.ts
+++ b/site/e2e/helpers.ts
@@ -8,34 +8,105 @@ import {
Agent,
App,
AppSharingLevel,
- Parse_Complete,
- Parse_Response,
- Provision_Complete,
- Provision_Response,
+ Response,
+ ParseComplete,
+ PlanComplete,
+ ApplyComplete,
Resource,
+ RichParameter,
} from "./provisionerGenerated"
import { port } from "./playwright.config"
import * as ssh from "ssh2"
import { Duplex } from "stream"
+import { WorkspaceBuildParameter } from "api/typesGenerated"
// createWorkspace creates a workspace for a template.
// It does not wait for it to be running, but it does navigate to the page.
export const createWorkspace = async (
page: Page,
templateName: string,
+ richParameters: RichParameter[] = [],
+ buildParameters: WorkspaceBuildParameter[] = [],
): Promise => {
await page.goto("/templates/" + templateName + "/workspace", {
waitUntil: "networkidle",
})
const name = randomName()
await page.getByLabel("name").fill(name)
+
+ await fillParameters(page, richParameters, buildParameters)
await page.getByTestId("form-submit").click()
await expect(page).toHaveURL("/@admin/" + name)
- await page.getByTestId("build-status").isVisible()
+
+ await page.waitForSelector(
+ "span[data-testid='build-status'] >> text=Running",
+ {
+ state: "visible",
+ },
+ )
return name
}
+export const verifyParameters = async (
+ page: Page,
+ workspaceName: string,
+ richParameters: RichParameter[],
+ expectedBuildParameters: WorkspaceBuildParameter[],
+) => {
+ await page.goto("/@admin/" + workspaceName + "/settings/parameters", {
+ waitUntil: "networkidle",
+ })
+ await expect(page).toHaveURL(
+ "/@admin/" + workspaceName + "/settings/parameters",
+ )
+
+ for (const buildParameter of expectedBuildParameters) {
+ const richParameter = richParameters.find(
+ (richParam) => richParam.name === buildParameter.name,
+ )
+ if (!richParameter) {
+ throw new Error(
+ "build parameter is expected to be present in rich parameter schema",
+ )
+ }
+
+ const parameterLabel = await page.waitForSelector(
+ "[data-testid='parameter-field-" + richParameter.name + "']",
+ { state: "visible" },
+ )
+
+ const muiDisabled = richParameter.mutable ? "" : ".Mui-disabled"
+
+ if (richParameter.type === "bool") {
+ const parameterField = await parameterLabel.waitForSelector(
+ "[data-testid='parameter-field-bool'] .MuiRadio-root.Mui-checked" +
+ muiDisabled +
+ " input",
+ )
+ const value = await parameterField.inputValue()
+ expect(value).toEqual(buildParameter.value)
+ } else if (richParameter.options.length > 0) {
+ const parameterField = await parameterLabel.waitForSelector(
+ "[data-testid='parameter-field-options'] .MuiRadio-root.Mui-checked" +
+ muiDisabled +
+ " input",
+ )
+ const value = await parameterField.inputValue()
+ expect(value).toEqual(buildParameter.value)
+ } else if (richParameter.type === "list(string)") {
+ throw new Error("not implemented yet") // FIXME
+ } else {
+ // text or number
+ const parameterField = await parameterLabel.waitForSelector(
+ "[data-testid='parameter-field-text'] input" + muiDisabled,
+ )
+ const value = await parameterField.inputValue()
+ expect(value).toEqual(buildParameter.value)
+ }
+ }
+}
+
// createTemplate navigates to the /templates/new page and uploads a template
// with the resources provided in the responses argument.
export const createTemplate = async (
@@ -65,20 +136,21 @@ export const createTemplate = async (
export const sshIntoWorkspace = async (
page: Page,
workspace: string,
+ binaryPath = "go",
+ binaryArgs: string[] = [],
): Promise => {
+ if (binaryPath === "go") {
+ binaryArgs = ["run", coderMainPath()]
+ }
const sessionToken = await findSessionToken(page)
return new Promise((resolve, reject) => {
- const cp = spawn(
- "go",
- ["run", coderMainPath(), "ssh", "--stdio", workspace],
- {
- env: {
- ...process.env,
- CODER_SESSION_TOKEN: sessionToken,
- CODER_URL: "http://localhost:3000",
- },
+ const cp = spawn(binaryPath, [...binaryArgs, "ssh", "--stdio", workspace], {
+ env: {
+ ...process.env,
+ CODER_SESSION_TOKEN: sessionToken,
+ CODER_URL: "http://localhost:3000",
},
- )
+ })
cp.on("error", (err) => reject(err))
const proxyStream = new Duplex({
read: (size) => {
@@ -106,6 +178,50 @@ export const sshIntoWorkspace = async (
})
}
+export const stopWorkspace = async (page: Page, workspaceName: string) => {
+ await page.goto("/@admin/" + workspaceName, {
+ waitUntil: "domcontentloaded",
+ })
+ await expect(page).toHaveURL("/@admin/" + workspaceName)
+
+ await page.getByTestId("workspace-stop-button").click()
+
+ await page.waitForSelector(
+ "span[data-testid='build-status'] >> text=Stopped",
+ {
+ state: "visible",
+ },
+ )
+}
+
+export const buildWorkspaceWithParameters = async (
+ page: Page,
+ workspaceName: string,
+ richParameters: RichParameter[] = [],
+ buildParameters: WorkspaceBuildParameter[] = [],
+ confirm: boolean = false,
+) => {
+ await page.goto("/@admin/" + workspaceName, {
+ waitUntil: "domcontentloaded",
+ })
+ await expect(page).toHaveURL("/@admin/" + workspaceName)
+
+ await page.getByTestId("build-parameters-button").click()
+
+ await fillParameters(page, richParameters, buildParameters)
+ await page.getByTestId("build-parameters-submit").click()
+ if (confirm) {
+ await page.getByTestId("confirm-button").click()
+ }
+
+ await page.waitForSelector(
+ "span[data-testid='build-status'] >> text=Running",
+ {
+ state: "visible",
+ },
+ )
+}
+
// startAgent runs the coder agent with the provided token.
// It awaits the agent to be ready before returning.
export const startAgent = async (page: Page, token: string): Promise => {
@@ -122,7 +238,7 @@ export const downloadCoderVersion = async (
}
const binaryName = "coder-e2e-" + version
- const tempDir = "/tmp"
+ const tempDir = "/tmp/coder-e2e-cache"
// The install script adds `./bin` automatically to the path :shrug:
const binaryPath = path.join(tempDir, "bin", binaryName)
@@ -140,26 +256,35 @@ export const downloadCoderVersion = async (
// Runs our public install script using our options to
// install the binary!
await new Promise((resolve, reject) => {
- const cp = spawn("sh", [
- "-c",
+ const cp = spawn(
+ "sh",
[
- "curl",
- "-L",
- "https://coder.com/install.sh",
- "|",
- "sh",
- "-s",
- "--",
- "--version",
- version,
- "--method",
- "standalone",
- "--prefix",
- tempDir,
- "--binary-name",
- binaryName,
- ].join(" "),
- ])
+ "-c",
+ [
+ "curl",
+ "-L",
+ "https://coder.com/install.sh",
+ "|",
+ "sh",
+ "-s",
+ "--",
+ "--version",
+ version,
+ "--method",
+ "standalone",
+ "--prefix",
+ tempDir,
+ "--binary-name",
+ binaryName,
+ ].join(" "),
+ ],
+ {
+ env: {
+ ...process.env,
+ XDG_CACHE_HOME: "/tmp/coder-e2e-cache",
+ },
+ },
+ )
// eslint-disable-next-line no-console -- Needed for debugging
cp.stderr.on("data", (data) => console.log(data.toString()))
cp.on("close", (code) => {
@@ -191,7 +316,7 @@ export const startAgentWithCommand = async (
buffer = Buffer.concat([buffer, data])
})
try {
- await page.getByTestId("agent-status-ready").isVisible()
+ await page.getByTestId("agent-status-ready").waitFor({ state: "visible" })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The error is a string
} catch (ex: any) {
throw new Error(ex.toString() + "\n" + buffer.toString())
@@ -221,11 +346,11 @@ type RecursivePartial = {
interface EchoProvisionerResponses {
// parse is for observing any Terraform variables
- parse?: RecursivePartial[]
+ parse?: RecursivePartial[]
// plan occurs when the template is imported
- plan?: RecursivePartial[]
+ plan?: RecursivePartial[]
// apply occurs when the workspace is built
- apply?: RecursivePartial[]
+ apply?: RecursivePartial[]
}
// createTemplateVersionTar consumes a series of echo provisioner protobufs and
@@ -237,109 +362,133 @@ const createTemplateVersionTar = async (
responses = {}
}
if (!responses.parse) {
- responses.parse = [{}]
+ responses.parse = [
+ {
+ parse: {},
+ },
+ ]
}
if (!responses.apply) {
- responses.apply = [{}]
+ responses.apply = [
+ {
+ apply: {},
+ },
+ ]
}
if (!responses.plan) {
- responses.plan = responses.apply
+ responses.plan = responses.apply.map((response) => {
+ if (response.log) {
+ return response
+ }
+ return {
+ plan: {
+ error: response.apply?.error ?? "",
+ resources: response.apply?.resources ?? [],
+ parameters: response.apply?.parameters ?? [],
+ gitAuthProviders: response.apply?.gitAuthProviders ?? [],
+ },
+ }
+ })
}
const tar = new TarWriter()
responses.parse.forEach((response, index) => {
- response.complete = {
+ response.parse = {
templateVariables: [],
- ...response.complete,
- } as Parse_Complete
+ error: "",
+ readme: new Uint8Array(),
+ ...response.parse,
+ } as ParseComplete
tar.addFile(
`${index}.parse.protobuf`,
- Parse_Response.encode(response as Parse_Response).finish(),
+ Response.encode(response as Response).finish(),
)
})
- const fillProvisionResponse = (
- response: RecursivePartial,
- ) => {
- response.complete = {
+ const fillResource = (resource: RecursivePartial) => {
+ if (resource.agents) {
+ resource.agents = resource.agents?.map(
+ (agent: RecursivePartial) => {
+ if (agent.apps) {
+ agent.apps = agent.apps?.map((app: RecursivePartial) => {
+ return {
+ command: "",
+ displayName: "example",
+ external: false,
+ icon: "",
+ sharingLevel: AppSharingLevel.PUBLIC,
+ slug: "example",
+ subdomain: false,
+ url: "",
+ ...app,
+ } as App
+ })
+ }
+ return {
+ apps: [],
+ architecture: "amd64",
+ connectionTimeoutSeconds: 300,
+ directory: "",
+ env: {},
+ id: randomUUID(),
+ metadata: [],
+ motdFile: "",
+ name: "dev",
+ operatingSystem: "linux",
+ shutdownScript: "",
+ shutdownScriptTimeoutSeconds: 0,
+ startupScript: "",
+ startupScriptBehavior: "",
+ startupScriptTimeoutSeconds: 300,
+ troubleshootingUrl: "",
+ token: randomUUID(),
+ ...agent,
+ } as Agent
+ },
+ )
+ }
+ return {
+ agents: [],
+ dailyCost: 0,
+ hide: false,
+ icon: "",
+ instanceType: "",
+ metadata: [],
+ name: "dev",
+ type: "echo",
+ ...resource,
+ } as Resource
+ }
+
+ responses.apply.forEach((response, index) => {
+ response.apply = {
error: "",
state: new Uint8Array(),
resources: [],
parameters: [],
gitAuthProviders: [],
- plan: new Uint8Array(),
- ...response.complete,
- } as Provision_Complete
- response.complete.resources = response.complete.resources?.map(
- (resource) => {
- if (resource.agents) {
- resource.agents = resource.agents?.map((agent) => {
- if (agent.apps) {
- agent.apps = agent.apps?.map((app) => {
- return {
- command: "",
- displayName: "example",
- external: false,
- icon: "",
- sharingLevel: AppSharingLevel.PUBLIC,
- slug: "example",
- subdomain: false,
- url: "",
- ...app,
- } as App
- })
- }
- return {
- apps: [],
- architecture: "amd64",
- connectionTimeoutSeconds: 300,
- directory: "",
- env: {},
- id: randomUUID(),
- metadata: [],
- motdFile: "",
- name: "dev",
- operatingSystem: "linux",
- shutdownScript: "",
- shutdownScriptTimeoutSeconds: 0,
- startupScript: "",
- startupScriptBehavior: "",
- startupScriptTimeoutSeconds: 300,
- troubleshootingUrl: "",
- token: randomUUID(),
- ...agent,
- } as Agent
- })
- }
- return {
- agents: [],
- dailyCost: 0,
- hide: false,
- icon: "",
- instanceType: "",
- metadata: [],
- name: "dev",
- type: "echo",
- ...resource,
- } as Resource
- },
- )
- }
-
- responses.apply.forEach((response, index) => {
- fillProvisionResponse(response)
+ ...response.apply,
+ } as ApplyComplete
+ response.apply.resources = response.apply.resources?.map(fillResource)
tar.addFile(
- `${index}.provision.apply.protobuf`,
- Provision_Response.encode(response as Provision_Response).finish(),
+ `${index}.apply.protobuf`,
+ Response.encode(response as Response).finish(),
)
})
responses.plan.forEach((response, index) => {
- fillProvisionResponse(response)
+ response.plan = {
+ error: "",
+ resources: [],
+ parameters: [],
+ gitAuthProviders: [],
+ ...response.plan,
+ } as PlanComplete
+ response.plan.resources = response.plan.resources?.map(fillResource)
tar.addFile(
- `${index}.provision.plan.protobuf`,
- Provision_Response.encode(response as Provision_Response).finish(),
+ `${index}.plan.protobuf`,
+ Response.encode(response as Response).finish(),
)
})
const tarFile = await tar.write()
@@ -391,3 +540,79 @@ const findSessionToken = async (page: Page): Promise => {
}
return sessionCookie.value
}
+
+export const echoResponsesWithParameters = (
+ richParameters: RichParameter[],
+): EchoProvisionerResponses => {
+ return {
+ parse: [
+ {
+ parse: {},
+ },
+ ],
+ plan: [
+ {
+ plan: {
+ parameters: richParameters,
+ },
+ },
+ ],
+ apply: [
+ {
+ apply: {
+ resources: [
+ {
+ name: "example",
+ },
+ ],
+ },
+ },
+ ],
+ }
+}
+
+export const fillParameters = async (
+ page: Page,
+ richParameters: RichParameter[] = [],
+ buildParameters: WorkspaceBuildParameter[] = [],
+) => {
+ for (const buildParameter of buildParameters) {
+ const richParameter = richParameters.find(
+ (richParam) => richParam.name === buildParameter.name,
+ )
+ if (!richParameter) {
+ throw new Error(
+ "build parameter is expected to be present in rich parameter schema",
+ )
+ }
+
+ const parameterLabel = await page.waitForSelector(
+ "[data-testid='parameter-field-" + richParameter.name + "']",
+ { state: "visible" },
+ )
+
+ if (richParameter.type === "bool") {
+ const parameterField = await parameterLabel.waitForSelector(
+ "[data-testid='parameter-field-bool'] .MuiRadio-root input[value='" +
+ buildParameter.value +
+ "']",
+ )
+ await parameterField.check()
+ } else if (richParameter.options.length > 0) {
+ const parameterField = await parameterLabel.waitForSelector(
+ "[data-testid='parameter-field-options'] .MuiRadio-root input[value='" +
+ buildParameter.value +
+ "']",
+ )
+ await parameterField.check()
+ } else if (richParameter.type === "list(string)") {
+ throw new Error("not implemented yet") // FIXME
+ } else {
+ // text or number
+ const parameterField = await parameterLabel.waitForSelector(
+ "[data-testid='parameter-field-text'] input",
+ )
+ await parameterField.fill(buildParameter.value)
+ }
+ }
+}
diff --git a/site/e2e/parameters.ts b/site/e2e/parameters.ts
new file mode 100644
index 0000000000000..240eeb0f8566c
--- /dev/null
+++ b/site/e2e/parameters.ts
@@ -0,0 +1,156 @@
+import { RichParameter } from "./provisionerGenerated"
+
+// Rich parameters
+
+const emptyParameter: RichParameter = {
+ name: "",
+ description: "",
+ type: "",
+ mutable: false,
+ defaultValue: "",
+ icon: "",
+ options: [],
+ validationRegex: "",
+ validationError: "",
+ validationMin: undefined,
+ validationMax: undefined,
+ validationMonotonic: "",
+ required: false,
+ displayName: "",
+ order: 0,
+ ephemeral: false,
+}
+
+// firstParameter is mutable string with a default value (parameter value not required).
+export const firstParameter: RichParameter = {
+ ...emptyParameter,
+
+ name: "first_parameter",
+ displayName: "First parameter",
+ type: "number",
+ description: "This is first parameter.",
+ icon: "/emojis/1f310.png",
+ defaultValue: "123",
+ mutable: true,
+ order: 1,
+}
+
+// secondParameter is immutable string with a default value (parameter value not required).
+export const secondParameter: RichParameter = {
+ ...emptyParameter,
+
+ name: "second_parameter",
+ displayName: "Second parameter",
+ type: "string",
+ description: "This is second parameter.",
+ defaultValue: "abc",
+ order: 2,
+}
+
+// thirdParameter is mutable string with an empty default value (parameter value not required).
+export const thirdParameter: RichParameter = {
+ ...emptyParameter,
+
+ name: "third_parameter",
+ type: "string",
+ description: "This is third parameter.",
+ defaultValue: "",
+ mutable: true,
+ order: 3,
+}
+
+// fourthParameter is immutable boolean with a default "true" value (parameter value not required).
+export const fourthParameter: RichParameter = {
+ ...emptyParameter,
+
+ name: "fourth_parameter",
+ type: "bool",
+ description: "This is fourth parameter.",
+ defaultValue: "true",
+ order: 3,
+}
+
+// fifthParameter is immutable "string with options", with a default option selected (parameter value not required).
+export const fifthParameter: RichParameter = {
+ ...emptyParameter,
+
+ name: "fifth_parameter",
+ displayName: "Fifth parameter",
+ type: "string",
+ options: [
+ {
+ name: "ABC",
+ description: "This is ABC",
+ value: "abc",
+ icon: "",
+ },
+ {
+ name: "DEF",
+ description: "This is DEF",
+ value: "def",
+ icon: "",
+ },
+ {
+ name: "GHI",
+ description: "This is GHI",
+ value: "ghi",
+ icon: "",
+ },
+ ],
+ description: "This is fifth parameter.",
+ defaultValue: "def",
+ order: 3,
+}
+
+// sixthParameter is mutable string without a default value (parameter value is required).
+export const sixthParameter: RichParameter = {
+ ...emptyParameter,
+
+ name: "sixth_parameter",
+ displayName: "Sixth parameter",
+ type: "number",
+ description: "This is sixth parameter.",
+ icon: "/emojis/1f310.png",
+ required: true,
+ mutable: true,
+ order: 1,
+}
+
+// seventhParameter is immutable string without a default value (parameter value is required).
+export const seventhParameter: RichParameter = {
+ ...emptyParameter,
+
+ name: "seventh_parameter",
+ displayName: "Seventh parameter",
+ type: "string",
+ description: "This is seventh parameter.",
+ required: true,
+ order: 1,
+}
+
+// Build options
+
+export const firstBuildOption: RichParameter = {
+ ...emptyParameter,
+
+ name: "first_build_option",
+ displayName: "First build option",
+ type: "string",
+ description: "This is first build option.",
+ icon: "/emojis/1f310.png",
+ defaultValue: "ABCDEF",
+ mutable: true,
+ ephemeral: true,
+}
+
+export const secondBuildOption: RichParameter = {
+ ...emptyParameter,
+
+ name: "second_build_option",
+ displayName: "Second build option",
+ type: "bool",
+ description: "This is second build option.",
+ defaultValue: "false",
+ mutable: true,
+ ephemeral: true,
+}
diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts
index 0ff9a6fe74622..78eb4495ce371 100644
--- a/site/e2e/playwright.config.ts
+++ b/site/e2e/playwright.config.ts
@@ -27,6 +27,7 @@ export default defineConfig({
use: {
storageState: STORAGE_STATE,
},
+ timeout: 60000,
},
],
use: {
diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts
index 6acdde763f32c..d79d5b422282c 100644
--- a/site/e2e/provisionerGenerated.ts
+++ b/site/e2e/provisionerGenerated.ts
@@ -21,6 +21,7 @@ export enum AppSharingLevel {
UNRECOGNIZED = -1,
}
+/** WorkspaceTransition is the desired outcome of a build */
export enum WorkspaceTransition {
START = 0,
STOP = 1,
@@ -64,7 +65,10 @@ export interface RichParameter {
validationMax?: number | undefined
validationMonotonic: string
required: boolean
+ /** legacy_variable_name was removed (= 14) */
displayName: string
+ order: number
+ ephemeral: boolean
}
/** RichParameterValue holds the key/value mapping of a parameter. */
@@ -174,29 +178,8 @@ export interface Resource_Metadata {
isNull: boolean
}
-/** Parse consumes source-code from a directory to produce inputs. */
-export interface Parse {}
-
-export interface Parse_Request {
- directory: string
-}
-
-export interface Parse_Complete {
- templateVariables: TemplateVariable[]
-}
-
-export interface Parse_Response {
- log?: Log | undefined
- complete?: Parse_Complete | undefined
-}
-
-/**
- * Provision consumes source-code from a directory to produce resources.
- * Exactly one of Plan or Apply must be provided in a single session.
- */
-export interface Provision {}
-
-export interface Provision_Metadata {
+/** Metadata is information about a workspace used in the execution of a build */
+export interface Metadata {
coderUrl: string
workspaceTransition: WorkspaceTransition
workspaceName: string
@@ -210,49 +193,74 @@ export interface Provision_Metadata {
workspaceOwnerSessionToken: string
}
-/**
- * Config represents execution configuration shared by both Plan and
- * Apply commands.
- */
-export interface Provision_Config {
- directory: string
+/** Config represents execution configuration shared by all subsequent requests in the Session */
+export interface Config {
+ /** template_source_archive is a tar of the template source files */
+ templateSourceArchive: Uint8Array
+ /** state is the provisioner state (if any) */
state: Uint8Array
- metadata: Provision_Metadata | undefined
provisionerLogLevel: string
}
-export interface Provision_Plan {
- config: Provision_Config | undefined
+/** ParseRequest consumes source-code to produce inputs. */
+export interface ParseRequest {}
+
+/** ParseComplete indicates a request to parse completed. */
+export interface ParseComplete {
+ error: string
+ templateVariables: TemplateVariable[]
+ readme: Uint8Array
+}
+
+/** PlanRequest asks the provisioner to plan what resources & parameters it will create */
+export interface PlanRequest {
+ metadata: Metadata | undefined
richParameterValues: RichParameterValue[]
variableValues: VariableValue[]
gitAuthProviders: GitAuthProvider[]
}
-export interface Provision_Apply {
- config: Provision_Config | undefined
- plan: Uint8Array
+/** PlanComplete indicates a request to plan completed. */
+export interface PlanComplete {
+ error: string
+ resources: Resource[]
+ parameters: RichParameter[]
+ gitAuthProviders: string[]
}
-export interface Provision_Cancel {}
-
-export interface Provision_Request {
- plan?: Provision_Plan | undefined
- apply?: Provision_Apply | undefined
- cancel?: Provision_Cancel | undefined
+/**
+ * ApplyRequest asks the provisioner to apply the changes. Apply MUST be preceded by a successful plan request/response
+ * in the same Session. The plan data is not transmitted over the wire and is cached by the provisioner in the Session.
+ */
+export interface ApplyRequest {
+ metadata: Metadata | undefined
}
-export interface Provision_Complete {
+/** ApplyComplete indicates a request to apply completed. */
+export interface ApplyComplete {
state: Uint8Array
error: string
resources: Resource[]
parameters: RichParameter[]
gitAuthProviders: string[]
- plan: Uint8Array
}
-export interface Provision_Response {
+/** CancelRequest requests that the previous request be canceled gracefully. */
+export interface CancelRequest {}
+
+export interface Request {
+ config?: Config | undefined
+ parse?: ParseRequest | undefined
+ plan?: PlanRequest | undefined
+ apply?: ApplyRequest | undefined
+ cancel?: CancelRequest | undefined
+}
+
+export interface Response {
log?: Log | undefined
- complete?: Provision_Complete | undefined
+ parse?: ParseComplete | undefined
+ plan?: PlanComplete | undefined
+ apply?: ApplyComplete | undefined
}
export const Empty = {
@@ -356,6 +364,12 @@ export const RichParameter = {
if (message.displayName !== "") {
writer.uint32(122).string(message.displayName)
}
+ if (message.order !== 0) {
+ writer.uint32(128).int32(message.order)
+ }
+ if (message.ephemeral === true) {
+ writer.uint32(136).bool(message.ephemeral)
+ }
return writer
},
}
@@ -639,60 +653,9 @@ export const Resource_Metadata = {
},
}
-export const Parse = {
- encode(_: Parse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
- return writer
- },
-}
-
-export const Parse_Request = {
- encode(
- message: Parse_Request,
- writer: _m0.Writer = _m0.Writer.create(),
- ): _m0.Writer {
- if (message.directory !== "") {
- writer.uint32(10).string(message.directory)
- }
- return writer
- },
-}
-
-export const Parse_Complete = {
- encode(
- message: Parse_Complete,
- writer: _m0.Writer = _m0.Writer.create(),
- ): _m0.Writer {
- for (const v of message.templateVariables) {
- TemplateVariable.encode(v!, writer.uint32(10).fork()).ldelim()
- }
- return writer
- },
-}
-
-export const Parse_Response = {
- encode(
- message: Parse_Response,
- writer: _m0.Writer = _m0.Writer.create(),
- ): _m0.Writer {
- if (message.log !== undefined) {
- Log.encode(message.log, writer.uint32(10).fork()).ldelim()
- }
- if (message.complete !== undefined) {
- Parse_Complete.encode(message.complete, writer.uint32(18).fork()).ldelim()
- }
- return writer
- },
-}
-
-export const Provision = {
- encode(_: Provision, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
- return writer
- },
-}
-
-export const Provision_Metadata = {
+export const Metadata = {
encode(
- message: Provision_Metadata,
+ message: Metadata,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.coderUrl !== "") {
@@ -732,96 +695,108 @@ export const Provision_Metadata = {
},
}
-export const Provision_Config = {
+export const Config = {
encode(
- message: Provision_Config,
+ message: Config,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
- if (message.directory !== "") {
- writer.uint32(10).string(message.directory)
+ if (message.templateSourceArchive.length !== 0) {
+ writer.uint32(10).bytes(message.templateSourceArchive)
}
if (message.state.length !== 0) {
writer.uint32(18).bytes(message.state)
}
- if (message.metadata !== undefined) {
- Provision_Metadata.encode(
- message.metadata,
- writer.uint32(26).fork(),
- ).ldelim()
- }
if (message.provisionerLogLevel !== "") {
- writer.uint32(34).string(message.provisionerLogLevel)
+ writer.uint32(26).string(message.provisionerLogLevel)
}
return writer
},
}
-export const Provision_Plan = {
+export const ParseRequest = {
encode(
- message: Provision_Plan,
+ _: ParseRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
- if (message.config !== undefined) {
- Provision_Config.encode(message.config, writer.uint32(10).fork()).ldelim()
+ return writer
+ },
+}
+
+export const ParseComplete = {
+ encode(
+ message: ParseComplete,
+ writer: _m0.Writer = _m0.Writer.create(),
+ ): _m0.Writer {
+ if (message.error !== "") {
+ writer.uint32(10).string(message.error)
}
- for (const v of message.richParameterValues) {
- RichParameterValue.encode(v!, writer.uint32(26).fork()).ldelim()
+ for (const v of message.templateVariables) {
+ TemplateVariable.encode(v!, writer.uint32(18).fork()).ldelim()
}
- for (const v of message.variableValues) {
- VariableValue.encode(v!, writer.uint32(34).fork()).ldelim()
- }
- for (const v of message.gitAuthProviders) {
- GitAuthProvider.encode(v!, writer.uint32(42).fork()).ldelim()
+ if (message.readme.length !== 0) {
+ writer.uint32(26).bytes(message.readme)
}
return writer
},
}
-export const Provision_Apply = {
+export const PlanRequest = {
encode(
- message: Provision_Apply,
+ message: PlanRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
- if (message.config !== undefined) {
- Provision_Config.encode(message.config, writer.uint32(10).fork()).ldelim()
+ if (message.metadata !== undefined) {
+ Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim()
}
- if (message.plan.length !== 0) {
- writer.uint32(18).bytes(message.plan)
+ for (const v of message.richParameterValues) {
+ RichParameterValue.encode(v!, writer.uint32(18).fork()).ldelim()
+ }
+ for (const v of message.variableValues) {
+ VariableValue.encode(v!, writer.uint32(26).fork()).ldelim()
+ }
+ for (const v of message.gitAuthProviders) {
+ GitAuthProvider.encode(v!, writer.uint32(34).fork()).ldelim()
}
return writer
},
}
-export const Provision_Cancel = {
+export const PlanComplete = {
encode(
- _: Provision_Cancel,
+ message: PlanComplete,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
+ if (message.error !== "") {
+ writer.uint32(10).string(message.error)
+ }
+ for (const v of message.resources) {
+ Resource.encode(v!, writer.uint32(18).fork()).ldelim()
+ }
+ for (const v of message.parameters) {
+ RichParameter.encode(v!, writer.uint32(26).fork()).ldelim()
+ }
+ for (const v of message.gitAuthProviders) {
+ writer.uint32(34).string(v!)
+ }
return writer
},
}
-export const Provision_Request = {
+export const ApplyRequest = {
encode(
- message: Provision_Request,
+ message: ApplyRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
- if (message.plan !== undefined) {
- Provision_Plan.encode(message.plan, writer.uint32(10).fork()).ldelim()
- }
- if (message.apply !== undefined) {
- Provision_Apply.encode(message.apply, writer.uint32(18).fork()).ldelim()
- }
- if (message.cancel !== undefined) {
- Provision_Cancel.encode(message.cancel, writer.uint32(26).fork()).ldelim()
+ if (message.metadata !== undefined) {
+ Metadata.encode(message.metadata, writer.uint32(10).fork()).ldelim()
}
return writer
},
}
-export const Provision_Complete = {
+export const ApplyComplete = {
encode(
- message: Provision_Complete,
+ message: ApplyComplete,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.state.length !== 0) {
@@ -839,34 +814,76 @@ export const Provision_Complete = {
for (const v of message.gitAuthProviders) {
writer.uint32(42).string(v!)
}
- if (message.plan.length !== 0) {
- writer.uint32(50).bytes(message.plan)
+ return writer
+ },
+}
+
+export const CancelRequest = {
+ encode(
+ _: CancelRequest,
+ writer: _m0.Writer = _m0.Writer.create(),
+ ): _m0.Writer {
+ return writer
+ },
+}
+
+export const Request = {
+ encode(
+ message: Request,
+ writer: _m0.Writer = _m0.Writer.create(),
+ ): _m0.Writer {
+ if (message.config !== undefined) {
+ Config.encode(message.config, writer.uint32(10).fork()).ldelim()
+ }
+ if (message.parse !== undefined) {
+ ParseRequest.encode(message.parse, writer.uint32(18).fork()).ldelim()
+ }
+ if (message.plan !== undefined) {
+ PlanRequest.encode(message.plan, writer.uint32(26).fork()).ldelim()
+ }
+ if (message.apply !== undefined) {
+ ApplyRequest.encode(message.apply, writer.uint32(34).fork()).ldelim()
+ }
+ if (message.cancel !== undefined) {
+ CancelRequest.encode(message.cancel, writer.uint32(42).fork()).ldelim()
}
return writer
},
}
-export const Provision_Response = {
+export const Response = {
encode(
- message: Provision_Response,
+ message: Response,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.log !== undefined) {
Log.encode(message.log, writer.uint32(10).fork()).ldelim()
}
- if (message.complete !== undefined) {
- Provision_Complete.encode(
- message.complete,
- writer.uint32(18).fork(),
- ).ldelim()
+ if (message.parse !== undefined) {
+ ParseComplete.encode(message.parse, writer.uint32(18).fork()).ldelim()
+ }
+ if (message.plan !== undefined) {
+ PlanComplete.encode(message.plan, writer.uint32(26).fork()).ldelim()
+ }
+ if (message.apply !== undefined) {
+ ApplyComplete.encode(message.apply, writer.uint32(34).fork()).ldelim()
}
return writer
},
}
export interface Provisioner {
- Parse(request: Parse_Request): Observable
- Provision(
- request: Observable,
- ): Observable
+ /**
+ * Session represents provisioning a single template import or workspace. The daemon always sends Config followed
+ * by one of the requests (ParseRequest, PlanRequest, ApplyRequest). The provisioner should respond with a stream
+ * of zero or more Logs, followed by the corresponding complete message (ParseComplete, PlanComplete,
+ * ApplyComplete). The daemon may then send a new request. A request to apply MUST be preceded by a request plan,
+ * and the provisioner should store the plan data on the Session after a successful plan, so that the daemon may
+ * request an apply. If the daemon closes the Session without an apply, the plan data may be safely discarded.
+ *
+ * The daemon may send a CancelRequest, asynchronously to ask the provisioner to cancel the previous ParseRequest,
+ * PlanRequest, or ApplyRequest. The provisioner MUST reply with a complete message corresponding to the request
+ * that was canceled. If the provisioner has already completed the request, it may ignore the CancelRequest.
+ */
+ Session(request: Observable): Observable
}
diff --git a/site/e2e/tests/app.spec.ts b/site/e2e/tests/app.spec.ts
index b3646fbac1caa..aa69475dc8184 100644
--- a/site/e2e/tests/app.spec.ts
+++ b/site/e2e/tests/app.spec.ts
@@ -20,7 +20,7 @@ test("app", async ({ context, page }) => {
const template = await createTemplate(page, {
apply: [
{
- complete: {
+ apply: {
resources: [
{
agents: [
diff --git a/site/e2e/tests/createWorkspace.spec.ts b/site/e2e/tests/createWorkspace.spec.ts
index 3317691300f5f..1effc01976651 100644
--- a/site/e2e/tests/createWorkspace.spec.ts
+++ b/site/e2e/tests/createWorkspace.spec.ts
@@ -1,11 +1,27 @@
import { test } from "@playwright/test"
-import { createTemplate, createWorkspace } from "../helpers"
+import {
+ createTemplate,
+ createWorkspace,
+ echoResponsesWithParameters,
+ verifyParameters,
+} from "../helpers"
+
+import {
+ secondParameter,
+ fourthParameter,
+ fifthParameter,
+ firstParameter,
+ thirdParameter,
+ seventhParameter,
+ sixthParameter,
+} from "../parameters"
+import { RichParameter } from "../provisionerGenerated"
test("create workspace", async ({ page }) => {
const template = await createTemplate(page, {
apply: [
{
- complete: {
+ apply: {
resources: [
{
name: "example",
@@ -17,3 +33,85 @@ test("create workspace", async ({ page }) => {
})
await createWorkspace(page, template)
})
+
+test("create workspace with default immutable parameters", async ({ page }) => {
+ const richParameters: RichParameter[] = [
+ secondParameter,
+ fourthParameter,
+ fifthParameter,
+ ]
+ const template = await createTemplate(
+ page,
+ echoResponsesWithParameters(richParameters),
+ )
+ const workspaceName = await createWorkspace(page, template)
+ await verifyParameters(page, workspaceName, richParameters, [
+ { name: secondParameter.name, value: secondParameter.defaultValue },
+ { name: fourthParameter.name, value: fourthParameter.defaultValue },
+ { name: fifthParameter.name, value: fifthParameter.defaultValue },
+ ])
+})
+
+test("create workspace with default mutable parameters", async ({ page }) => {
+ const richParameters: RichParameter[] = [firstParameter, thirdParameter]
+ const template = await createTemplate(
+ page,
+ echoResponsesWithParameters(richParameters),
+ )
+ const workspaceName = await createWorkspace(page, template)
+ await verifyParameters(page, workspaceName, richParameters, [
+ { name: firstParameter.name, value: firstParameter.defaultValue },
+ { name: thirdParameter.name, value: thirdParameter.defaultValue },
+ ])
+})
+
+test("create workspace with default and required parameters", async ({
+ page,
+}) => {
+ const richParameters: RichParameter[] = [
+ secondParameter,
+ fourthParameter,
+ sixthParameter,
+ seventhParameter,
+ ]
+ const buildParameters = [
+ { name: sixthParameter.name, value: "12345" },
+ { name: seventhParameter.name, value: "abcdef" },
+ ]
+ const template = await createTemplate(
+ page,
+ echoResponsesWithParameters(richParameters),
+ )
+ const workspaceName = await createWorkspace(
+ page,
+ template,
+ richParameters,
+ buildParameters,
+ )
+ await verifyParameters(page, workspaceName, richParameters, [
+ // user values:
+ ...buildParameters,
+ // default values:
+ { name: secondParameter.name, value: secondParameter.defaultValue },
+ { name: fourthParameter.name, value: fourthParameter.defaultValue },
+ ])
+})
+
+test("create workspace and overwrite default parameters", async ({ page }) => {
+ const richParameters: RichParameter[] = [secondParameter, fourthParameter]
+ const buildParameters = [
+ { name: secondParameter.name, value: "AAAAA" },
+ { name: fourthParameter.name, value: "false" },
+ ]
+ const template = await createTemplate(
+ page,
+ echoResponsesWithParameters(richParameters),
+ )
+ const workspaceName = await createWorkspace(
+ page,
+ template,
+ richParameters,
+ buildParameters,
+ )
+ await verifyParameters(page, workspaceName, richParameters, buildParameters)
+})
diff --git a/site/e2e/tests/outdatedAgent.spec.ts b/site/e2e/tests/outdatedAgent.spec.ts
index 2b88ea71110df..e10c3f6edb290 100644
--- a/site/e2e/tests/outdatedAgent.spec.ts
+++ b/site/e2e/tests/outdatedAgent.spec.ts
@@ -15,7 +15,7 @@ test("ssh with agent " + agentVersion, async ({ page }) => {
const template = await createTemplate(page, {
apply: [
{
- complete: {
+ apply: {
resources: [
{
agents: [
diff --git a/site/e2e/tests/outdatedCLI.spec.ts b/site/e2e/tests/outdatedCLI.spec.ts
new file mode 100644
index 0000000000000..1b09fccf5e13f
--- /dev/null
+++ b/site/e2e/tests/outdatedCLI.spec.ts
@@ -0,0 +1,52 @@
+import { test } from "@playwright/test"
+import { randomUUID } from "crypto"
+import {
+ createTemplate,
+ createWorkspace,
+ downloadCoderVersion,
+ sshIntoWorkspace,
+ startAgent,
+} from "../helpers"
+
+const clientVersion = "v0.14.0"
+
+test("ssh with client " + clientVersion, async ({ page }) => {
+ const token = randomUUID()
+ const template = await createTemplate(page, {
+ apply: [
+ {
+ apply: {
+ resources: [
+ {
+ agents: [
+ {
+ token,
+ },
+ ],
+ },
+ ],
+ },
+ },
+ ],
+ })
+ const workspace = await createWorkspace(page, template)
+ await startAgent(page, token)
+ const binaryPath = await downloadCoderVersion(clientVersion)
+
+ const client = await sshIntoWorkspace(page, workspace, binaryPath)
+ await new Promise((resolve, reject) => {
+ // We just exec a command to be certain the agent is running!
+ client.exec("exit 0", (err, stream) => {
+ if (err) {
+ return reject(err)
+ }
+ stream.on("exit", (code) => {
+ if (code !== 0) {
+ return reject(new Error(`Command exited with code ${code}`))
+ }
+ client.end()
+ resolve()
+ })
+ })
+ })
+})
diff --git a/site/e2e/tests/restartWorkspace.spec.ts b/site/e2e/tests/restartWorkspace.spec.ts
new file mode 100644
index 0000000000000..eb5d0c99c0217
--- /dev/null
+++ b/site/e2e/tests/restartWorkspace.spec.ts
@@ -0,0 +1,45 @@
+import { test } from "@playwright/test"
+import {
+ buildWorkspaceWithParameters,
+ createTemplate,
+ createWorkspace,
+ echoResponsesWithParameters,
+ verifyParameters,
+} from "../helpers"
+
+import { firstBuildOption, secondBuildOption } from "../parameters"
+import { RichParameter } from "../provisionerGenerated"
+
+test("restart workspace with ephemeral parameters", async ({ page }) => {
+ const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]
+ const template = await createTemplate(
+ page,
+ echoResponsesWithParameters(richParameters),
+ )
+ const workspaceName = await createWorkspace(page, template)
+
+ // Verify that build options are default (not selected).
+ await verifyParameters(page, workspaceName, richParameters, [
+ { name: firstBuildOption.name, value: firstBuildOption.defaultValue },
+ { name: secondBuildOption.name, value: secondBuildOption.defaultValue },
+ ])
+
+ // Now, restart the workspace with ephemeral parameters selected.
+ const buildParameters = [
+ { name: firstBuildOption.name, value: "AAAAA" },
+ { name: secondBuildOption.name, value: "true" },
+ ]
+ await buildWorkspaceWithParameters(
+ page,
+ workspaceName,
+ richParameters,
+ buildParameters,
+ true,
+ )
+
+ // Verify that build options are default (not selected).
+ await verifyParameters(page, workspaceName, richParameters, [
+ { name: firstBuildOption.name, value: firstBuildOption.defaultValue },
+ { name: secondBuildOption.name, value: secondBuildOption.defaultValue },
+ ])
+})
diff --git a/site/e2e/tests/startWorkspace.spec.ts b/site/e2e/tests/startWorkspace.spec.ts
new file mode 100644
index 0000000000000..232ac27299849
--- /dev/null
+++ b/site/e2e/tests/startWorkspace.spec.ts
@@ -0,0 +1,49 @@
+import { test } from "@playwright/test"
+import {
+ buildWorkspaceWithParameters,
+ createTemplate,
+ createWorkspace,
+ echoResponsesWithParameters,
+ stopWorkspace,
+ verifyParameters,
+} from "../helpers"
+
+import { firstBuildOption, secondBuildOption } from "../parameters"
+import { RichParameter } from "../provisionerGenerated"
+
+test("start workspace with ephemeral parameters", async ({ page }) => {
+ const richParameters: RichParameter[] = [firstBuildOption, secondBuildOption]
+ const template = await createTemplate(
+ page,
+ echoResponsesWithParameters(richParameters),
+ )
+ const workspaceName = await createWorkspace(page, template)
+
+ // Verify that build options are default (not selected).
+ await verifyParameters(page, workspaceName, richParameters, [
+ { name: firstBuildOption.name, value: firstBuildOption.defaultValue },
+ { name: secondBuildOption.name, value: secondBuildOption.defaultValue },
+ ])
+
+ // Stop the workspace
+ await stopWorkspace(page, workspaceName)
+
+ // Now, start the workspace with ephemeral parameters selected.
+ const buildParameters = [
+ { name: firstBuildOption.name, value: "AAAAA" },
+ { name: secondBuildOption.name, value: "true" },
+ ]
+
+ await buildWorkspaceWithParameters(
+ page,
+ workspaceName,
+ richParameters,
+ buildParameters,
+ )
+
+ // Verify that build options are default (not selected).
+ await verifyParameters(page, workspaceName, richParameters, [
+ { name: firstBuildOption.name, value: firstBuildOption.defaultValue },
+ { name: secondBuildOption.name, value: secondBuildOption.defaultValue },
+ ])
+})
diff --git a/site/e2e/tests/webTerminal.spec.ts b/site/e2e/tests/webTerminal.spec.ts
index 492634b293a3e..8869d352d3fc8 100644
--- a/site/e2e/tests/webTerminal.spec.ts
+++ b/site/e2e/tests/webTerminal.spec.ts
@@ -7,7 +7,7 @@ test("web terminal", async ({ context, page }) => {
const template = await createTemplate(page, {
apply: [
{
- complete: {
+ apply: {
resources: [
{
agents: [
diff --git a/site/index.html b/site/index.html
index 92801202a5d88..29cafbb453a46 100644
--- a/site/index.html
+++ b/site/index.html
@@ -10,6 +10,7 @@
+ Coder
@@ -35,6 +36,12 @@
href="/favicons/favicon.svg"
data-react-helmet="true"
/>
+
diff --git a/site/package.json b/site/package.json
index 538e158218292..a2230d57694d2 100644
--- a/site/package.json
+++ b/site/package.json
@@ -19,7 +19,7 @@
"lint:types": "tsc --noEmit",
"playwright:install": "playwright install --with-deps chromium",
"playwright:test": "playwright test --config=e2e/playwright.config.ts",
- "gen:provisioner": "protoc --plugin=./node_modules/.bin/protoc-gen-ts-proto --ts_proto_out=./e2e/ --ts_proto_opt=outputJsonMethods=false,outputEncodeMethods=encode-no-creation,outputClientImpl=false,nestJs=false,outputPartialMethods=false,fileSuffix=Generated,suffix=hey -I ../provisionersdk/proto ../provisionersdk/proto/provisioner.proto && prettier --cache --write './e2e/provisionerGenerated.ts'",
+ "gen:provisioner": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./e2e/ --ts_proto_opt=outputJsonMethods=false,outputEncodeMethods=encode-no-creation,outputClientImpl=false,nestJs=false,outputPartialMethods=false,fileSuffix=Generated,suffix=hey -I ../provisionersdk/proto ../provisionersdk/proto/provisioner.proto && pnpm exec prettier --ignore-path '/dev/null' --cache --write './e2e/provisionerGenerated.ts'",
"storybook": "STORYBOOK=true storybook dev -p 6006",
"storybook:build": "storybook build",
"test": "jest --selectProjects test",
@@ -48,6 +48,7 @@
"@types/color-convert": "2.0.0",
"@types/lodash": "4.14.196",
"@types/react-color": "3.0.6",
+ "@types/react-date-range": "1.4.4",
"@types/semver": "7.5.0",
"@vitejs/plugin-react": "4.0.1",
"@xstate/inspect": "0.8.0",
@@ -55,7 +56,7 @@
"ansi-to-html": "0.7.2",
"axios": "1.3.4",
"canvas": "2.11.0",
- "chart.js": "3.9.1",
+ "chart.js": "4.3.3",
"chartjs-adapter-date-fns": "3.0.0",
"chroma-js": "2.4.2",
"color-convert": "2.0.1",
@@ -77,12 +78,13 @@
"react-chartjs-2": "5.2.0",
"react-color": "2.19.3",
"react-confetti": "6.1.0",
+ "react-date-range": "1.4.0",
"react-dom": "18.2.0",
"react-headless-tabs": "6.0.3",
"react-helmet-async": "1.3.0",
"react-i18next": "12.2.2",
"react-markdown": "8.0.3",
- "react-router-dom": "6.14.1",
+ "react-router-dom": "6.15.0",
"react-syntax-highlighter": "15.5.0",
"react-use": "17.4.0",
"react-virtualized-auto-sizer": "1.0.20",
@@ -101,7 +103,9 @@
"xterm": "5.2.1",
"xterm-addon-canvas": "0.4.0",
"xterm-addon-fit": "0.7.0",
+ "xterm-addon-unicode11": "0.5.0",
"xterm-addon-web-links": "0.8.0",
+ "xterm-addon-webgl": "0.15.0",
"yup": "1.2.0"
},
"devDependencies": {
@@ -170,7 +174,8 @@
"ts-node": "10.9.1",
"ts-proto": "1.156.0",
"typescript": "5.1.6",
- "vite-plugin-checker": "0.6.0"
+ "vite-plugin-checker": "0.6.0",
+ "vite-plugin-turbosnap": "1.0.2"
},
"browserslist": [
"chrome 66",
diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml
index abb876c024c61..f137e12289fb1 100644
--- a/site/pnpm-lock.yaml
+++ b/site/pnpm-lock.yaml
@@ -60,6 +60,9 @@ dependencies:
'@types/react-color':
specifier: 3.0.6
version: 3.0.6
+ '@types/react-date-range':
+ specifier: 1.4.4
+ version: 1.4.4
'@types/semver':
specifier: 7.5.0
version: 7.5.0
@@ -82,11 +85,11 @@ dependencies:
specifier: 2.11.0
version: 2.11.0
chart.js:
- specifier: 3.9.1
- version: 3.9.1
+ specifier: 4.3.3
+ version: 4.3.3
chartjs-adapter-date-fns:
specifier: 3.0.0
- version: 3.0.0(chart.js@3.9.1)(date-fns@2.30.0)
+ version: 3.0.0(chart.js@4.3.3)(date-fns@2.30.0)
chroma-js:
specifier: 2.4.2
version: 2.4.2
@@ -140,13 +143,16 @@ dependencies:
version: 18.2.0
react-chartjs-2:
specifier: 5.2.0
- version: 5.2.0(chart.js@3.9.1)(react@18.2.0)
+ version: 5.2.0(chart.js@4.3.3)(react@18.2.0)
react-color:
specifier: 2.19.3
version: 2.19.3(react@18.2.0)
react-confetti:
specifier: 6.1.0
version: 6.1.0(react@18.2.0)
+ react-date-range:
+ specifier: 1.4.0
+ version: 1.4.0(date-fns@2.30.0)(react@18.2.0)
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
@@ -163,8 +169,8 @@ dependencies:
specifier: 8.0.3
version: 8.0.3(@types/react@18.2.6)(react@18.2.0)
react-router-dom:
- specifier: 6.14.1
- version: 6.14.1(react-dom@18.2.0)(react@18.2.0)
+ specifier: 6.15.0
+ version: 6.15.0(react-dom@18.2.0)(react@18.2.0)
react-syntax-highlighter:
specifier: 15.5.0
version: 15.5.0(react@18.2.0)
@@ -219,9 +225,15 @@ dependencies:
xterm-addon-fit:
specifier: 0.7.0
version: 0.7.0(xterm@5.2.1)
+ xterm-addon-unicode11:
+ specifier: 0.5.0
+ version: 0.5.0(xterm@5.2.1)
xterm-addon-web-links:
specifier: 0.8.0
version: 0.8.0(xterm@5.2.1)
+ xterm-addon-webgl:
+ specifier: 0.15.0
+ version: 0.15.0(xterm@5.2.1)
yup:
specifier: 1.2.0
version: 1.2.0
@@ -409,7 +421,7 @@ devDependencies:
version: 7.2.0
storybook-addon-react-router-v6:
specifier: 2.0.0
- version: 2.0.0(@storybook/blocks@7.2.0)(@storybook/channels@7.2.0)(@storybook/components@7.2.0)(@storybook/core-events@7.2.0)(@storybook/manager-api@7.2.0)(@storybook/preview-api@7.2.0)(@storybook/theming@7.2.0)(react-dom@18.2.0)(react-router-dom@6.14.1)(react-router@6.14.2)(react@18.2.0)
+ version: 2.0.0(@storybook/blocks@7.3.1)(@storybook/channels@7.3.1)(@storybook/components@7.3.1)(@storybook/core-events@7.3.1)(@storybook/manager-api@7.3.1)(@storybook/preview-api@7.3.1)(@storybook/theming@7.3.1)(react-dom@18.2.0)(react-router-dom@6.15.0)(react-router@6.15.0)(react@18.2.0)
storybook-react-context:
specifier: 0.6.0
version: 0.6.0(react-dom@18.2.0)
@@ -425,6 +437,9 @@ devDependencies:
vite-plugin-checker:
specifier: 0.6.0
version: 0.6.0(eslint@8.46.0)(typescript@5.1.6)(vite@4.4.2)
+ vite-plugin-turbosnap:
+ specifier: 1.0.2
+ version: 1.0.2
packages:
@@ -1712,6 +1727,13 @@ packages:
resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==}
dev: true
+ /@babel/runtime@7.22.10:
+ resolution: {integrity: sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.14.0
+ dev: true
+
/@babel/runtime@7.22.6:
resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==}
engines: {node: '>=6.9.0'}
@@ -1915,6 +1937,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/android-arm64@0.18.20:
+ resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/android-arm@0.18.17:
resolution: {integrity: sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==}
engines: {node: '>=12'}
@@ -1923,6 +1954,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/android-arm@0.18.20:
+ resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/android-x64@0.18.17:
resolution: {integrity: sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==}
engines: {node: '>=12'}
@@ -1931,6 +1971,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/android-x64@0.18.20:
+ resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/darwin-arm64@0.18.17:
resolution: {integrity: sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==}
engines: {node: '>=12'}
@@ -1939,6 +1988,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/darwin-arm64@0.18.20:
+ resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/darwin-x64@0.18.17:
resolution: {integrity: sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==}
engines: {node: '>=12'}
@@ -1947,6 +2005,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/darwin-x64@0.18.20:
+ resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/freebsd-arm64@0.18.17:
resolution: {integrity: sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==}
engines: {node: '>=12'}
@@ -1955,6 +2022,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/freebsd-arm64@0.18.20:
+ resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/freebsd-x64@0.18.17:
resolution: {integrity: sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==}
engines: {node: '>=12'}
@@ -1963,6 +2039,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/freebsd-x64@0.18.20:
+ resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-arm64@0.18.17:
resolution: {integrity: sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==}
engines: {node: '>=12'}
@@ -1971,6 +2056,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/linux-arm64@0.18.20:
+ resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-arm@0.18.17:
resolution: {integrity: sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==}
engines: {node: '>=12'}
@@ -1979,6 +2073,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/linux-arm@0.18.20:
+ resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-ia32@0.18.17:
resolution: {integrity: sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==}
engines: {node: '>=12'}
@@ -1987,6 +2090,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/linux-ia32@0.18.20:
+ resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-loong64@0.18.17:
resolution: {integrity: sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==}
engines: {node: '>=12'}
@@ -1995,6 +2107,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/linux-loong64@0.18.20:
+ resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-mips64el@0.18.17:
resolution: {integrity: sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==}
engines: {node: '>=12'}
@@ -2003,6 +2124,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/linux-mips64el@0.18.20:
+ resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-ppc64@0.18.17:
resolution: {integrity: sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==}
engines: {node: '>=12'}
@@ -2011,6 +2141,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/linux-ppc64@0.18.20:
+ resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-riscv64@0.18.17:
resolution: {integrity: sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==}
engines: {node: '>=12'}
@@ -2019,6 +2158,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/linux-riscv64@0.18.20:
+ resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-s390x@0.18.17:
resolution: {integrity: sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==}
engines: {node: '>=12'}
@@ -2027,6 +2175,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/linux-s390x@0.18.20:
+ resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/linux-x64@0.18.17:
resolution: {integrity: sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==}
engines: {node: '>=12'}
@@ -2035,6 +2192,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/linux-x64@0.18.20:
+ resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/netbsd-x64@0.18.17:
resolution: {integrity: sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==}
engines: {node: '>=12'}
@@ -2043,6 +2209,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/netbsd-x64@0.18.20:
+ resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/openbsd-x64@0.18.17:
resolution: {integrity: sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==}
engines: {node: '>=12'}
@@ -2051,6 +2226,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/openbsd-x64@0.18.20:
+ resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/sunos-x64@0.18.17:
resolution: {integrity: sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==}
engines: {node: '>=12'}
@@ -2059,6 +2243,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/sunos-x64@0.18.20:
+ resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/win32-arm64@0.18.17:
resolution: {integrity: sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==}
engines: {node: '>=12'}
@@ -2067,6 +2260,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/win32-arm64@0.18.20:
+ resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/win32-ia32@0.18.17:
resolution: {integrity: sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==}
engines: {node: '>=12'}
@@ -2075,6 +2277,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/win32-ia32@0.18.20:
+ resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@esbuild/win32-x64@0.18.17:
resolution: {integrity: sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==}
engines: {node: '>=12'}
@@ -2083,6 +2294,15 @@ packages:
requiresBuild: true
optional: true
+ /@esbuild/win32-x64@0.18.20:
+ resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@eslint-community/eslint-utils@4.4.0(eslint@8.46.0):
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2510,6 +2730,10 @@ packages:
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
dev: true
+ /@kurkle/color@0.3.2:
+ resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
+ dev: false
+
/@mapbox/node-pre-gyp@1.0.11:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true
@@ -3231,6 +3455,35 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: true
+ /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.10
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@radix-ui/react-direction': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@radix-ui/react-id': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@types/react': 18.2.6
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: true
+
/@radix-ui/react-select@1.2.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==}
peerDependencies:
@@ -3272,6 +3525,27 @@ packages:
react-remove-scroll: 2.5.5(@types/react@18.2.6)(react@18.2.0)
dev: true
+ /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.10
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@types/react': 18.2.6
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: true
+
/@radix-ui/react-slot@1.0.2(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
@@ -3287,6 +3561,83 @@ packages:
react: 18.2.0
dev: true
+ /@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.10
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@radix-ui/react-direction': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@types/react': 18.2.6
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: true
+
+ /@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.10
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@types/react': 18.2.6
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: true
+
+ /@radix-ui/react-toolbar@1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.22.10
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@radix-ui/react-direction': 1.0.1(@types/react@18.2.6)(react@18.2.0)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-separator': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@types/react': 18.2.6
+ '@types/react-dom': 18.2.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: true
+
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
@@ -3416,14 +3767,9 @@ packages:
'@babel/runtime': 7.22.6
dev: true
- /@remix-run/router@1.7.1:
- resolution: {integrity: sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ==}
- engines: {node: '>=14'}
-
- /@remix-run/router@1.7.2:
- resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==}
- engines: {node: '>=14'}
- dev: true
+ /@remix-run/router@1.8.0:
+ resolution: {integrity: sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==}
+ engines: {node: '>=14.0.0'}
/@rollup/pluginutils@5.0.2:
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
@@ -3843,6 +4189,44 @@ packages:
- supports-color
dev: true
+ /@storybook/blocks@7.3.1(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-MIMM5+nU/3/RHEmCmSwkHs3Mq6mwJqUpkWUDPx81sQnq9C5r0NHHNmHGTqxF/SPyptPxmWGI88ETpiidVZK6RQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ '@storybook/channels': 7.3.1
+ '@storybook/client-logger': 7.3.1
+ '@storybook/components': 7.3.1(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/core-events': 7.3.1
+ '@storybook/csf': 0.1.1
+ '@storybook/docs-tools': 7.3.1
+ '@storybook/global': 5.0.0
+ '@storybook/manager-api': 7.3.1(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/preview-api': 7.3.1
+ '@storybook/theming': 7.3.1(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/types': 7.3.1
+ '@types/lodash': 4.14.196
+ color-convert: 2.0.1
+ dequal: 2.0.3
+ lodash: 4.17.21
+ markdown-to-jsx: 7.3.2(react@18.2.0)
+ memoizerific: 1.11.3
+ polished: 4.2.2
+ react: 18.2.0
+ react-colorful: 5.6.1(react-dom@18.2.0)(react@18.2.0)
+ react-dom: 18.2.0(react@18.2.0)
+ telejson: 7.1.0
+ tocbot: 4.21.1
+ ts-dedent: 2.2.0
+ util-deprecate: 1.0.2
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+ - encoding
+ - supports-color
+ dev: true
+
/@storybook/builder-manager@7.2.0:
resolution: {integrity: sha512-WGenq08db5mmlMTQ3dFsZD1tNYx43vjgbDJOeABUJ8pyTDZ0WPT6lfRWn9D2qzG1Sie4bkv2FyJdlc/AfM7SIQ==}
dependencies:
@@ -3927,6 +4311,17 @@ packages:
tiny-invariant: 1.3.1
dev: true
+ /@storybook/channels@7.3.1:
+ resolution: {integrity: sha512-DHdUdwfnMOSmtYv55Ixysklo/ZeD3TiTEQvyBaxhnMR3G0j7nb+TxqyfAn4fb7bntOPRNVB1Vz3nZXkkjrPNgw==}
+ dependencies:
+ '@storybook/client-logger': 7.3.1
+ '@storybook/core-events': 7.3.1
+ '@storybook/global': 5.0.0
+ qs: 6.11.2
+ telejson: 7.1.0
+ tiny-invariant: 1.3.1
+ dev: true
+
/@storybook/cli@7.2.0:
resolution: {integrity: sha512-0RxleuwhSbREr5FxNu/N+TIK4CZJDVDDXCGTpXnRZrA4phzUIhrkG/9wDfW/jo3GHfyKa8PE1mYkqtvG3J3rVQ==}
hasBin: true
@@ -3991,6 +4386,12 @@ packages:
'@storybook/global': 5.0.0
dev: true
+ /@storybook/client-logger@7.3.1:
+ resolution: {integrity: sha512-VfKi8C5Z1hquaP6xtVn9ngKcnXZjHNV6+RAqLbUJyAoGeO8fFaMblYgbY+tF7Xyf3bZKMLBo4QqtegTh2QjdAA==}
+ dependencies:
+ '@storybook/global': 5.0.0
+ dev: true
+
/@storybook/codemod@7.2.0:
resolution: {integrity: sha512-CxsGogfqTZzEa4QLRnywbH1fSa9MV/KKLnyDOlUnUv6GLHC9IRQAxeXrum9RJOkAhVMl1hBvBBgsPcBu7AnHUQ==}
dependencies:
@@ -4034,6 +4435,30 @@ packages:
- '@types/react-dom'
dev: true
+ /@storybook/components@7.3.1(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-8dk3WutobHvjxweVzA9Vqrp564vWOTQaV38JSi84ME8wzOdl20Xne9LoeMnqPHXFhnVZdm/Gkosfv4tqkDy4aw==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ '@radix-ui/react-select': 1.2.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-toolbar': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/client-logger': 7.3.1
+ '@storybook/csf': 0.1.1
+ '@storybook/global': 5.0.0
+ '@storybook/icons': 1.1.6(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/theming': 7.3.1(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/types': 7.3.1
+ memoizerific: 1.11.3
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ use-resize-observer: 9.1.0(react-dom@18.2.0)(react@18.2.0)
+ util-deprecate: 1.0.2
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+ dev: true
+
/@storybook/core-client@7.2.0:
resolution: {integrity: sha512-U/5BAGGI9HIO1RHetQR0V4a9ISWDRlcik8mQhOVVcvd6eMkyS9O8r3unVaXTjjAUQvDsP2il89fV6bkouJBfKA==}
dependencies:
@@ -4071,6 +4496,36 @@ packages:
- supports-color
dev: true
+ /@storybook/core-common@7.3.1:
+ resolution: {integrity: sha512-jALwn9T6xjVQ/GBD2UVMi0XAhJDIsSNf3ghxatRQpa5dphG4nZccF6xwnUdsQqDGr8E4lHgDDzIKP/wqQ3fi1Q==}
+ dependencies:
+ '@storybook/node-logger': 7.3.1
+ '@storybook/types': 7.3.1
+ '@types/find-cache-dir': 3.2.1
+ '@types/node': 16.18.41
+ '@types/node-fetch': 2.6.4
+ '@types/pretty-hrtime': 1.0.1
+ chalk: 4.1.2
+ esbuild: 0.18.20
+ esbuild-register: 3.4.2(esbuild@0.18.20)
+ file-system-cache: 2.3.0
+ find-cache-dir: 3.3.2
+ find-up: 5.0.0
+ fs-extra: 11.1.1
+ glob: 10.3.3
+ handlebars: 4.7.8
+ lazy-universal-dotenv: 4.0.0
+ node-fetch: 2.6.12
+ picomatch: 2.3.1
+ pkg-dir: 5.0.0
+ pretty-hrtime: 1.0.3
+ resolve-from: 5.0.0
+ ts-dedent: 2.2.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+ dev: true
+
/@storybook/core-events@6.5.16:
resolution: {integrity: sha512-qMZQwmvzpH5F2uwNUllTPg6eZXr2OaYZQRRN8VZJiuorZzDNdAFmiVWMWdkThwmyLEJuQKXxqCL8lMj/7PPM+g==}
dependencies:
@@ -4081,6 +4536,10 @@ packages:
resolution: {integrity: sha512-Y1o8vGBnbZ/bYsukPiK33CHURSob3tywg8WRtAuwWnDaZiM9IXgkEHbOK1zfkPTnz2gSXEX19KlpTmMxm0W//w==}
dev: true
+ /@storybook/core-events@7.3.1:
+ resolution: {integrity: sha512-7Pkgwmj/9B7Z3NNSn2swnviBrg9L1VeYSFw6JJKxtQskt8QoY8LxAsPzVMlHjqRmO6sO7lHo9FgpzIFxdmFaAA==}
+ dev: true
+
/@storybook/core-server@7.2.0:
resolution: {integrity: sha512-sVdx8lLVJ99dok1SX4Tl6SHMI4UroKxNoJuJ/Ie29YksYHJuzDo9pP1SpkdWtqIeS4AngqeB1iLi+wB6nZneJQ==}
dependencies:
@@ -4195,10 +4654,35 @@ packages:
- supports-color
dev: true
+ /@storybook/docs-tools@7.3.1:
+ resolution: {integrity: sha512-9N8CRarcejQoYhIKxbSrS9WJwdbrnj2I8tRWS91cgC2o4pDykqoXD7hXabVixQREzHOZEwakKAg8LsDLfCZCkw==}
+ dependencies:
+ '@storybook/core-common': 7.3.1
+ '@storybook/preview-api': 7.3.1
+ '@storybook/types': 7.3.1
+ '@types/doctrine': 0.0.3
+ doctrine: 3.0.0
+ lodash: 4.17.21
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+ dev: true
+
/@storybook/global@5.0.0:
resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
dev: true
+ /@storybook/icons@1.1.6(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-co5gDCYPojRAc5lRMnWxbjrR1V37/rTmAo9Vok4a1hDpHZIwkGTWesdzvYivSQXYFxZTpxdM1b5K3W87brnahw==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: true
+
/@storybook/manager-api@7.2.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-sKaG+VBS8wXGaT+vEihK/2VXJwShhFVOsvOd81vfaM97btik0IhCEHtV7VCNW2lDidIGw7u2DX7QO0tav/Qf1w==}
peerDependencies:
@@ -4224,6 +4708,31 @@ packages:
ts-dedent: 2.2.0
dev: true
+ /@storybook/manager-api@7.3.1(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-jFH0EfWasdwHW8X5DUzTbH5mpdCZBHU7lIEUj6lVMBcBxbTniqBiG7mkwbW9VLocqEbBZimLCb/2RtTpK1Ue3Q==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ '@storybook/channels': 7.3.1
+ '@storybook/client-logger': 7.3.1
+ '@storybook/core-events': 7.3.1
+ '@storybook/csf': 0.1.1
+ '@storybook/global': 5.0.0
+ '@storybook/router': 7.3.1(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/theming': 7.3.1(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/types': 7.3.1
+ dequal: 2.0.3
+ lodash: 4.17.21
+ memoizerific: 1.11.3
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ semver: 7.5.3
+ store2: 2.14.2
+ telejson: 7.1.0
+ ts-dedent: 2.2.0
+ dev: true
+
/@storybook/manager@7.2.0:
resolution: {integrity: sha512-XwKjEA0p8f8rsv5XBXcmGrE4MNMlq/+wazQLyxWUyW3iMiYI0px0QjrQPnEGjOUasyLA+sRGrhy0gJ2Z9/XowQ==}
dev: true
@@ -4236,6 +4745,10 @@ packages:
resolution: {integrity: sha512-rQTmw3oSaeenUCOxOa/8+ZtxDxNPhHIURv2Qpr/q5JkcDf13I6HimqVRxeccU+g3Bq/ueceOXMcAuoH4oewtUw==}
dev: true
+ /@storybook/node-logger@7.3.1:
+ resolution: {integrity: sha512-UVjXJ3nRsGI+yyVFCDKFCjkzrQsUSAMORSlo5vOqypO3PjSahGQBgKjlKnZGXwvdGKB2FW56PbKnb/sPBI/kPg==}
+ dev: true
+
/@storybook/postinstall@7.2.0:
resolution: {integrity: sha512-E/hhZmbo0G7sv/Wq4dW9b27+n9883DY8Md3ju8AVB3Q1DPvKClmgpA6MRbEJtcj0Qh8LgPOnrbxfLsVbJwHpTg==}
dev: true
@@ -4259,6 +4772,25 @@ packages:
util-deprecate: 1.0.2
dev: true
+ /@storybook/preview-api@7.3.1:
+ resolution: {integrity: sha512-otFvUJBFxhg11O5XLiyqddTS1ge/tjIs4gA4Uli6M+a6PV+SdNuTE8OjpvvgjsFTFdhyciHKTimKSLAqvopcuw==}
+ dependencies:
+ '@storybook/channels': 7.3.1
+ '@storybook/client-logger': 7.3.1
+ '@storybook/core-events': 7.3.1
+ '@storybook/csf': 0.1.1
+ '@storybook/global': 5.0.0
+ '@storybook/types': 7.3.1
+ '@types/qs': 6.9.7
+ dequal: 2.0.3
+ lodash: 4.17.21
+ memoizerific: 1.11.3
+ qs: 6.11.2
+ synchronous-promise: 2.0.17
+ ts-dedent: 2.2.0
+ util-deprecate: 1.0.2
+ dev: true
+
/@storybook/preview@7.2.0:
resolution: {integrity: sha512-x3pOQFvVqJgfjC2Wt5AKyyym1031m6crl+lmxsDUtcenEhFazJ5iVLzlB5x4p+98QMkblHPqsx8JUMbAALV8Xw==}
dev: true
@@ -4369,6 +4901,19 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: true
+ /@storybook/router@7.3.1(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-KY+Mo0oF2xcRUDCXPJjAB5xy7d8Hi2dh8VqLahGa14ZHwhsZ/RxqE2bypwLXXkRpEiyOpfMbSsG73+1ml3fIUg==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ '@storybook/client-logger': 7.3.1
+ memoizerific: 1.11.3
+ qs: 6.11.2
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: true
+
/@storybook/semver@7.3.2:
resolution: {integrity: sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==}
engines: {node: '>=10'}
@@ -4422,6 +4967,20 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: true
+ /@storybook/theming@7.3.1(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-1CF6bT8o8pZcd/ptl1q4CiTGY4oLV19tE8Wnhd/TO934fdMp4fUx1FF4pFL6an98lxVeZT0JQ4uvkuaTvHJFRQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
+ '@storybook/client-logger': 7.3.1
+ '@storybook/global': 5.0.0
+ memoizerific: 1.11.3
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: true
+
/@storybook/types@7.2.0:
resolution: {integrity: sha512-jwoA/TIp+U8Vz868aQT+XfoAw6qFrtn2HbZlTfwNWZsUhPFlMsGrwIVEpWqBWIoe6WITU/lNw3BuRmxul+wvAQ==}
dependencies:
@@ -4431,6 +4990,15 @@ packages:
file-system-cache: 2.3.0
dev: true
+ /@storybook/types@7.3.1:
+ resolution: {integrity: sha512-QR714i/Stus/RYqJ8chTCfWNt3RY6/64xRXxaMLqkx75OIq5+rtsmes9I5iUqM4FuupvE7YdlZ5xKvxLYLYgJQ==}
+ dependencies:
+ '@storybook/channels': 7.3.1
+ '@types/babel__core': 7.20.1
+ '@types/express': 4.17.17
+ file-system-cache: 2.3.0
+ dev: true
+
/@swc/core-darwin-arm64@1.3.38:
resolution: {integrity: sha512-4ZTJJ/cR0EsXW5UxFCifZoGfzQ07a8s4ayt1nLvLQ5QoB1GTAf9zsACpvWG8e7cmCR0L76R5xt8uJuyr+noIXA==}
engines: {node: '>=10'}
@@ -4913,6 +5481,10 @@ packages:
resolution: {integrity: sha512-8q9ZexmdYYyc5/cfujaXb4YOucpQxAV4RMG0himLyDUOEr8Mr79VrqsFI+cQ2M2h89YIuy95lbxuYjxT4Hk4kQ==}
dev: true
+ /@types/node@16.18.41:
+ resolution: {integrity: sha512-YZJjn+Aaw0xihnpdImxI22jqGbp0DCgTFKRycygjGx/Y27NnWFJa5FJ7P+MRT3u07dogEeMVh70pWpbIQollTA==}
+ dev: true
+
/@types/node@18.17.0:
resolution: {integrity: sha512-GXZxEtOxYGFchyUzxvKI14iff9KZ2DI+A6a37o6EQevtg6uO9t+aUZKcaC1Te5Ng1OnLM7K9NVVj+FbecD9cJg==}
@@ -4952,6 +5524,13 @@ packages:
'@types/reactcss': 1.2.6
dev: false
+ /@types/react-date-range@1.4.4:
+ resolution: {integrity: sha512-9Y9NyNgaCsEVN/+O4HKuxzPbVjRVBGdOKRxMDcsTRWVG62lpYgnxefNckTXDWup8FvczoqPW0+ESZR6R1yymDg==}
+ dependencies:
+ '@types/react': 18.2.6
+ date-fns: 2.30.0
+ dev: false
+
/@types/react-dom@18.2.4:
resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==}
dependencies:
@@ -6172,17 +6751,20 @@ packages:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true
- /chart.js@3.9.1:
- resolution: {integrity: sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==}
+ /chart.js@4.3.3:
+ resolution: {integrity: sha512-aTk7pBw+x6sQYhon/NR3ikfUJuym/LdgpTlgZRe2PaEhjUMKBKyNaFCMVRAyTEWYFNO7qRu7iQVqOw/OqzxZxQ==}
+ engines: {pnpm: '>=7'}
+ dependencies:
+ '@kurkle/color': 0.3.2
dev: false
- /chartjs-adapter-date-fns@3.0.0(chart.js@3.9.1)(date-fns@2.30.0):
+ /chartjs-adapter-date-fns@3.0.0(chart.js@4.3.3)(date-fns@2.30.0):
resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==}
peerDependencies:
chart.js: '>=2.8.0'
date-fns: '>=2.0.0'
dependencies:
- chart.js: 3.9.1
+ chart.js: 4.3.3
date-fns: 2.30.0
dev: false
@@ -6226,6 +6808,10 @@ packages:
resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==}
dev: true
+ /classnames@2.3.2:
+ resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
+ dev: false
+
/clean-regexp@1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
@@ -6472,8 +7058,8 @@ packages:
path-type: 4.0.0
yaml: 1.10.2
- /cpu-features@0.0.8:
- resolution: {integrity: sha512-BbHBvtYhUhksqTjr6bhNOjGgMnhwhGTQmOoZGD+K7BCaQDCuZl/Ve1ZxUSMRwVC4D/rkCPQ2MAIeYzrWyK7eEg==}
+ /cpu-features@0.0.9:
+ resolution: {integrity: sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==}
engines: {node: '>=10.0.0'}
requiresBuild: true
dependencies:
@@ -7107,6 +7693,17 @@ packages:
- supports-color
dev: true
+ /esbuild-register@3.4.2(esbuild@0.18.20):
+ resolution: {integrity: sha512-kG/XyTDyz6+YDuyfB9ZoSIOOmgyFCH+xPRtsCa8W85HLRV5Csp+o3jWVbOSHgSLfyLc5DmP+KFDNwty4mEjC+Q==}
+ peerDependencies:
+ esbuild: '>=0.12 <1'
+ dependencies:
+ debug: 4.3.4
+ esbuild: 0.18.20
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/esbuild@0.18.17:
resolution: {integrity: sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==}
engines: {node: '>=12'}
@@ -7136,6 +7733,36 @@ packages:
'@esbuild/win32-ia32': 0.18.17
'@esbuild/win32-x64': 0.18.17
+ /esbuild@0.18.20:
+ resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ '@esbuild/android-arm': 0.18.20
+ '@esbuild/android-arm64': 0.18.20
+ '@esbuild/android-x64': 0.18.20
+ '@esbuild/darwin-arm64': 0.18.20
+ '@esbuild/darwin-x64': 0.18.20
+ '@esbuild/freebsd-arm64': 0.18.20
+ '@esbuild/freebsd-x64': 0.18.20
+ '@esbuild/linux-arm': 0.18.20
+ '@esbuild/linux-arm64': 0.18.20
+ '@esbuild/linux-ia32': 0.18.20
+ '@esbuild/linux-loong64': 0.18.20
+ '@esbuild/linux-mips64el': 0.18.20
+ '@esbuild/linux-ppc64': 0.18.20
+ '@esbuild/linux-riscv64': 0.18.20
+ '@esbuild/linux-s390x': 0.18.20
+ '@esbuild/linux-x64': 0.18.20
+ '@esbuild/netbsd-x64': 0.18.20
+ '@esbuild/openbsd-x64': 0.18.20
+ '@esbuild/sunos-x64': 0.18.20
+ '@esbuild/win32-arm64': 0.18.20
+ '@esbuild/win32-ia32': 0.18.20
+ '@esbuild/win32-x64': 0.18.20
+ dev: true
+
/escalade@3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'}
@@ -8206,6 +8833,19 @@ packages:
uglify-js: 3.17.4
dev: true
+ /handlebars@4.7.8:
+ resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
+ engines: {node: '>=0.4.7'}
+ hasBin: true
+ dependencies:
+ minimist: 1.2.8
+ neo-async: 2.6.2
+ source-map: 0.6.1
+ wordwrap: 1.0.0
+ optionalDependencies:
+ uglify-js: 3.17.4
+ dev: true
+
/has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
dev: true
@@ -9828,6 +10468,15 @@ packages:
react: 18.2.0
dev: true
+ /markdown-to-jsx@7.3.2(react@18.2.0):
+ resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==}
+ engines: {node: '>= 10'}
+ peerDependencies:
+ react: '>= 0.14.0'
+ dependencies:
+ react: 18.2.0
+ dev: true
+
/material-colors@1.2.6:
resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==}
dev: false
@@ -11134,13 +11783,13 @@ packages:
unpipe: 1.0.0
dev: true
- /react-chartjs-2@5.2.0(chart.js@3.9.1)(react@18.2.0):
+ /react-chartjs-2@5.2.0(chart.js@4.3.3)(react@18.2.0):
resolution: {integrity: sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==}
peerDependencies:
chart.js: ^4.1.1
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
- chart.js: 3.9.1
+ chart.js: 4.3.3
react: 18.2.0
dev: false
@@ -11179,6 +11828,20 @@ packages:
tween-functions: 1.2.0
dev: false
+ /react-date-range@1.4.0(date-fns@2.30.0)(react@18.2.0):
+ resolution: {integrity: sha512-+9t0HyClbCqw1IhYbpWecjsiaftCeRN5cdhsi9v06YdimwyMR2yYHWcgVn3URwtN/txhqKpEZB6UX1fHpvK76w==}
+ peerDependencies:
+ date-fns: 2.0.0-alpha.7 || >=2.0.0
+ react: ^0.14 || ^15.0.0-rc || >=15.0
+ dependencies:
+ classnames: 2.3.2
+ date-fns: 2.30.0
+ prop-types: 15.8.1
+ react: 18.2.0
+ react-list: 0.8.17(react@18.2.0)
+ shallow-equal: 1.2.1
+ dev: false
+
/react-docgen-typescript@2.2.2(typescript@5.1.6):
resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==}
peerDependencies:
@@ -11313,6 +11976,15 @@ packages:
/react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
+ /react-list@0.8.17(react@18.2.0):
+ resolution: {integrity: sha512-pgmzGi0G5uGrdHzMhgO7KR1wx5ZXVvI3SsJUmkblSAKtewIhMwbQiMuQiTE83ozo04BQJbe0r3WIWzSO0dR1xg==}
+ peerDependencies:
+ react: 0.14 || 15 - 18
+ dependencies:
+ prop-types: 15.8.1
+ react: 18.2.0
+ dev: false
+
/react-markdown@8.0.3(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-We36SfqaKoVNpN1QqsZwWSv/OZt5J15LNgTLWynwAN5b265hrQrsjMtlRNwUvS+YyR3yDM8HpTNc4pK9H/Gc0A==}
peerDependencies:
@@ -11379,36 +12051,26 @@ packages:
use-sidecar: 1.1.2(@types/react@18.2.6)(react@18.2.0)
dev: true
- /react-router-dom@6.14.1(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw==}
- engines: {node: '>=14'}
+ /react-router-dom@6.15.0(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==}
+ engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
dependencies:
- '@remix-run/router': 1.7.1
+ '@remix-run/router': 1.8.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
- react-router: 6.14.1(react@18.2.0)
-
- /react-router@6.14.1(react@18.2.0):
- resolution: {integrity: sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g==}
- engines: {node: '>=14'}
- peerDependencies:
- react: '>=16.8'
- dependencies:
- '@remix-run/router': 1.7.1
- react: 18.2.0
+ react-router: 6.15.0(react@18.2.0)
- /react-router@6.14.2(react@18.2.0):
- resolution: {integrity: sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==}
- engines: {node: '>=14'}
+ /react-router@6.15.0(react@18.2.0):
+ resolution: {integrity: sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==}
+ engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
dependencies:
- '@remix-run/router': 1.7.2
+ '@remix-run/router': 1.8.0
react: 18.2.0
- dev: true
/react-style-singleton@2.2.1(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
@@ -11631,6 +12293,10 @@ packages:
/regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+ /regenerator-runtime@0.14.0:
+ resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
+ dev: true
+
/regenerator-transform@0.15.1:
resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==}
dependencies:
@@ -12001,6 +12667,10 @@ packages:
kind-of: 6.0.3
dev: true
+ /shallow-equal@1.2.1:
+ resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
+ dev: false
+
/shallowequal@1.1.0:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
dev: false
@@ -12151,7 +12821,7 @@ packages:
asn1: 0.2.6
bcrypt-pbkdf: 1.0.2
optionalDependencies:
- cpu-features: 0.0.8
+ cpu-features: 0.0.9
nan: 2.17.0
dev: true
@@ -12206,7 +12876,7 @@ packages:
resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==}
dev: true
- /storybook-addon-react-router-v6@2.0.0(@storybook/blocks@7.2.0)(@storybook/channels@7.2.0)(@storybook/components@7.2.0)(@storybook/core-events@7.2.0)(@storybook/manager-api@7.2.0)(@storybook/preview-api@7.2.0)(@storybook/theming@7.2.0)(react-dom@18.2.0)(react-router-dom@6.14.1)(react-router@6.14.2)(react@18.2.0):
+ /storybook-addon-react-router-v6@2.0.0(@storybook/blocks@7.3.1)(@storybook/channels@7.3.1)(@storybook/components@7.3.1)(@storybook/core-events@7.3.1)(@storybook/manager-api@7.3.1)(@storybook/preview-api@7.3.1)(@storybook/theming@7.3.1)(react-dom@18.2.0)(react-router-dom@6.15.0)(react-router@6.15.0)(react@18.2.0):
resolution: {integrity: sha512-M+PR7rdacFDwUCQZRBJVnzyEOqHrDVrTqN8ufqo+TuXxk33QZvb3QeZuo0d2UTYctgA1GY74EX9RJCEXZpv6VQ==}
peerDependencies:
'@storybook/blocks': ^7.0.0
@@ -12226,18 +12896,18 @@ packages:
react-dom:
optional: true
dependencies:
- '@storybook/blocks': 7.2.0(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
- '@storybook/channels': 7.2.0
- '@storybook/components': 7.2.0(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
- '@storybook/core-events': 7.2.0
- '@storybook/manager-api': 7.2.0(react-dom@18.2.0)(react@18.2.0)
- '@storybook/preview-api': 7.2.0
- '@storybook/theming': 7.2.0(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/blocks': 7.3.1(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/channels': 7.3.1
+ '@storybook/components': 7.3.1(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/core-events': 7.3.1
+ '@storybook/manager-api': 7.3.1(react-dom@18.2.0)(react@18.2.0)
+ '@storybook/preview-api': 7.3.1
+ '@storybook/theming': 7.3.1(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-inspector: 6.0.2(react@18.2.0)
- react-router: 6.14.2(react@18.2.0)
- react-router-dom: 6.14.1(react-dom@18.2.0)(react@18.2.0)
+ react-router: 6.15.0(react@18.2.0)
+ react-router-dom: 6.15.0(react-dom@18.2.0)(react@18.2.0)
dev: true
/storybook-react-context@0.6.0(react-dom@18.2.0):
@@ -12603,6 +13273,10 @@ packages:
resolution: {integrity: sha512-vXk8htr8mIl3hc2s2mDkaPTBfqmqZA2o0x7eXbxUibdrpEIPdpM0L9hH/RvEvlgSM+ZTgS34sGipk5+VrLJCLA==}
dev: true
+ /tocbot@4.21.1:
+ resolution: {integrity: sha512-IfajhBTeg0HlMXu1f+VMbPef05QpDTsZ9X2Yn1+8npdaXsXg/+wrm9Ze1WG5OS1UDC3qJ5EQN/XOZ3gfXjPFCw==}
+ dev: true
+
/toggle-selection@1.0.6:
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
dev: false
@@ -13226,6 +13900,10 @@ packages:
vscode-uri: 3.0.7
dev: true
+ /vite-plugin-turbosnap@1.0.2:
+ resolution: {integrity: sha512-irjKcKXRn7v5bPAg4mAbsS6DgibpP1VUFL9tlgxU6lloK6V9yw9qCZkS+s2PtbkZpWNzr3TN3zVJAc6J7gJZmA==}
+ dev: true
+
/vite@4.4.2(@types/node@18.17.0):
resolution: {integrity: sha512-zUcsJN+UvdSyHhYa277UHhiJ3iq4hUBwHavOpsNUGsTgjBeoBlK8eDt+iT09pBq0h9/knhG/SPrZiM7cGmg7NA==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -13530,6 +14208,14 @@ packages:
xterm: 5.2.1
dev: false
+ /xterm-addon-unicode11@0.5.0(xterm@5.2.1):
+ resolution: {integrity: sha512-Jm4/g4QiTxiKiTbYICQgC791ubhIZyoIwxAIgOW8z8HWFNY+lwk+dwaKEaEeGBfM48Vk8fklsUW9u/PlenYEBg==}
+ peerDependencies:
+ xterm: ^5.0.0
+ dependencies:
+ xterm: 5.2.1
+ dev: false
+
/xterm-addon-web-links@0.8.0(xterm@5.2.1):
resolution: {integrity: sha512-J4tKngmIu20ytX9SEJjAP3UGksah7iALqBtfTwT9ZnmFHVplCumYQsUJfKuS+JwMhjsjH61YXfndenLNvjRrEw==}
peerDependencies:
@@ -13538,6 +14224,14 @@ packages:
xterm: 5.2.1
dev: false
+ /xterm-addon-webgl@0.15.0(xterm@5.2.1):
+ resolution: {integrity: sha512-ZLcqogMFHr4g/YRhcCh3xE8tTklnyut/M+O/XhVsFBRB/YCvYhPdLQ5/AQk54V0wjWAQpa8CF3W8DVR9OqyMCg==}
+ peerDependencies:
+ xterm: ^5.0.0
+ dependencies:
+ xterm: 5.2.1
+ dev: false
+
/xterm@5.2.1:
resolution: {integrity: sha512-cs5Y1fFevgcdoh2hJROMVIWwoBHD80P1fIP79gopLHJIE4kTzzblanoivxTiQ4+92YM9IxS36H1q0MxIJXQBcA==}
dev: false
diff --git a/site/site.go b/site/site.go
index 6b63bd963c4d1..4619bd5b047e2 100644
--- a/site/site.go
+++ b/site/site.go
@@ -34,13 +34,13 @@ import (
"golang.org/x/sync/singleflight"
"golang.org/x/xerrors"
- "github.com/coder/coder/buildinfo"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/database/dbauthz"
- "github.com/coder/coder/coderd/httpapi"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
+ "github.com/coder/coder/v2/buildinfo"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
)
// We always embed the error page HTML because it it doesn't need to be built,
diff --git a/site/site_test.go b/site/site_test.go
index de4e150618b8b..1785898fcaf90 100644
--- a/site/site_test.go
+++ b/site/site_test.go
@@ -22,14 +22,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/database/db2sdk"
- "github.com/coder/coder/coderd/database/dbfake"
- "github.com/coder/coder/coderd/database/dbgen"
- "github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/codersdk"
- "github.com/coder/coder/site"
- "github.com/coder/coder/testutil"
+ "github.com/coder/coder/v2/coderd/database"
+ "github.com/coder/coder/v2/coderd/database/db2sdk"
+ "github.com/coder/coder/v2/coderd/database/dbfake"
+ "github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/httpmw"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/site"
+ "github.com/coder/coder/v2/testutil"
)
func TestInjection(t *testing.T) {
diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx
index 449d4925febb3..785048fe7298d 100644
--- a/site/src/AppRouter.tsx
+++ b/site/src/AppRouter.tsx
@@ -272,10 +272,7 @@ export const AppRouter: FC = () => {
} />
- }
- >
+ }>
} />
} />
} />
@@ -325,7 +322,7 @@ export const AppRouter: FC = () => {
{/* Terminal and CLI auth pages don't have the dashboard layout */}
}
+ element={ }
/>
} />
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index bd2f36967f74c..3567e4f977332 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -488,7 +488,7 @@ export function waitForBuild(build: TypesGen.WorkspaceBuild) {
const { job } = await getWorkspaceBuildByNumber(
build.workspace_owner_name,
build.workspace_name,
- String(build.build_number),
+ build.build_number,
)
latestJobInfo = job
@@ -554,6 +554,21 @@ export const cancelWorkspaceBuild = async (
return response.data
}
+export const updateWorkspaceDormancy = async (
+ workspaceId: string,
+ dormant: boolean,
+): Promise => {
+ const data: TypesGen.UpdateWorkspaceDormancy = {
+ dormant: dormant,
+ }
+
+ const response = await axios.put(
+ `/api/v2/workspaces/${workspaceId}/dormant`,
+ data,
+ )
+ return response.data
+}
+
export const restartWorkspace = async ({
workspace,
buildParameters,
@@ -757,7 +772,7 @@ export const getWorkspaceBuilds = async (
export const getWorkspaceBuildByNumber = async (
username = "me",
workspaceName: string,
- buildNumber: string,
+ buildNumber: number,
): Promise => {
const response = await axios.get(
`/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`,
@@ -793,6 +808,10 @@ export const putWorkspaceExtension = async (
})
}
+export const refreshEntitlements = async (): Promise => {
+ await axios.post("/api/v2/licenses/refresh-entitlements")
+}
+
export const getEntitlements = async (): Promise => {
try {
const response = await axios.get("/api/v2/entitlements")
@@ -806,6 +825,7 @@ export const getEntitlements = async (): Promise => {
require_telemetry: false,
trial: false,
warnings: [],
+ refreshed_at: "",
}
}
throw ex
@@ -1205,7 +1225,6 @@ const getMissingParameters = (
if (isMutableAndRequired || isImmutable) {
requiredParameters.push(p)
- return
}
})
@@ -1228,6 +1247,35 @@ const getMissingParameters = (
missingParameters.push(parameter)
}
+ // Check if parameter "options" changed and we can't use old build parameters.
+ templateParameters.forEach((templateParameter) => {
+ if (templateParameter.options.length === 0) {
+ return
+ }
+
+ // Check if there is a new value
+ let buildParameter = newBuildParameters.find(
+ (p) => p.name === templateParameter.name,
+ )
+
+ // If not, get the old one
+ if (!buildParameter) {
+ buildParameter = oldBuildParameters.find(
+ (p) => p.name === templateParameter.name,
+ )
+ }
+
+ if (!buildParameter) {
+ return
+ }
+
+ const matchingOption = templateParameter.options.find(
+ (option) => option.value === buildParameter?.value,
+ )
+ if (!matchingOption) {
+ missingParameters.push(templateParameter)
+ }
+ })
return missingParameters
}
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index fdfa3542dbf6a..2cd98259380b0 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -200,8 +200,8 @@ export interface CreateTemplateRequest {
readonly allow_user_autostart?: boolean
readonly allow_user_autostop?: boolean
readonly failure_ttl_ms?: number
- readonly inactivity_ttl_ms?: number
- readonly locked_ttl_ms?: number
+ readonly dormant_ttl_ms?: number
+ readonly delete_ttl_ms?: number
readonly disable_everyone_group_access: boolean
}
@@ -247,6 +247,7 @@ export interface CreateUserRequest {
readonly email: string
readonly username: string
readonly password: string
+ readonly login_type: LoginType
readonly disable_login: boolean
readonly organization_id: string
}
@@ -304,6 +305,7 @@ export interface DERP {
// From codersdk/deployment.go
export interface DERPConfig {
readonly block_direct: boolean
+ readonly force_websockets: boolean
readonly url: string
readonly path: string
}
@@ -320,7 +322,7 @@ export interface DERPServerConfig {
readonly region_id: number
readonly region_code: string
readonly region_name: string
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly stun_addresses: string[]
readonly relay_url: string
}
@@ -354,9 +356,9 @@ export interface DeploymentValues {
readonly derp?: DERP
readonly prometheus?: PrometheusConfig
readonly pprof?: PprofConfig
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly proxy_trusted_headers?: string[]
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly proxy_trusted_origins?: string[]
readonly cache_directory?: string
readonly in_memory_database?: boolean
@@ -368,7 +370,7 @@ export interface DeploymentValues {
readonly trace?: TraceConfig
readonly secure_auth_cookie?: boolean
readonly strict_transport_security?: number
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly strict_transport_security_options?: string[]
readonly ssh_keygen_algorithm?: string
readonly metrics_cache_refresh_interval?: number
@@ -378,7 +380,7 @@ export interface DeploymentValues {
readonly scim_api_key?: string
readonly provisioner?: ProvisionerConfig
readonly rate_limit?: RateLimitConfig
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly experiments?: string[]
readonly update_check?: boolean
readonly max_token_lifetime?: number
@@ -390,7 +392,7 @@ export interface DeploymentValues {
readonly disable_session_expiry_refresh?: boolean
readonly disable_password_auth?: boolean
readonly support?: SupportConfig
- // Named type "github.com/coder/coder/cli/clibase.Struct[[]github.com/coder/coder/codersdk.GitAuthConfig]" unknown, using "any"
+ // Named type "github.com/coder/coder/v2/cli/clibase.Struct[[]github.com/coder/coder/v2/codersdk.GitAuthConfig]" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
readonly git_auth?: any
readonly config_ssh?: SSHConfig
@@ -399,10 +401,10 @@ export interface DeploymentValues {
readonly proxy_health_status_interval?: number
readonly enable_terraform_debug_mode?: boolean
readonly user_quiet_hours_schedule?: UserQuietHoursScheduleConfig
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.YAMLConfigPath")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.YAMLConfigPath")
readonly config?: string
readonly write_config?: boolean
- // Named type "github.com/coder/coder/cli/clibase.HostPort" unknown, using "any"
+ // Named type "github.com/coder/coder/v2/cli/clibase.HostPort" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
readonly address?: any
}
@@ -415,6 +417,7 @@ export interface Entitlements {
readonly has_license: boolean
readonly trial: boolean
readonly require_telemetry: boolean
+ readonly refreshed_at: string
}
// From codersdk/deployment.go
@@ -513,6 +516,7 @@ export interface Group {
readonly members: User[]
readonly avatar_url: string
readonly quota_allowance: number
+ readonly source: GroupSource
}
// From codersdk/workspaceapps.go
@@ -552,7 +556,7 @@ export interface LinkConfig {
// From codersdk/deployment.go
export interface LoggingConfig {
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly log_filter: string[]
readonly human: string
readonly json: string
@@ -586,9 +590,9 @@ export interface OAuth2Config {
export interface OAuth2GithubConfig {
readonly client_id: string
readonly client_secret: string
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly allowed_orgs: string[]
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly allowed_teams: string[]
readonly allow_signups: boolean
readonly allow_everyone: boolean
@@ -614,27 +618,33 @@ export interface OIDCConfig {
readonly allow_signups: boolean
readonly client_id: string
readonly client_secret: string
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ readonly client_key_file: string
+ readonly client_cert_file: string
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly email_domain: string[]
readonly issuer_url: string
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly scopes: string[]
readonly ignore_email_verified: boolean
readonly username_field: string
readonly email_field: string
- // Named type "github.com/coder/coder/cli/clibase.Struct[map[string]string]" unknown, using "any"
+ // Named type "github.com/coder/coder/v2/cli/clibase.Struct[map[string]string]" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
readonly auth_url_params: any
readonly ignore_user_info: boolean
+ readonly group_auto_create: boolean
+ // Named type "github.com/coder/coder/v2/cli/clibase.Regexp" unknown, using "any"
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
+ readonly group_regex_filter: any
readonly groups_field: string
- // Named type "github.com/coder/coder/cli/clibase.Struct[map[string]string]" unknown, using "any"
+ // Named type "github.com/coder/coder/v2/cli/clibase.Struct[map[string]string]" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
readonly group_mapping: any
readonly user_role_field: string
- // Named type "github.com/coder/coder/cli/clibase.Struct[map[string][]string]" unknown, using "any"
+ // Named type "github.com/coder/coder/v2/cli/clibase.Struct[map[string][]string]" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
readonly user_role_mapping: any
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly user_roles_default: string[]
readonly sign_in_text: string
readonly icon_url: string
@@ -692,7 +702,7 @@ export interface PatchWorkspaceProxy {
// From codersdk/deployment.go
export interface PprofConfig {
readonly enable: boolean
- // Named type "github.com/coder/coder/cli/clibase.HostPort" unknown, using "any"
+ // Named type "github.com/coder/coder/v2/cli/clibase.HostPort" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
readonly address: any
}
@@ -700,7 +710,7 @@ export interface PprofConfig {
// From codersdk/deployment.go
export interface PrometheusConfig {
readonly enable: boolean
- // Named type "github.com/coder/coder/cli/clibase.HostPort" unknown, using "any"
+ // Named type "github.com/coder/coder/v2/cli/clibase.HostPort" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
readonly address: any
readonly collect_agent_stats: boolean
@@ -714,6 +724,7 @@ export interface ProvisionerConfig {
readonly daemon_poll_interval: number
readonly daemon_poll_jitter: number
readonly force_cancel_interval: number
+ readonly daemon_psk: string
}
// From codersdk/provisionerdaemons.go
@@ -813,7 +824,7 @@ export interface Role {
// From codersdk/deployment.go
export interface SSHConfig {
readonly DeploymentName: string
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly SSHConfigOptions: string[]
}
@@ -848,7 +859,7 @@ export interface SessionCountDeploymentStats {
// From codersdk/deployment.go
export interface SupportConfig {
- // Named type "github.com/coder/coder/cli/clibase.Struct[[]github.com/coder/coder/codersdk.LinkConfig]" unknown, using "any"
+ // Named type "github.com/coder/coder/v2/cli/clibase.Struct[[]github.com/coder/coder/v2/codersdk.LinkConfig]" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
readonly links: any
}
@@ -861,15 +872,15 @@ export interface SwaggerConfig {
// From codersdk/deployment.go
export interface TLSConfig {
readonly enable: boolean
- // Named type "github.com/coder/coder/cli/clibase.HostPort" unknown, using "any"
+ // Named type "github.com/coder/coder/v2/cli/clibase.HostPort" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
readonly address: any
readonly redirect_http: boolean
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly cert_file: string[]
readonly client_auth: string
readonly client_ca_file: string
- // This is likely an enum in an external package ("github.com/coder/coder/cli/clibase.StringArray")
+ // This is likely an enum in an external package ("github.com/coder/coder/v2/cli/clibase.StringArray")
readonly key_file: string[]
readonly min_version: string
readonly client_cert_file: string
@@ -906,8 +917,8 @@ export interface Template {
readonly allow_user_autostop: boolean
readonly allow_user_cancel_workspace_jobs: boolean
readonly failure_ttl_ms: number
- readonly inactivity_ttl_ms: number
- readonly locked_ttl_ms: number
+ readonly time_til_dormant_ms: number
+ readonly time_til_dormant_autodelete_ms: number
}
// From codersdk/templates.go
@@ -986,6 +997,8 @@ export interface TemplateParameterUsage {
readonly template_ids: string[]
readonly display_name: string
readonly name: string
+ readonly type: string
+ readonly description: string
readonly options?: TemplateVersionParameterOption[]
readonly values: TemplateParameterValue[]
}
@@ -1139,8 +1152,10 @@ export interface UpdateTemplateMeta {
readonly allow_user_autostop?: boolean
readonly allow_user_cancel_workspace_jobs?: boolean
readonly failure_ttl_ms?: number
- readonly inactivity_ttl_ms?: number
- readonly locked_ttl_ms?: number
+ readonly time_til_dormant_ms?: number
+ readonly time_til_dormant_autodelete_ms?: number
+ readonly update_workspace_last_used_at: boolean
+ readonly update_workspace_dormant_at: boolean
}
// From codersdk/users.go
@@ -1165,8 +1180,8 @@ export interface UpdateWorkspaceAutostartRequest {
}
// From codersdk/workspaces.go
-export interface UpdateWorkspaceLock {
- readonly lock: boolean
+export interface UpdateWorkspaceDormancy {
+ readonly dormant: boolean
}
// From codersdk/workspaceproxy.go
@@ -1288,6 +1303,7 @@ export interface Workspace {
readonly template_display_name: string
readonly template_icon: string
readonly template_allow_user_cancel_workspace_jobs: boolean
+ readonly template_active_version_id: string
readonly latest_build: WorkspaceBuild
readonly outdated: boolean
readonly name: string
@@ -1295,7 +1311,7 @@ export interface Workspace {
readonly ttl_ms?: number
readonly last_used_at: string
readonly deleting_at?: string
- readonly locked_at?: string
+ readonly dormant_at?: string
readonly health: WorkspaceHealth
}
@@ -1332,7 +1348,7 @@ export interface WorkspaceAgent {
readonly login_before_ready: boolean
readonly shutdown_script?: string
readonly shutdown_script_timeout_seconds: number
- readonly subsystem: AgentSubsystem
+ readonly subsystems: AgentSubsystem[]
readonly health: WorkspaceAgentHealth
}
@@ -1537,8 +1553,12 @@ export type APIKeyScope = "all" | "application_connect"
export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"]
// From codersdk/workspaceagents.go
-export type AgentSubsystem = "envbox"
-export const AgentSubsystems: AgentSubsystem[] = ["envbox"]
+export type AgentSubsystem = "envbox" | "envbuilder" | "exectrace"
+export const AgentSubsystems: AgentSubsystem[] = [
+ "envbox",
+ "envbuilder",
+ "exectrace",
+]
// From codersdk/audit.go
export type AuditAction =
@@ -1585,6 +1605,7 @@ export type Experiment =
| "tailnet_pg_coordinator"
| "template_restart_requirement"
| "workspace_actions"
+ | "workspaces_batch_actions"
export const Experiments: Experiment[] = [
"deployment_health_page",
"moons",
@@ -1592,6 +1613,7 @@ export const Experiments: Experiment[] = [
"tailnet_pg_coordinator",
"template_restart_requirement",
"workspace_actions",
+ "workspaces_batch_actions",
]
// From codersdk/deployment.go
@@ -1634,18 +1656,17 @@ export const GitProviders: GitProvider[] = [
"gitlab",
]
+// From codersdk/groups.go
+export type GroupSource = "oidc" | "user"
+export const GroupSources: GroupSource[] = ["oidc", "user"]
+
// From codersdk/insights.go
export type InsightsReportInterval = "day"
export const InsightsReportIntervals: InsightsReportInterval[] = ["day"]
// From codersdk/provisionerdaemons.go
-export type JobErrorCode =
- | "MISSING_TEMPLATE_PARAMETER"
- | "REQUIRED_TEMPLATE_VARIABLES"
-export const JobErrorCodes: JobErrorCode[] = [
- "MISSING_TEMPLATE_PARAMETER",
- "REQUIRED_TEMPLATE_VARIABLES",
-]
+export type JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES"
+export const JobErrorCodes: JobErrorCode[] = ["REQUIRED_TEMPLATE_VARIABLES"]
// From codersdk/provisionerdaemons.go
export type LogLevel = "debug" | "error" | "info" | "trace" | "warn"
@@ -1656,8 +1677,9 @@ export type LogSource = "provisioner" | "provisioner_daemon"
export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"]
// From codersdk/apikey.go
-export type LoginType = "github" | "none" | "oidc" | "password" | "token"
+export type LoginType = "" | "github" | "none" | "oidc" | "password" | "token"
export const LoginTypes: LoginType[] = [
+ "",
"github",
"none",
"oidc",
@@ -1763,22 +1785,26 @@ export type ResourceType =
| "git_ssh_key"
| "group"
| "license"
+ | "organization"
| "template"
| "template_version"
| "user"
| "workspace"
| "workspace_build"
+ | "workspace_proxy"
export const ResourceTypes: ResourceType[] = [
"api_key",
"convert_login",
"git_ssh_key",
"group",
"license",
+ "organization",
"template",
"template_version",
"user",
"workspace",
"workspace_build",
+ "workspace_proxy",
]
// From codersdk/serversentevents.go
@@ -1790,8 +1816,8 @@ export const ServerSentEventTypes: ServerSentEventType[] = [
]
// From codersdk/insights.go
-export type TemplateAppsType = "builtin"
-export const TemplateAppsTypes: TemplateAppsType[] = ["builtin"]
+export type TemplateAppsType = "app" | "builtin"
+export const TemplateAppsTypes: TemplateAppsType[] = ["app", "builtin"]
// From codersdk/templates.go
export type TemplateRole = "" | "admin" | "use"
diff --git a/site/src/components/Alert/Alert.stories.tsx b/site/src/components/Alert/Alert.stories.tsx
index ada32cd994ec9..c5139f7c1c1a1 100644
--- a/site/src/components/Alert/Alert.stories.tsx
+++ b/site/src/components/Alert/Alert.stories.tsx
@@ -17,6 +17,14 @@ const ExampleAction = (
)
+export const Success: Story = {
+ args: {
+ children: "You're doing great!",
+ severity: "success",
+ onRetry: undefined,
+ },
+}
+
export const Warning: Story = {
args: {
children: "This is a warning",
diff --git a/site/src/components/BuildIcon/BuildIcon.tsx b/site/src/components/BuildIcon/BuildIcon.tsx
new file mode 100644
index 0000000000000..ccdc68da1cb3e
--- /dev/null
+++ b/site/src/components/BuildIcon/BuildIcon.tsx
@@ -0,0 +1,22 @@
+import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined"
+import StopOutlined from "@mui/icons-material/StopOutlined"
+import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
+import { WorkspaceTransition } from "api/typesGenerated"
+import { ComponentProps } from "react"
+
+type SVGIcon = typeof PlayArrowOutlined
+
+type SVGIconProps = ComponentProps
+
+const iconByTransition: Record = {
+ start: PlayArrowOutlined,
+ stop: StopOutlined,
+ delete: DeleteOutlined,
+}
+
+export const BuildIcon = (
+ props: SVGIconProps & { transition: WorkspaceTransition },
+) => {
+ const Icon = iconByTransition[props.transition]
+ return
+}
diff --git a/site/src/components/BuildsTable/BuildAvatar.tsx b/site/src/components/BuildsTable/BuildAvatar.tsx
index e095bda76a32e..3733db4650ffc 100644
--- a/site/src/components/BuildsTable/BuildAvatar.tsx
+++ b/site/src/components/BuildsTable/BuildAvatar.tsx
@@ -1,14 +1,12 @@
import Badge from "@mui/material/Badge"
import { useTheme, withStyles } from "@mui/styles"
import { FC } from "react"
-import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined"
-import PauseOutlined from "@mui/icons-material/PauseOutlined"
-import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
-import { WorkspaceBuild, WorkspaceTransition } from "api/typesGenerated"
+import { WorkspaceBuild } from "api/typesGenerated"
import { getDisplayWorkspaceBuildStatus } from "utils/workspace"
import { Avatar, AvatarProps } from "components/Avatar/Avatar"
import { PaletteIndex } from "theme/theme"
import { Theme } from "@mui/material/styles"
+import { BuildIcon } from "components/BuildIcon/BuildIcon"
interface StylesBadgeProps {
type: PaletteIndex
@@ -31,12 +29,6 @@ export interface BuildAvatarProps {
size?: AvatarProps["size"]
}
-const iconByTransition: Record = {
- start: ,
- stop: ,
- delete: ,
-}
-
export const BuildAvatar: FC = ({ build, size }) => {
const theme = useTheme()
const displayBuildStatus = getDisplayWorkspaceBuildStatus(theme, build)
@@ -55,7 +47,7 @@ export const BuildAvatar: FC = ({ build, size }) => {
badgeContent={
}
>
- {iconByTransition[build.transition]}
+
)
diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx
index c2f03155e7c62..c0423bfc5101f 100644
--- a/site/src/components/CreateUserForm/CreateUserForm.tsx
+++ b/site/src/components/CreateUserForm/CreateUserForm.tsx
@@ -13,6 +13,10 @@ import { FullPageForm } from "../FullPageForm/FullPageForm"
import { Stack } from "../Stack/Stack"
import { ErrorAlert } from "components/Alert/ErrorAlert"
import { hasApiFieldErrors, isApiError } from "api/errors"
+import MenuItem from "@mui/material/MenuItem"
+import { makeStyles } from "@mui/styles"
+import { Theme } from "@mui/material/styles"
+import Link from "@mui/material/Link"
export const Language = {
emailLabel: "Email",
@@ -25,12 +29,44 @@ export const Language = {
cancel: "Cancel",
}
+export const authMethodLanguage = {
+ password: {
+ displayName: "Password",
+ description: "Use an email address and password to login",
+ },
+ oidc: {
+ displayName: "OpenID Connect",
+ description: "Use an OpenID Connect provider for authentication",
+ },
+ github: {
+ displayName: "Github",
+ description: "Use Github OAuth for authentication",
+ },
+ none: {
+ displayName: "None",
+ description: (
+ <>
+ Disable authentication for this user (See the{" "}
+
+ documentation
+ {" "}
+ for more details)
+ >
+ ),
+ },
+}
+
export interface CreateUserFormProps {
onSubmit: (user: TypesGen.CreateUserRequest) => void
onCancel: () => void
error?: unknown
isLoading: boolean
myOrgId: string
+ authMethods?: TypesGen.AuthMethods
}
const validationSchema = Yup.object({
@@ -38,13 +74,18 @@ const validationSchema = Yup.object({
.trim()
.email(Language.emailInvalid)
.required(Language.emailRequired),
- password: Yup.string().required(Language.passwordRequired),
+ password: Yup.string().when("login_type", {
+ is: "password",
+ then: (schema) => schema.required(Language.passwordRequired),
+ otherwise: (schema) => schema,
+ }),
username: nameValidator(Language.usernameLabel),
+ login_type: Yup.string().oneOf(Object.keys(authMethodLanguage)),
})
export const CreateUserForm: FC<
React.PropsWithChildren
-> = ({ onSubmit, onCancel, error, isLoading, myOrgId }) => {
+> = ({ onSubmit, onCancel, error, isLoading, myOrgId, authMethods }) => {
const form: FormikContextType =
useFormik({
initialValues: {
@@ -53,6 +94,7 @@ export const CreateUserForm: FC<
username: "",
organization_id: myOrgId,
disable_login: false,
+ login_type: "",
},
validationSchema,
onSubmit,
@@ -62,6 +104,15 @@ export const CreateUserForm: FC<
error,
)
+ const styles = useStyles()
+
+ const methods = [
+ authMethods?.password.enabled && "password",
+ authMethods?.oidc.enabled && "oidc",
+ authMethods?.github.enabled && "github",
+ "none",
+ ].filter(Boolean) as Array
+
return (
{isApiError(error) && !hasApiFieldErrors(error) && (
@@ -85,10 +136,53 @@ export const CreateUserForm: FC<
label={Language.emailLabel}
/>
{
+ if (e.target.value !== "password") {
+ await form.setFieldValue("password", "")
+ }
+ await form.setFieldValue("login_type", e.target.value)
+ }}
+ SelectProps={{
+ renderValue: (selected: unknown) =>
+ authMethodLanguage[selected as keyof typeof authMethodLanguage]
+ ?.displayName ?? "",
+ }}
+ >
+ {methods.map((value) => {
+ const language = authMethodLanguage[value]
+ return (
+
+
+ {language.displayName}
+
+ {language.description}
+
+
+
+ )
+ })}
+
+
@@ -98,3 +192,12 @@ export const CreateUserForm: FC<
)
}
+
+const useStyles = makeStyles((theme) => ({
+ labelDescription: {
+ fontSize: 14,
+ color: theme.palette.text.secondary,
+ wordWrap: "normal",
+ whiteSpace: "break-spaces",
+ },
+}))
diff --git a/site/src/components/DAUChart/DAUChart.tsx b/site/src/components/DAUChart/DAUChart.tsx
index add0b6aa17db0..ae93b0e086dff 100644
--- a/site/src/components/DAUChart/DAUChart.tsx
+++ b/site/src/components/DAUChart/DAUChart.tsx
@@ -9,8 +9,7 @@ import {
defaults,
Legend,
LinearScale,
- LineElement,
- PointElement,
+ BarElement,
TimeScale,
Title,
Tooltip,
@@ -23,14 +22,13 @@ import {
} from "components/Tooltips/HelpTooltip"
import dayjs from "dayjs"
import { FC } from "react"
-import { Line } from "react-chartjs-2"
+import { Bar } from "react-chartjs-2"
ChartJS.register(
CategoryScale,
LinearScale,
TimeScale,
- PointElement,
- LineElement,
+ BarElement,
Title,
Tooltip,
Legend,
@@ -54,12 +52,21 @@ export const DAUChart: FC = ({ daus }) => {
defaults.font.family = theme.typography.fontFamily as string
defaults.color = theme.palette.text.secondary
- const options: ChartOptions<"line"> = {
+ const options: ChartOptions<"bar"> = {
responsive: true,
plugins: {
legend: {
display: false,
},
+ tooltip: {
+ displayColors: false,
+ callbacks: {
+ title: (context) => {
+ const date = new Date(context[0].parsed.x)
+ return date.toLocaleDateString()
+ },
+ },
+ },
},
scales: {
y: {
@@ -69,11 +76,12 @@ export const DAUChart: FC = ({ daus }) => {
},
},
x: {
- ticks: {},
+ ticks: {
+ stepSize: daus.entries.length > 10 ? 2 : undefined,
+ },
type: "time",
time: {
unit: "day",
- stepSize: 2,
},
},
},
@@ -81,7 +89,7 @@ export const DAUChart: FC = ({ daus }) => {
}
return (
- = ({ daus }) => {
{
label: "Daily Active Users",
data: data,
- tension: 1 / 4,
backgroundColor: theme.palette.secondary.dark,
borderColor: theme.palette.secondary.dark,
+ barThickness: 8,
+ borderWidth: 2,
+ borderRadius: Number.MAX_VALUE,
+ borderSkipped: false,
},
],
}}
diff --git a/site/src/components/Dashboard/DashboardLayout.tsx b/site/src/components/Dashboard/DashboardLayout.tsx
index e7f01c9e3a161..605f305a49356 100644
--- a/site/src/components/Dashboard/DashboardLayout.tsx
+++ b/site/src/components/Dashboard/DashboardLayout.tsx
@@ -12,7 +12,7 @@ import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService"
import { Navbar } from "../Navbar/Navbar"
import Snackbar from "@mui/material/Snackbar"
import Link from "@mui/material/Link"
-import Box from "@mui/material/Box"
+import Box, { BoxProps } from "@mui/material/Box"
import InfoOutlined from "@mui/icons-material/InfoOutlined"
import Button from "@mui/material/Button"
import { docs } from "utils/docs"
@@ -102,10 +102,27 @@ export const DashboardLayout: FC = () => {
)
}
+export const DashboardFullPage = (props: BoxProps) => {
+ return (
+
+ )
+}
+
const useStyles = makeStyles({
site: {
display: "flex",
- minHeight: "100vh",
+ minHeight: "100%",
flexDirection: "column",
},
siteContent: {
diff --git a/site/src/components/Dashboard/DashboardProvider.tsx b/site/src/components/Dashboard/DashboardProvider.tsx
index fd9065abdb27a..ed26b64ffc481 100644
--- a/site/src/components/Dashboard/DashboardProvider.tsx
+++ b/site/src/components/Dashboard/DashboardProvider.tsx
@@ -87,3 +87,13 @@ export const useDashboard = (): DashboardProviderValue => {
return context
}
+
+export const useIsWorkspaceActionsEnabled = (): boolean => {
+ const { entitlements, experiments } = useDashboard()
+ const allowAdvancedScheduling =
+ entitlements.features["advanced_template_scheduling"].enabled
+ // This check can be removed when https://github.com/coder/coder/milestone/19
+ // is merged up
+ const allowWorkspaceActions = experiments.includes("workspace_actions")
+ return allowWorkspaceActions && allowAdvancedScheduling
+}
diff --git a/site/src/components/DeploySettingsLayout/Badges.tsx b/site/src/components/DeploySettingsLayout/Badges.tsx
index f9d34bca918b6..465422ef03a3f 100644
--- a/site/src/components/DeploySettingsLayout/Badges.tsx
+++ b/site/src/components/DeploySettingsLayout/Badges.tsx
@@ -23,11 +23,15 @@ export const EntitledBadge: FC = () => {
)
}
-export const HealthyBadge: FC = () => {
+export const HealthyBadge: FC<{ derpOnly: boolean }> = ({ derpOnly }) => {
const styles = useStyles()
+ let text = "Healthy"
+ if (derpOnly) {
+ text = "Healthy (DERP Only)"
+ }
return (
- Healthy
+ {text}
)
}
diff --git a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx
index edee6dc836491..1a5db47748a8b 100644
--- a/site/src/components/DeploymentBanner/DeploymentBannerView.tsx
+++ b/site/src/components/DeploymentBanner/DeploymentBannerView.tsx
@@ -260,7 +260,7 @@ const useStyles = makeStyles((theme) => ({
height: bannerHeight,
bottom: 0,
zIndex: 1,
- padding: theme.spacing(1, 2),
+ padding: theme.spacing(0, 2),
backgroundColor: theme.palette.background.paper,
display: "flex",
alignItems: "center",
@@ -268,12 +268,8 @@ const useStyles = makeStyles((theme) => ({
fontSize: 12,
gap: theme.spacing(4),
borderTop: `1px solid ${theme.palette.divider}`,
-
- [theme.breakpoints.down("lg")]: {
- flexDirection: "column",
- gap: theme.spacing(1),
- alignItems: "left",
- },
+ overflowX: "auto",
+ whiteSpace: "nowrap",
},
group: {
display: "flex",
diff --git a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx
index 5d77d34b28906..b16437ea62375 100644
--- a/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx
+++ b/site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx
@@ -7,6 +7,9 @@ import {
DialogActionButtonsProps,
} from "../Dialog"
import { ConfirmDialogType } from "../types"
+import Checkbox from "@mui/material/Checkbox"
+import FormControlLabel from "@mui/material/FormControlLabel"
+import { Stack } from "@mui/system"
interface ConfirmDialogTypeConfig {
confirmText: ReactNode
@@ -151,3 +154,168 @@ export const ConfirmDialog: FC> = ({
)
}
+
+export interface ScheduleDialogProps extends ConfirmDialogProps {
+ readonly inactiveWorkspacesToGoDormant: number
+ readonly inactiveWorkspacesToGoDormantInWeek: number
+ readonly dormantWorkspacesToBeDeleted: number
+ readonly dormantWorkspacesToBeDeletedInWeek: number
+ readonly updateDormantWorkspaces: (confirm: boolean) => void
+ readonly updateInactiveWorkspaces: (confirm: boolean) => void
+ readonly dormantValueChanged: boolean
+ readonly deletionValueChanged: boolean
+}
+
+export const ScheduleDialog: FC> = ({
+ cancelText,
+ confirmLoading,
+ disabled = false,
+ hideCancel,
+ onClose,
+ onConfirm,
+ type,
+ open = false,
+ title,
+ inactiveWorkspacesToGoDormant,
+ inactiveWorkspacesToGoDormantInWeek,
+ dormantWorkspacesToBeDeleted,
+ dormantWorkspacesToBeDeletedInWeek,
+ updateDormantWorkspaces,
+ updateInactiveWorkspaces,
+ dormantValueChanged,
+ deletionValueChanged,
+}) => {
+ const styles = useScheduleStyles({ type })
+
+ const defaults = CONFIRM_DIALOG_DEFAULTS["delete"]
+
+ if (typeof hideCancel === "undefined") {
+ hideCancel = defaults.hideCancel
+ }
+
+ const showDormancyWarning =
+ dormantValueChanged &&
+ (inactiveWorkspacesToGoDormant > 0 ||
+ inactiveWorkspacesToGoDormantInWeek > 0)
+ const showDeletionWarning =
+ deletionValueChanged &&
+ (dormantWorkspacesToBeDeleted > 0 || dormantWorkspacesToBeDeletedInWeek > 0)
+
+ return (
+
+
+
{title}
+ <>
+ {showDormancyWarning && (
+ <>
+
{"Dormancy Threshold"}
+
+ {`
+ This change will result in ${inactiveWorkspacesToGoDormant} workspaces being immediately transitioned to the dormant state and ${inactiveWorkspacesToGoDormantInWeek} over the next seven days. To prevent this, do you want to reset the inactivity period for all template workspaces?`}
+ {
+ updateInactiveWorkspaces(e.target.checked)
+ }}
+ />
+ }
+ label="Reset"
+ />
+
+ >
+ )}
+
+ {showDeletionWarning && (
+ <>
+
{"Dormancy Auto-Deletion"}
+
+ {`This change will result in ${dormantWorkspacesToBeDeleted} workspaces being immediately deleted and ${dormantWorkspacesToBeDeletedInWeek} over the next 7 days. To prevent this, do you want to reset the dormancy period for all template workspaces?`}
+ {
+ updateDormantWorkspaces(e.target.checked)
+ }}
+ />
+ }
+ label="Reset"
+ />
+
+ >
+ )}
+ >
+
+
+
+
+
+
+ )
+}
+
+const useScheduleStyles = makeStyles((theme) => ({
+ dialogWrapper: {
+ "& .MuiPaper-root": {
+ background: theme.palette.background.paper,
+ border: `1px solid ${theme.palette.divider}`,
+ width: "100%",
+ maxWidth: theme.spacing(125),
+ },
+ "& .MuiDialogActions-spacing": {
+ padding: `0 ${theme.spacing(5)} ${theme.spacing(5)}`,
+ },
+ },
+ dialogContent: {
+ color: theme.palette.text.secondary,
+ padding: theme.spacing(5),
+ },
+ dialogTitle: {
+ margin: 0,
+ marginBottom: theme.spacing(2),
+ color: theme.palette.text.primary,
+ fontWeight: 400,
+ fontSize: theme.spacing(2.5),
+ },
+ dialogDescription: {
+ color: theme.palette.text.secondary,
+ lineHeight: "160%",
+ fontSize: 16,
+
+ "& strong": {
+ color: theme.palette.text.primary,
+ },
+
+ "& p:not(.MuiFormHelperText-root)": {
+ margin: 0,
+ },
+
+ "& > p": {
+ margin: theme.spacing(1, 0),
+ },
+ },
+}))
diff --git a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx
index 0a56fe65b25c6..f46405cbb4c31 100644
--- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx
+++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx
@@ -39,7 +39,7 @@ export const DeleteDialog: FC> = ({
{info}
- {t("deleteDialog.confirm", { entity })}
+ {t("deleteDialog.confirm", { entity, name })}