Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84ec2dc1db | |||
| a11b169029 | |||
| 2a98123701 | |||
| 2ed7226e85 | |||
| 2101dbce03 | |||
| cdeba67944 | |||
| bda13a2818 | |||
| 353888a5d8 | |||
| 3fc6111994 | |||
| 3eb9abcbd3 |
@@ -9,7 +9,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: buildjet/setup-go@v5
|
||||
uses: buildjet/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ inputs.version }}
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
- name: Setup Node
|
||||
uses: buildjet/setup-node@v4.0.1
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18.19.0
|
||||
# See https://github.com/actions/setup-node#caching-global-packages-data
|
||||
|
||||
@@ -7,5 +7,5 @@ runs:
|
||||
- name: Install Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: 1.7.5
|
||||
terraform_version: 1.5.7
|
||||
terraform_wrapper: false
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
notify:
|
||||
after_n_builds: 5
|
||||
|
||||
comment: false
|
||||
|
||||
github_checks:
|
||||
annotations: false
|
||||
|
||||
coverage:
|
||||
range: 50..75
|
||||
round: down
|
||||
precision: 2
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
informational: yes
|
||||
project:
|
||||
default:
|
||||
target: 65%
|
||||
informational: true
|
||||
|
||||
ignore:
|
||||
# This is generated code.
|
||||
- coderd/database/models.go
|
||||
- coderd/database/queries.sql.go
|
||||
- coderd/database/databasefake
|
||||
# These are generated or don't require tests.
|
||||
- cmd
|
||||
- coderd/tunnel
|
||||
- coderd/database/dump
|
||||
- coderd/database/postgres
|
||||
- peerbroker/proto
|
||||
- provisionerd/proto
|
||||
- provisionersdk/proto
|
||||
- scripts
|
||||
- site/.storybook
|
||||
- rules.go
|
||||
# Packages used for writing tests.
|
||||
- cli/clitest
|
||||
- coderd/coderdtest
|
||||
- pty/ptytest
|
||||
+76
-67
@@ -142,7 +142,7 @@ jobs:
|
||||
|
||||
# Check for any typos
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.20.10
|
||||
uses: crate-ci/typos@v1.19.0
|
||||
with:
|
||||
config: .github/workflows/typos.toml
|
||||
|
||||
@@ -269,6 +269,16 @@ jobs:
|
||||
id: test
|
||||
shell: bash
|
||||
run: |
|
||||
# Code coverage is more computationally expensive and also
|
||||
# prevents test caching, so we disable it on alternate operating
|
||||
# systems.
|
||||
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
|
||||
echo "cover=true" >> $GITHUB_OUTPUT
|
||||
export COVERAGE_FLAGS='-covermode=atomic -coverprofile="gotests.coverage" -coverpkg=./...'
|
||||
else
|
||||
echo "cover=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# if macOS, install google-chrome for scaletests. As another concern,
|
||||
# should we really have this kind of external dependency requirement
|
||||
# on standard CI?
|
||||
@@ -287,7 +297,7 @@ jobs:
|
||||
fi
|
||||
export TS_DEBUG_DISCO=true
|
||||
gotestsum --junitfile="gotests.xml" --jsonfile="gotests.json" \
|
||||
--packages="./..." -- $PARALLEL_FLAG -short -failfast
|
||||
--packages="./..." -- $PARALLEL_FLAG -short -failfast $COVERAGE_FLAGS
|
||||
|
||||
- name: Upload test stats to Datadog
|
||||
timeout-minutes: 1
|
||||
@@ -297,6 +307,19 @@ jobs:
|
||||
with:
|
||||
api-key: ${{ secrets.DATADOG_API_KEY }}
|
||||
|
||||
- name: Check code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
# This action has a tendency to error out unexpectedly, it has
|
||||
# the `fail_ci_if_error` option that defaults to `false`, but
|
||||
# that is no guarantee, see:
|
||||
# https://github.com/codecov/codecov-action/issues/788
|
||||
continue-on-error: true
|
||||
if: steps.test.outputs.cover && github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./gotests.coverage
|
||||
flags: unittest-go-${{ matrix.os }}
|
||||
|
||||
test-go-pg:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
needs:
|
||||
@@ -332,6 +355,19 @@ jobs:
|
||||
with:
|
||||
api-key: ${{ secrets.DATADOG_API_KEY }}
|
||||
|
||||
- name: Check code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
# This action has a tendency to error out unexpectedly, it has
|
||||
# the `fail_ci_if_error` option that defaults to `false`, but
|
||||
# that is no guarantee, see:
|
||||
# https://github.com/codecov/codecov-action/issues/788
|
||||
continue-on-error: true
|
||||
if: github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./gotests.coverage
|
||||
flags: unittest-go-postgres-linux
|
||||
|
||||
test-go-race:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
needs: changes
|
||||
@@ -378,20 +414,24 @@ jobs:
|
||||
- run: pnpm test:ci --max-workers $(nproc)
|
||||
working-directory: site
|
||||
|
||||
- name: Check code coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
# This action has a tendency to error out unexpectedly, it has
|
||||
# the `fail_ci_if_error` option that defaults to `false`, but
|
||||
# that is no guarantee, see:
|
||||
# https://github.com/codecov/codecov-action/issues/788
|
||||
continue-on-error: true
|
||||
if: github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./site/coverage/lcov.info
|
||||
flags: unittest-js
|
||||
|
||||
test-e2e:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-16vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
needs: changes
|
||||
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant:
|
||||
- enterprise: false
|
||||
name: test-e2e
|
||||
- enterprise: true
|
||||
name: test-e2e-enterprise
|
||||
name: ${{ matrix.variant.name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -404,40 +444,52 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
# Assume that the checked-in versions are up-to-date
|
||||
- run: make gen/mark-fresh
|
||||
name: make gen
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
|
||||
- run: pnpm build
|
||||
working-directory: site
|
||||
- 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 go.uber.org/mock/mockgen@v0.4.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
|
||||
|
||||
- run: pnpm playwright:install
|
||||
working-directory: site
|
||||
|
||||
# Run tests that don't require an enterprise license without an enterprise license
|
||||
- run: pnpm playwright:test --forbid-only --workers 1
|
||||
if: ${{ !matrix.variant.enterprise }}
|
||||
env:
|
||||
DEBUG: pw:api
|
||||
working-directory: site
|
||||
|
||||
# Run all of the tests with an enterprise license
|
||||
- run: pnpm playwright:test --forbid-only --workers 1
|
||||
if: ${{ matrix.variant.enterprise }}
|
||||
env:
|
||||
DEBUG: pw:api
|
||||
CODER_E2E_ENTERPRISE_LICENSE: ${{ secrets.CODER_E2E_ENTERPRISE_LICENSE }}
|
||||
CODER_E2E_REQUIRE_ENTERPRISE_TESTS: "1"
|
||||
working-directory: site
|
||||
# Temporarily allow these to fail so that I can gather data about which
|
||||
# tests are failing.
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload Playwright Failed Tests
|
||||
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: failed-test-videos${{ matrix.variant.enterprise && '-enterprise' || '-agpl' }}
|
||||
name: failed-test-videos
|
||||
path: ./site/test-results/**/*.webm
|
||||
retention-days: 7
|
||||
|
||||
@@ -445,7 +497,7 @@ jobs:
|
||||
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debug-pprof-dumps${{ matrix.variant.enterprise && '-enterprise' || '-agpl' }}
|
||||
name: debug-pprof-dumps
|
||||
path: ./site/test-results/**/debug-pprof-*.txt
|
||||
retention-days: 7
|
||||
|
||||
@@ -591,7 +643,6 @@ jobs:
|
||||
- test-e2e
|
||||
- offlinedocs
|
||||
- sqlc-vet
|
||||
- dependency-license-review
|
||||
# Allow this job to run even if the needed jobs fail, are skipped or
|
||||
# cancelled.
|
||||
if: always()
|
||||
@@ -608,7 +659,6 @@ jobs:
|
||||
echo "- test-js: ${{ needs.test-js.result }}"
|
||||
echo "- test-e2e: ${{ needs.test-e2e.result }}"
|
||||
echo "- offlinedocs: ${{ needs.offlinedocs.result }}"
|
||||
echo "- dependency-license-review: ${{ needs.dependency-license-review.result }}"
|
||||
echo
|
||||
|
||||
# We allow skipped jobs to pass, but not failed or cancelled jobs.
|
||||
@@ -849,44 +899,3 @@ jobs:
|
||||
- name: Setup and run sqlc vet
|
||||
run: |
|
||||
make sqlc-vet
|
||||
|
||||
# dependency-license-review checks that no license-incompatible dependencies have been introduced.
|
||||
# This action is not intended to do a vulnerability check since that is handled by a separate action.
|
||||
dependency-license-review:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref != 'refs/heads/main'
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
id: review
|
||||
# TODO: Replace this with the latest release once https://github.com/actions/dependency-review-action/pull/761 is merged.
|
||||
uses: actions/dependency-review-action@49fbbe0acb033b7824f26d00b005d7d598d76301
|
||||
with:
|
||||
allow-licenses: Apache-2.0, BSD-2-Clause, BSD-3-Clause, CC0-1.0, ISC, MIT, MIT-0, MPL-2.0
|
||||
allow-dependencies-licenses: "pkg:golang/github.com/pelletier/go-toml/v2"
|
||||
license-check: true
|
||||
vulnerability-check: false
|
||||
- name: "Report"
|
||||
# make sure this step runs even if the previous failed
|
||||
if: always()
|
||||
shell: bash
|
||||
env:
|
||||
VULNERABLE_CHANGES: ${{ steps.review.outputs.invalid-license-changes }}
|
||||
run: |
|
||||
fields=( "unlicensed" "unresolved" "forbidden" )
|
||||
|
||||
# This is unfortunate that we have to do this but the action does not support failing on
|
||||
# an unknown license. The unknown dependency could easily have a GPL license which
|
||||
# would be problematic for us.
|
||||
# Track https://github.com/actions/dependency-review-action/issues/672 for when
|
||||
# we can remove this brittle workaround.
|
||||
for field in "${fields[@]}"; do
|
||||
# Use jq to check if the array is not empty
|
||||
if [[ $(echo "$VULNERABLE_CHANGES" | jq ".${field} | length") -ne 0 ]]; then
|
||||
echo "Invalid or unknown licenses detected, contact @sreya to ensure your added dependency falls under one of our allowed licenses."
|
||||
echo "$VULNERABLE_CHANGES" | jq
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "No incompatible licenses detected"
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
},
|
||||
{
|
||||
"pattern": "tailscale.com"
|
||||
},
|
||||
{
|
||||
"pattern": "wireguard.com"
|
||||
}
|
||||
],
|
||||
"aliveStatusCodes": [200, 0]
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
# GitHub release workflow.
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_channel:
|
||||
type: choice
|
||||
description: Release channel
|
||||
options:
|
||||
- mainline
|
||||
- stable
|
||||
release_notes:
|
||||
description: Release notes for the publishing the release. This is required to create a release.
|
||||
dry_run:
|
||||
description: Perform a dry-run release (devel). Note that ref must be an annotated tag when run without dry-run.
|
||||
type: boolean
|
||||
@@ -33,8 +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 }}
|
||||
CODER_RELEASE_CHANNEL: ${{ inputs.release_channel }}
|
||||
CODER_RELEASE_NOTES: ${{ inputs.release_notes }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -69,45 +62,21 @@ jobs:
|
||||
echo "CODER_FORCE_VERSION=$version" >> $GITHUB_ENV
|
||||
echo "$version"
|
||||
|
||||
# Verify that all expectations for a release are met.
|
||||
- name: Verify release input
|
||||
if: ${{ !inputs.dry_run }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${GITHUB_REF}" != "refs/tags/v"* ]]; then
|
||||
echo "Ref must be a semver tag when creating a release, did you use scripts/release.sh?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2.10.2 -> release/2.10
|
||||
version="$(./scripts/version.sh)"
|
||||
release_branch=release/${version%.*}
|
||||
branch_contains_tag=$(git branch --remotes --contains "${GITHUB_REF}" --list "*/${release_branch}" --format='%(refname)')
|
||||
if [[ -z "${branch_contains_tag}" ]]; then
|
||||
echo "Ref tag must exist in a branch named ${release_branch} when creating a release, did you use scripts/release.sh?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${CODER_RELEASE_NOTES}" ]]; then
|
||||
echo "Release notes are required to create a release, did you use scripts/release.sh?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Release inputs verified:"
|
||||
echo
|
||||
echo "- Ref: ${GITHUB_REF}"
|
||||
echo "- Version: ${version}"
|
||||
echo "- Release channel: ${CODER_RELEASE_CHANNEL}"
|
||||
echo "- Release branch: ${release_branch}"
|
||||
echo "- Release notes: true"
|
||||
|
||||
- name: Create release notes file
|
||||
- name: Create release notes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# We always have to set this since there might be commits on
|
||||
# main that didn't have a PR.
|
||||
CODER_IGNORE_MISSING_COMMIT_METADATA: "1"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ref=HEAD
|
||||
old_version="$(git describe --abbrev=0 "$ref^1")"
|
||||
version="v$(./scripts/version.sh)"
|
||||
|
||||
# Generate notes.
|
||||
release_notes_file="$(mktemp -t release_notes.XXXXXX)"
|
||||
echo "$CODER_RELEASE_NOTES" > "$release_notes_file"
|
||||
./scripts/release/generate_release_notes.sh --check-for-changelog --old-version "$old_version" --new-version "$version" --ref "$ref" >> "$release_notes_file"
|
||||
echo CODER_RELEASE_NOTES_FILE="$release_notes_file" >> $GITHUB_ENV
|
||||
|
||||
- name: Show release notes
|
||||
@@ -128,13 +97,6 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
# Necessary for signing Windows binaries.
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11.0"
|
||||
|
||||
- name: Install nsis and zstd
|
||||
run: sudo apt-get install -y nsis zstd
|
||||
|
||||
@@ -168,32 +130,6 @@ jobs:
|
||||
AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }}
|
||||
AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }}
|
||||
|
||||
- name: Setup Windows EV Signing Certificate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
touch /tmp/ev_cert.pem
|
||||
chmod 600 /tmp/ev_cert.pem
|
||||
echo "$EV_SIGNING_CERT" > /tmp/ev_cert.pem
|
||||
wget https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar -O /tmp/jsign-6.0.jar
|
||||
env:
|
||||
EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }}
|
||||
|
||||
# - name: Test migrations from current ref to main
|
||||
# run: |
|
||||
# make test-migrations
|
||||
|
||||
# Setup GCloud for signing Windows binaries.
|
||||
- name: Authenticate to Google Cloud
|
||||
id: gcloud_auth
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }}
|
||||
service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }}
|
||||
token_format: "access_token"
|
||||
|
||||
- name: Setup GCloud SDK
|
||||
uses: "google-github-actions/setup-gcloud@v2"
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -208,26 +144,16 @@ jobs:
|
||||
build/coder_helm_"$version".tgz \
|
||||
build/provisioner_helm_"$version".tgz
|
||||
env:
|
||||
CODER_SIGN_WINDOWS: "1"
|
||||
CODER_SIGN_DARWIN: "1"
|
||||
AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
|
||||
AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
|
||||
AC_APIKEY_ISSUER_ID: ${{ secrets.AC_APIKEY_ISSUER_ID }}
|
||||
AC_APIKEY_ID: ${{ secrets.AC_APIKEY_ID }}
|
||||
AC_APIKEY_FILE: /tmp/apple_apikey.p8
|
||||
EV_KEY: ${{ secrets.EV_KEY }}
|
||||
EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }}
|
||||
EV_TSA_URL: ${{ secrets.EV_TSA_URL }}
|
||||
EV_CERTIFICATE_PATH: /tmp/ev_cert.pem
|
||||
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
|
||||
JSIGN_PATH: /tmp/jsign-6.0.jar
|
||||
|
||||
- name: Delete Apple Developer certificate and API key
|
||||
run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
|
||||
|
||||
- name: Delete Windows EV Signing Cert
|
||||
run: rm /tmp/ev_cert.pem
|
||||
|
||||
- name: Determine base image tag
|
||||
id: image-base-tag
|
||||
run: |
|
||||
@@ -335,9 +261,6 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
publish_args=()
|
||||
if [[ $CODER_RELEASE_CHANNEL == "stable" ]]; then
|
||||
publish_args+=(--stable)
|
||||
fi
|
||||
if [[ $CODER_DRY_RUN == *t* ]]; then
|
||||
publish_args+=(--dry-run)
|
||||
fi
|
||||
|
||||
@@ -15,7 +15,6 @@ Hashi = "Hashi"
|
||||
trialer = "trialer"
|
||||
encrypter = "encrypter"
|
||||
hel = "hel" # as in helsinki
|
||||
pn = "pn" # this is used as proto node
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
|
||||
@@ -4,11 +4,6 @@ on:
|
||||
schedule:
|
||||
- cron: "0 9 * * 1"
|
||||
workflow_dispatch: # allows to run manually for testing
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**"
|
||||
|
||||
jobs:
|
||||
check-docs:
|
||||
|
||||
@@ -200,8 +200,7 @@ endef
|
||||
# calling this manually.
|
||||
$(CODER_ALL_BINARIES): go.mod go.sum \
|
||||
$(GO_SRC_FILES) \
|
||||
$(shell find ./examples/templates) \
|
||||
site/static/error.html
|
||||
$(shell find ./examples/templates)
|
||||
|
||||
$(get-mode-os-arch-ext)
|
||||
if [[ "$$os" != "windows" ]] && [[ "$$ext" != "" ]]; then
|
||||
@@ -383,9 +382,9 @@ install: build/coder_$(VERSION)_$(GOOS)_$(GOARCH)$(GOOS_BIN_EXT)
|
||||
cp "$<" "$$output_file"
|
||||
.PHONY: install
|
||||
|
||||
BOLD := $(shell tput bold 2>/dev/null)
|
||||
GREEN := $(shell tput setaf 2 2>/dev/null)
|
||||
RESET := $(shell tput sgr0 2>/dev/null)
|
||||
BOLD := $(shell tput bold)
|
||||
GREEN := $(shell tput setaf 2)
|
||||
RESET := $(shell tput sgr0)
|
||||
|
||||
fmt: fmt/eslint fmt/prettier fmt/terraform fmt/shfmt fmt/go
|
||||
.PHONY: fmt
|
||||
@@ -784,14 +783,6 @@ test-postgres: test-postgres-docker
|
||||
-count=1
|
||||
.PHONY: test-postgres
|
||||
|
||||
test-migrations: test-postgres-docker
|
||||
echo "--- test migrations"
|
||||
set -euo pipefail
|
||||
COMMIT_FROM=$(shell git rev-parse --short HEAD)
|
||||
COMMIT_TO=$(shell git rev-parse --short main)
|
||||
echo "DROP DATABASE IF EXISTS migrate_test_$${COMMIT_FROM}; CREATE DATABASE migrate_test_$${COMMIT_FROM};" | psql 'postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable'
|
||||
go run ./scripts/migrate-test/main.go --from="$$COMMIT_FROM" --to="$$COMMIT_TO" --postgres-url="postgresql://postgres:postgres@localhost:5432/migrate_test_$${COMMIT_FROM}?sslmode=disable"
|
||||
|
||||
# NOTE: we set --memory to the same size as a GitHub runner.
|
||||
test-postgres-docker:
|
||||
docker rm -f test-postgres-docker || true
|
||||
|
||||
@@ -116,9 +116,3 @@ We are always working on new integrations. Feel free to open an issue to request
|
||||
|
||||
- [**Provision Coder with Terraform**](https://github.com/ElliotG/coder-oss-tf): Provision Coder on Google GKE, Azure AKS, AWS EKS, DigitalOcean DOKS, IBMCloud K8s, OVHCloud K8s, and Scaleway K8s Kapsule with Terraform
|
||||
- [**Coder Template GitHub Action**](https://github.com/marketplace/actions/update-coder-template): A GitHub Action that updates Coder templates
|
||||
|
||||
## Contributing
|
||||
|
||||
We are always happy to see new contributors to Coder. If you are new to the Coder codebase, we have
|
||||
[a guide on how to get started](https://coder.com/docs/v2/latest/CONTRIBUTING). We'd love to see your
|
||||
contributions!
|
||||
|
||||
+12
-12
@@ -240,11 +240,10 @@ type agent struct {
|
||||
sshServer *agentssh.Server
|
||||
sshMaxTimeout time.Duration
|
||||
|
||||
lifecycleUpdate chan struct{}
|
||||
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
|
||||
lifecycleMu sync.RWMutex // Protects following.
|
||||
lifecycleStates []agentsdk.PostLifecycleRequest
|
||||
lifecycleLastReportedIndex int // Keeps track of the last lifecycle state we successfully reported.
|
||||
lifecycleUpdate chan struct{}
|
||||
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
|
||||
lifecycleMu sync.RWMutex // Protects following.
|
||||
lifecycleStates []agentsdk.PostLifecycleRequest
|
||||
|
||||
network *tailnet.Conn
|
||||
addresses []netip.Prefix
|
||||
@@ -626,6 +625,7 @@ func (a *agent) reportMetadata(ctx context.Context, conn drpc.Conn) error {
|
||||
// changes are reported in order.
|
||||
func (a *agent) reportLifecycle(ctx context.Context, conn drpc.Conn) error {
|
||||
aAPI := proto.NewDRPCAgentClient(conn)
|
||||
lastReportedIndex := 0 // Start off with the created state without reporting it.
|
||||
for {
|
||||
select {
|
||||
case <-a.lifecycleUpdate:
|
||||
@@ -636,20 +636,20 @@ func (a *agent) reportLifecycle(ctx context.Context, conn drpc.Conn) error {
|
||||
for {
|
||||
a.lifecycleMu.RLock()
|
||||
lastIndex := len(a.lifecycleStates) - 1
|
||||
report := a.lifecycleStates[a.lifecycleLastReportedIndex]
|
||||
if len(a.lifecycleStates) > a.lifecycleLastReportedIndex+1 {
|
||||
report = a.lifecycleStates[a.lifecycleLastReportedIndex+1]
|
||||
report := a.lifecycleStates[lastReportedIndex]
|
||||
if len(a.lifecycleStates) > lastReportedIndex+1 {
|
||||
report = a.lifecycleStates[lastReportedIndex+1]
|
||||
}
|
||||
a.lifecycleMu.RUnlock()
|
||||
|
||||
if lastIndex == a.lifecycleLastReportedIndex {
|
||||
if lastIndex == lastReportedIndex {
|
||||
break
|
||||
}
|
||||
l, err := agentsdk.ProtoFromLifecycle(report)
|
||||
if err != nil {
|
||||
a.logger.Critical(ctx, "failed to convert lifecycle state", slog.F("report", report))
|
||||
// Skip this report; there is no point retrying. Maybe we can successfully convert the next one?
|
||||
a.lifecycleLastReportedIndex++
|
||||
lastReportedIndex++
|
||||
continue
|
||||
}
|
||||
payload := &proto.UpdateLifecycleRequest{Lifecycle: l}
|
||||
@@ -662,13 +662,13 @@ func (a *agent) reportLifecycle(ctx context.Context, conn drpc.Conn) error {
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "successfully reported lifecycle state")
|
||||
a.lifecycleLastReportedIndex++
|
||||
lastReportedIndex++
|
||||
select {
|
||||
case a.lifecycleReported <- report.State:
|
||||
case <-a.lifecycleReported:
|
||||
a.lifecycleReported <- report.State
|
||||
}
|
||||
if a.lifecycleLastReportedIndex < lastIndex {
|
||||
if lastReportedIndex < lastIndex {
|
||||
// Keep reporting until we've sent all messages, we can't
|
||||
// rely on the channel triggering us before the backlog is
|
||||
// consumed.
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter, defaultOverrides map[string]string) (string, error) {
|
||||
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
|
||||
label := templateVersionParameter.Name
|
||||
if templateVersionParameter.DisplayName != "" {
|
||||
label = templateVersionParameter.DisplayName
|
||||
@@ -26,11 +26,6 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
|
||||
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
|
||||
}
|
||||
|
||||
defaultValue := templateVersionParameter.DefaultValue
|
||||
if v, ok := defaultOverrides[templateVersionParameter.Name]; ok {
|
||||
defaultValue = v
|
||||
}
|
||||
|
||||
var err error
|
||||
var value string
|
||||
if templateVersionParameter.Type == "list(string)" {
|
||||
@@ -63,7 +58,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
|
||||
var richParameterOption *codersdk.TemplateVersionParameterOption
|
||||
richParameterOption, err = RichSelect(inv, RichSelectOptions{
|
||||
Options: templateVersionParameter.Options,
|
||||
Default: defaultValue,
|
||||
Default: templateVersionParameter.DefaultValue,
|
||||
HideSearch: true,
|
||||
})
|
||||
if err == nil {
|
||||
@@ -74,7 +69,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
|
||||
} else {
|
||||
text := "Enter a value"
|
||||
if !templateVersionParameter.Required {
|
||||
text += fmt.Sprintf(" (default: %q)", defaultValue)
|
||||
text += fmt.Sprintf(" (default: %q)", templateVersionParameter.DefaultValue)
|
||||
}
|
||||
text += ":"
|
||||
|
||||
@@ -92,7 +87,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
|
||||
|
||||
// If they didn't specify anything, use the default value if set.
|
||||
if len(templateVersionParameter.Options) == 0 && value == "" {
|
||||
value = defaultValue
|
||||
value = templateVersionParameter.DefaultValue
|
||||
}
|
||||
|
||||
return value, nil
|
||||
|
||||
+6
-15
@@ -165,11 +165,6 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
return xerrors.Errorf("can't parse given parameter values: %w", err)
|
||||
}
|
||||
|
||||
cliBuildParameterDefaults, err := asWorkspaceBuildParameters(parameterFlags.richParameterDefaults)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("can't parse given parameter defaults: %w", err)
|
||||
}
|
||||
|
||||
var sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
|
||||
if copyParametersFrom != "" {
|
||||
sourceWorkspaceParameters, err = client.WorkspaceBuildParameters(inv.Context(), sourceWorkspace.LatestBuild.ID)
|
||||
@@ -183,9 +178,8 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
TemplateVersionID: templateVersionID,
|
||||
NewWorkspaceName: workspaceName,
|
||||
|
||||
RichParameterFile: parameterFlags.richParameterFile,
|
||||
RichParameters: cliBuildParameters,
|
||||
RichParameterDefaults: cliBuildParameterDefaults,
|
||||
RichParameterFile: parameterFlags.richParameterFile,
|
||||
RichParameters: cliBuildParameters,
|
||||
|
||||
SourceWorkspaceParameters: sourceWorkspaceParameters,
|
||||
})
|
||||
@@ -268,7 +262,6 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
cliui.SkipPromptOption(),
|
||||
)
|
||||
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
|
||||
cmd.Options = append(cmd.Options, parameterFlags.cliParameterDefaults()...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -283,10 +276,9 @@ type prepWorkspaceBuildArgs struct {
|
||||
PromptBuildOptions bool
|
||||
BuildOptions []codersdk.WorkspaceBuildParameter
|
||||
|
||||
PromptRichParameters bool
|
||||
RichParameters []codersdk.WorkspaceBuildParameter
|
||||
RichParameterFile string
|
||||
RichParameterDefaults []codersdk.WorkspaceBuildParameter
|
||||
PromptRichParameters bool
|
||||
RichParameters []codersdk.WorkspaceBuildParameter
|
||||
RichParameterFile string
|
||||
}
|
||||
|
||||
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
|
||||
@@ -319,8 +311,7 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
|
||||
WithBuildOptions(args.BuildOptions).
|
||||
WithPromptRichParameters(args.PromptRichParameters).
|
||||
WithRichParameters(args.RichParameters).
|
||||
WithRichParametersFile(parameterFile).
|
||||
WithRichParametersDefaults(args.RichParameterDefaults)
|
||||
WithRichParametersFile(parameterFile)
|
||||
buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -315,68 +315,6 @@ func TestCreateWithRichParameters(t *testing.T) {
|
||||
<-doneChan
|
||||
})
|
||||
|
||||
t.Run("ParametersDefaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||||
|
||||
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name,
|
||||
"--parameter-default", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
|
||||
"--parameter-default", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
|
||||
"--parameter-default", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
|
||||
clitest.SetupConfig(t, member, 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{
|
||||
firstParameterDescription, firstParameterValue,
|
||||
secondParameterDescription, secondParameterValue,
|
||||
immutableParameterDescription, immutableParameterValue,
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
defaultValue := matches[i+1]
|
||||
|
||||
pty.ExpectMatch(match)
|
||||
pty.ExpectMatch(`Enter a value (default: "` + defaultValue + `")`)
|
||||
pty.WriteLine("")
|
||||
}
|
||||
pty.ExpectMatch("Confirm create?")
|
||||
pty.WriteLine("yes")
|
||||
<-doneChan
|
||||
|
||||
// Verify that the expected default values were used.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||
Name: "my-workspace",
|
||||
})
|
||||
require.NoError(t, err, "can't list available workspaces")
|
||||
require.Len(t, workspaces.Workspaces, 1)
|
||||
|
||||
workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
|
||||
require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
|
||||
|
||||
buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, buildParameters, 3)
|
||||
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstParameterValue})
|
||||
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: secondParameterName, Value: secondParameterValue})
|
||||
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: immutableParameterName, Value: immutableParameterValue})
|
||||
})
|
||||
|
||||
t.Run("RichParametersFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -244,8 +245,14 @@ func (o *scaleTestOutput) write(res harness.Results, stdout io.Writer) error {
|
||||
|
||||
// Sync the file to disk if it's a file.
|
||||
if s, ok := w.(interface{ Sync() error }); ok {
|
||||
// Best effort. If we get an error from syncing, just ignore it.
|
||||
_ = s.Sync()
|
||||
err := s.Sync()
|
||||
// On Linux, EINVAL is returned when calling fsync on /dev/stdout. We
|
||||
// can safely ignore this error.
|
||||
// On macOS, ENOTTY is returned when calling sync on /dev/stdout. We
|
||||
// can safely ignore this error.
|
||||
if err != nil && !xerrors.Is(err, syscall.EINVAL) && !xerrors.Is(err, syscall.ENOTTY) {
|
||||
return xerrors.Errorf("flush output file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if c != nil {
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
|
||||
// need to wait for the agent to start.
|
||||
workspaceQuery := inv.Args[0]
|
||||
autostart := true
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, codersdk.Me, workspaceQuery)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace and agent: %w", err)
|
||||
}
|
||||
|
||||
+2
-15
@@ -18,16 +18,14 @@ type workspaceParameterFlags struct {
|
||||
promptBuildOptions bool
|
||||
buildOptions []string
|
||||
|
||||
richParameterFile string
|
||||
richParameters []string
|
||||
richParameterDefaults []string
|
||||
richParameterFile string
|
||||
richParameters []string
|
||||
|
||||
promptRichParameters bool
|
||||
}
|
||||
|
||||
func (wpf *workspaceParameterFlags) allOptions() []serpent.Option {
|
||||
options := append(wpf.cliBuildOptions(), wpf.cliParameters()...)
|
||||
options = append(options, wpf.cliParameterDefaults()...)
|
||||
return append(options, wpf.alwaysPrompt())
|
||||
}
|
||||
|
||||
@@ -64,17 +62,6 @@ func (wpf *workspaceParameterFlags) cliParameters() []serpent.Option {
|
||||
}
|
||||
}
|
||||
|
||||
func (wpf *workspaceParameterFlags) cliParameterDefaults() []serpent.Option {
|
||||
return serpent.OptionSet{
|
||||
serpent.Option{
|
||||
Flag: "parameter-default",
|
||||
Env: "CODER_RICH_PARAMETER_DEFAULT",
|
||||
Description: `Rich parameter default values in the format "name=value".`,
|
||||
Value: serpent.StringArrayOf(&wpf.richParameterDefaults),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wpf *workspaceParameterFlags) alwaysPrompt() serpent.Option {
|
||||
return serpent.Option{
|
||||
Flag: "always-prompt",
|
||||
|
||||
@@ -26,10 +26,9 @@ type ParameterResolver struct {
|
||||
lastBuildParameters []codersdk.WorkspaceBuildParameter
|
||||
sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
|
||||
|
||||
richParameters []codersdk.WorkspaceBuildParameter
|
||||
richParametersDefaults map[string]string
|
||||
richParametersFile map[string]string
|
||||
buildOptions []codersdk.WorkspaceBuildParameter
|
||||
richParameters []codersdk.WorkspaceBuildParameter
|
||||
richParametersFile map[string]string
|
||||
buildOptions []codersdk.WorkspaceBuildParameter
|
||||
|
||||
promptRichParameters bool
|
||||
promptBuildOptions bool
|
||||
@@ -60,16 +59,6 @@ func (pr *ParameterResolver) WithRichParametersFile(fileMap map[string]string) *
|
||||
return pr
|
||||
}
|
||||
|
||||
func (pr *ParameterResolver) WithRichParametersDefaults(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
|
||||
if pr.richParametersDefaults == nil {
|
||||
pr.richParametersDefaults = make(map[string]string)
|
||||
}
|
||||
for _, p := range params {
|
||||
pr.richParametersDefaults[p.Name] = p.Value
|
||||
}
|
||||
return pr
|
||||
}
|
||||
|
||||
func (pr *ParameterResolver) WithPromptRichParameters(promptRichParameters bool) *ParameterResolver {
|
||||
pr.promptRichParameters = promptRichParameters
|
||||
return pr
|
||||
@@ -238,7 +227,7 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
|
||||
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
|
||||
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
|
||||
(tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
|
||||
parameterValue, err := cliui.RichParameter(inv, tvp, pr.richParametersDefaults)
|
||||
parameterValue, err := cliui.RichParameter(inv, tvp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ func (r *RootCmd) ping() *serpent.Command {
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(
|
||||
ctx, inv, client,
|
||||
false, // Do not autostart for a ping.
|
||||
workspaceName,
|
||||
codersdk.Me, workspaceName,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ func (r *RootCmd) portForward() *serpent.Command {
|
||||
return xerrors.New("no port-forwards requested")
|
||||
}
|
||||
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, codersdk.Me, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+65
-82
@@ -944,13 +944,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
var provisionerdWaitGroup sync.WaitGroup
|
||||
defer provisionerdWaitGroup.Wait()
|
||||
provisionerdMetrics := provisionerd.NewMetrics(options.PrometheusRegistry)
|
||||
|
||||
// Built in provisioner daemons will support the same types.
|
||||
// By default, this is the slice {"terraform"}
|
||||
provisionerTypes := make([]codersdk.ProvisionerType, 0)
|
||||
for _, pt := range vals.Provisioner.DaemonTypes {
|
||||
provisionerTypes = append(provisionerTypes, codersdk.ProvisionerType(pt))
|
||||
}
|
||||
for i := int64(0); i < vals.Provisioner.Daemons.Value(); i++ {
|
||||
suffix := fmt.Sprintf("%d", i)
|
||||
// The suffix is added to the hostname, so we may need to trim to fit into
|
||||
@@ -959,7 +952,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
name := fmt.Sprintf("%s-%s", hostname, suffix)
|
||||
daemonCacheDir := filepath.Join(cacheDir, fmt.Sprintf("provisioner-%d", i))
|
||||
daemon, err := newProvisionerDaemon(
|
||||
ctx, coderAPI, provisionerdMetrics, logger, vals, daemonCacheDir, errCh, &provisionerdWaitGroup, name, provisionerTypes,
|
||||
ctx, coderAPI, provisionerdMetrics, logger, vals, daemonCacheDir, errCh, &provisionerdWaitGroup, name,
|
||||
)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create provisioner daemon: %w", err)
|
||||
@@ -972,7 +965,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
defer shutdownConns()
|
||||
|
||||
// Ensures that old database entries are cleaned up over time!
|
||||
purger := dbpurge.New(ctx, logger.Named("dbpurge"), options.Database)
|
||||
purger := dbpurge.New(ctx, logger, options.Database)
|
||||
defer purger.Close()
|
||||
|
||||
// Updates workspace usage
|
||||
@@ -1347,7 +1340,6 @@ func newProvisionerDaemon(
|
||||
errCh chan error,
|
||||
wg *sync.WaitGroup,
|
||||
name string,
|
||||
provisionerTypes []codersdk.ProvisionerType,
|
||||
) (srv *provisionerd.Server, err error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer func() {
|
||||
@@ -1367,88 +1359,79 @@ func newProvisionerDaemon(
|
||||
return nil, xerrors.Errorf("mkdir work dir: %w", err)
|
||||
}
|
||||
|
||||
// Omit any duplicates
|
||||
provisionerTypes = slice.Unique(provisionerTypes)
|
||||
|
||||
// Populate the connector with the supported types.
|
||||
connector := provisionerd.LocalProvisioners{}
|
||||
for _, provisionerType := range provisionerTypes {
|
||||
switch provisionerType {
|
||||
case codersdk.ProvisionerTypeEcho:
|
||||
echoClient, echoServer := drpc.MemTransportPipe()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-ctx.Done()
|
||||
_ = echoClient.Close()
|
||||
_ = echoServer.Close()
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer cancel()
|
||||
if cfg.Provisioner.DaemonsEcho {
|
||||
echoClient, echoServer := drpc.MemTransportPipe()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-ctx.Done()
|
||||
_ = echoClient.Close()
|
||||
_ = echoServer.Close()
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer cancel()
|
||||
|
||||
err := echo.Serve(ctx, &provisionersdk.ServeOptions{
|
||||
Listener: echoServer,
|
||||
WorkDirectory: workDir,
|
||||
Logger: logger.Named("echo"),
|
||||
})
|
||||
if err != nil {
|
||||
select {
|
||||
case errCh <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
connector[string(database.ProvisionerTypeEcho)] = sdkproto.NewDRPCProvisionerClient(echoClient)
|
||||
case codersdk.ProvisionerTypeTerraform:
|
||||
tfDir := filepath.Join(cacheDir, "tf")
|
||||
err = os.MkdirAll(tfDir, 0o700)
|
||||
err := echo.Serve(ctx, &provisionersdk.ServeOptions{
|
||||
Listener: echoServer,
|
||||
WorkDirectory: workDir,
|
||||
Logger: logger.Named("echo"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("mkdir terraform dir: %w", err)
|
||||
}
|
||||
|
||||
tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName)
|
||||
terraformClient, terraformServer := drpc.MemTransportPipe()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-ctx.Done()
|
||||
_ = terraformClient.Close()
|
||||
_ = terraformServer.Close()
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer cancel()
|
||||
|
||||
err := terraform.Serve(ctx, &terraform.ServeOptions{
|
||||
ServeOptions: &provisionersdk.ServeOptions{
|
||||
Listener: terraformServer,
|
||||
Logger: logger.Named("terraform"),
|
||||
WorkDirectory: workDir,
|
||||
},
|
||||
CachePath: tfDir,
|
||||
Tracer: tracer,
|
||||
})
|
||||
if err != nil && !xerrors.Is(err, context.Canceled) {
|
||||
select {
|
||||
case errCh <- err:
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case errCh <- err:
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
connector[string(database.ProvisionerTypeTerraform)] = sdkproto.NewDRPCProvisionerClient(terraformClient)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown provisioner type %q", provisionerType)
|
||||
}
|
||||
}()
|
||||
connector[string(database.ProvisionerTypeEcho)] = sdkproto.NewDRPCProvisionerClient(echoClient)
|
||||
} else {
|
||||
tfDir := filepath.Join(cacheDir, "tf")
|
||||
err = os.MkdirAll(tfDir, 0o700)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("mkdir terraform dir: %w", err)
|
||||
}
|
||||
|
||||
tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName)
|
||||
terraformClient, terraformServer := drpc.MemTransportPipe()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-ctx.Done()
|
||||
_ = terraformClient.Close()
|
||||
_ = terraformServer.Close()
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer cancel()
|
||||
|
||||
err := terraform.Serve(ctx, &terraform.ServeOptions{
|
||||
ServeOptions: &provisionersdk.ServeOptions{
|
||||
Listener: terraformServer,
|
||||
Logger: logger.Named("terraform"),
|
||||
WorkDirectory: workDir,
|
||||
},
|
||||
CachePath: tfDir,
|
||||
Tracer: tracer,
|
||||
})
|
||||
if err != nil && !xerrors.Is(err, context.Canceled) {
|
||||
select {
|
||||
case errCh <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
connector[string(database.ProvisionerTypeTerraform)] = sdkproto.NewDRPCProvisionerClient(terraformClient)
|
||||
}
|
||||
|
||||
return provisionerd.New(func(dialCtx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
|
||||
// This debounces calls to listen every second. Read the comment
|
||||
// in provisionerdserver.go to learn more!
|
||||
return coderAPI.CreateInMemoryProvisionerDaemon(dialCtx, name, provisionerTypes)
|
||||
return coderAPI.CreateInMemoryProvisionerDaemon(dialCtx, name)
|
||||
}, &provisionerd.Options{
|
||||
Logger: logger.Named(fmt.Sprintf("provisionerd-%s", name)),
|
||||
UpdateInterval: time.Second,
|
||||
|
||||
+5
-10
@@ -1367,8 +1367,7 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--provisioner-daemons-echo",
|
||||
"--log-human", fiName,
|
||||
)
|
||||
clitest.Start(t, root)
|
||||
@@ -1386,8 +1385,7 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--provisioner-daemons-echo",
|
||||
"--log-human", fi,
|
||||
)
|
||||
clitest.Start(t, root)
|
||||
@@ -1405,8 +1403,7 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--provisioner-daemons-echo",
|
||||
"--log-json", fi,
|
||||
)
|
||||
clitest.Start(t, root)
|
||||
@@ -1427,8 +1424,7 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--provisioner-daemons-echo",
|
||||
"--log-stackdriver", fi,
|
||||
)
|
||||
// Attach pty so we get debug output from the command if this test
|
||||
@@ -1463,8 +1459,7 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--provisioner-daemons-echo",
|
||||
"--log-human", fi1,
|
||||
"--log-json", fi2,
|
||||
"--log-stackdriver", fi3,
|
||||
|
||||
+16
-16
@@ -39,7 +39,7 @@ func (r *RootCmd) speedtest() *serpent.Command {
|
||||
ctx, cancel := context.WithCancel(inv.Context())
|
||||
defer cancel()
|
||||
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, false, inv.Args[0])
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, false, codersdk.Me, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,22 +60,10 @@ func (r *RootCmd) speedtest() *serpent.Command {
|
||||
if r.disableDirect {
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
|
||||
}
|
||||
opts := &workspacesdk.DialAgentOptions{
|
||||
Logger: logger,
|
||||
}
|
||||
if pcapFile != "" {
|
||||
s := capture.New()
|
||||
opts.CaptureHook = s.LogPacket
|
||||
f, err := os.OpenFile(pcapFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
unregister := s.RegisterOutput(f)
|
||||
defer unregister()
|
||||
}
|
||||
conn, err := workspacesdk.New(client).
|
||||
DialAgent(ctx, workspaceAgent.ID, opts)
|
||||
DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{
|
||||
Logger: logger,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -114,6 +102,18 @@ func (r *RootCmd) speedtest() *serpent.Command {
|
||||
conn.AwaitReachable(ctx)
|
||||
}
|
||||
|
||||
if pcapFile != "" {
|
||||
s := capture.New()
|
||||
conn.InstallCaptureHook(s.LogPacket)
|
||||
f, err := os.OpenFile(pcapFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
unregister := s.RegisterOutput(f)
|
||||
defer unregister()
|
||||
}
|
||||
|
||||
var tsDir tsspeedtest.Direction
|
||||
switch direction {
|
||||
case "up":
|
||||
|
||||
+13
-36
@@ -55,7 +55,6 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
noWait bool
|
||||
logDirPath string
|
||||
remoteForwards []string
|
||||
env []string
|
||||
disableAutostart bool
|
||||
)
|
||||
client := new(codersdk.Client)
|
||||
@@ -145,26 +144,19 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
stack := newCloserStack(ctx, logger)
|
||||
defer stack.close(nil)
|
||||
|
||||
for _, remoteForward := range remoteForwards {
|
||||
isValid := validateRemoteForward(remoteForward)
|
||||
if !isValid {
|
||||
return xerrors.Errorf(`invalid format of remote-forward, expected: remote_port:local_address:local_port`)
|
||||
}
|
||||
if isValid && stdio {
|
||||
return xerrors.Errorf(`remote-forward can't be enabled in the stdio mode`)
|
||||
if len(remoteForwards) > 0 {
|
||||
for _, remoteForward := range remoteForwards {
|
||||
isValid := validateRemoteForward(remoteForward)
|
||||
if !isValid {
|
||||
return xerrors.Errorf(`invalid format of remote-forward, expected: remote_port:local_address:local_port`)
|
||||
}
|
||||
if isValid && stdio {
|
||||
return xerrors.Errorf(`remote-forward can't be enabled in the stdio mode`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var parsedEnv [][2]string
|
||||
for _, e := range env {
|
||||
k, v, ok := strings.Cut(e, "=")
|
||||
if !ok {
|
||||
return xerrors.Errorf("invalid environment variable setting %q", e)
|
||||
}
|
||||
parsedEnv = append(parsedEnv, [2]string{k, v})
|
||||
}
|
||||
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, codersdk.Me, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -383,12 +375,6 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
}()
|
||||
}
|
||||
|
||||
for _, kv := range parsedEnv {
|
||||
if err := sshSession.Setenv(kv[0], kv[1]); err != nil {
|
||||
return xerrors.Errorf("setenv: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = sshSession.RequestPty("xterm-256color", 128, 128, gossh.TerminalModes{})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("request pty: %w", err)
|
||||
@@ -497,13 +483,6 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
FlagShorthand: "R",
|
||||
Value: serpent.StringArrayOf(&remoteForwards),
|
||||
},
|
||||
{
|
||||
Flag: "env",
|
||||
Description: "Set environment variable(s) for session (key1=value1,key2=value2,...).",
|
||||
Env: "CODER_SSH_ENV",
|
||||
FlagShorthand: "e",
|
||||
Value: serpent.StringArrayOf(&env),
|
||||
},
|
||||
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)),
|
||||
}
|
||||
return cmd
|
||||
@@ -578,12 +557,10 @@ startWatchLoop:
|
||||
// getWorkspaceAgent returns the workspace and agent selected using either the
|
||||
// `<workspace>[.<agent>]` syntax via `in`.
|
||||
// If autoStart is true, the workspace will be started if it is not already running.
|
||||
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, input string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
|
||||
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, userID string, in string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
|
||||
var (
|
||||
workspace codersdk.Workspace
|
||||
// The input will be `owner/name.agent`
|
||||
// The agent is optional.
|
||||
workspaceParts = strings.Split(input, ".")
|
||||
workspace codersdk.Workspace
|
||||
workspaceParts = strings.Split(in, ".")
|
||||
err error
|
||||
)
|
||||
|
||||
|
||||
@@ -968,49 +968,6 @@ func TestSSH(t *testing.T) {
|
||||
<-cmdDone
|
||||
})
|
||||
|
||||
t.Run("Env", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Test not supported on windows")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
_ = agenttest.New(t, client.URL, agentToken)
|
||||
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
||||
|
||||
inv, root := clitest.New(t,
|
||||
"ssh",
|
||||
workspace.Name,
|
||||
"--env",
|
||||
"foo=bar,baz=qux",
|
||||
)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
inv.Stderr = pty.Output()
|
||||
|
||||
// Wait super long so this doesn't flake on -race test.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
|
||||
defer cancel()
|
||||
|
||||
w := clitest.StartWithWaiter(t, inv.WithContext(ctx))
|
||||
defer w.Wait() // We don't care about any exit error (exit code 255: SSH connection ended unexpectedly).
|
||||
|
||||
// Since something was output, it should be safe to write input.
|
||||
// This could show a prompt or "running startup scripts", so it's
|
||||
// not indicative of the SSH connection being ready.
|
||||
_ = pty.Peek(ctx, 1)
|
||||
|
||||
// Ensure the SSH connection is ready by testing the shell
|
||||
// input/output.
|
||||
pty.WriteLine("echo $foo $baz")
|
||||
pty.ExpectMatchContext(ctx, "bar qux")
|
||||
|
||||
// And we're done.
|
||||
pty.WriteLine("exit")
|
||||
})
|
||||
|
||||
t.Run("RemoteForwardUnixSocket", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Test not supported on windows")
|
||||
|
||||
+6
-12
@@ -99,12 +99,7 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
|
||||
|
||||
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
|
||||
if err != nil {
|
||||
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse rich parameters: %w", err)
|
||||
}
|
||||
|
||||
cliRichParameterDefaults, err := asWorkspaceBuildParameters(parameterFlags.richParameterDefaults)
|
||||
if err != nil {
|
||||
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse rich parameter defaults: %w", err)
|
||||
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse build options: %w", err)
|
||||
}
|
||||
|
||||
buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
||||
@@ -113,12 +108,11 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
|
||||
NewWorkspaceName: workspace.Name,
|
||||
LastBuildParameters: lastBuildParameters,
|
||||
|
||||
PromptBuildOptions: parameterFlags.promptBuildOptions,
|
||||
BuildOptions: buildOptions,
|
||||
PromptRichParameters: parameterFlags.promptRichParameters,
|
||||
RichParameters: cliRichParameters,
|
||||
RichParameterFile: parameterFlags.richParameterFile,
|
||||
RichParameterDefaults: cliRichParameterDefaults,
|
||||
PromptBuildOptions: parameterFlags.promptBuildOptions,
|
||||
BuildOptions: buildOptions,
|
||||
PromptRichParameters: parameterFlags.promptRichParameters,
|
||||
RichParameters: cliRichParameters,
|
||||
RichParameterFile: parameterFlags.richParameterFile,
|
||||
})
|
||||
if err != nil {
|
||||
return codersdk.CreateWorkspaceBuildRequest{}, err
|
||||
|
||||
+36
-58
@@ -13,7 +13,6 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -101,7 +100,7 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
||||
|
||||
// Check if we're running inside a workspace
|
||||
if val, found := os.LookupEnv("CODER"); found && val == "true" {
|
||||
cliui.Warn(inv.Stderr, "Running inside Coder workspace; this can affect results!")
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Running inside Coder workspace; this can affect results!")
|
||||
cliLog.Debug(inv.Context(), "running inside coder workspace")
|
||||
}
|
||||
|
||||
@@ -115,41 +114,32 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
||||
client.URL = u
|
||||
}
|
||||
|
||||
var (
|
||||
wsID uuid.UUID
|
||||
agtID uuid.UUID
|
||||
if len(inv.Args) == 0 {
|
||||
return xerrors.Errorf("must specify workspace name")
|
||||
}
|
||||
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
|
||||
if err != nil {
|
||||
return xerrors.Errorf("invalid workspace: %w", err)
|
||||
}
|
||||
cliLog.Debug(inv.Context(), "found workspace",
|
||||
slog.F("workspace_name", ws.Name),
|
||||
slog.F("workspace_id", ws.ID),
|
||||
)
|
||||
|
||||
if len(inv.Args) == 0 {
|
||||
cliLog.Warn(inv.Context(), "no workspace specified")
|
||||
cliui.Warn(inv.Stderr, "No workspace specified. This will result in incomplete information.")
|
||||
} else {
|
||||
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
|
||||
if err != nil {
|
||||
return xerrors.Errorf("invalid workspace: %w", err)
|
||||
}
|
||||
cliLog.Debug(inv.Context(), "found workspace",
|
||||
slog.F("workspace_name", ws.Name),
|
||||
slog.F("workspace_id", ws.ID),
|
||||
)
|
||||
wsID = ws.ID
|
||||
agentName := ""
|
||||
if len(inv.Args) > 1 {
|
||||
agentName = inv.Args[1]
|
||||
}
|
||||
|
||||
agt, found := findAgent(agentName, ws.LatestBuild.Resources)
|
||||
if !found {
|
||||
cliLog.Warn(inv.Context(), "could not find agent in workspace", slog.F("agent_name", agentName))
|
||||
} else {
|
||||
cliLog.Debug(inv.Context(), "found workspace agent",
|
||||
slog.F("agent_name", agt.Name),
|
||||
slog.F("agent_id", agt.ID),
|
||||
)
|
||||
agtID = agt.ID
|
||||
}
|
||||
agentName := ""
|
||||
if len(inv.Args) > 1 {
|
||||
agentName = inv.Args[1]
|
||||
}
|
||||
|
||||
agt, found := findAgent(agentName, ws.LatestBuild.Resources)
|
||||
if !found {
|
||||
return xerrors.Errorf("could not find agent named %q for workspace", agentName)
|
||||
}
|
||||
cliLog.Debug(inv.Context(), "found workspace agent",
|
||||
slog.F("agent_name", agt.Name),
|
||||
slog.F("agent_id", agt.ID),
|
||||
)
|
||||
|
||||
if outputPath == "" {
|
||||
cwd, err := filepath.Abs(".")
|
||||
if err != nil {
|
||||
@@ -175,8 +165,8 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
||||
Client: client,
|
||||
// Support adds a sink so we don't need to supply one ourselves.
|
||||
Log: clientLog,
|
||||
WorkspaceID: wsID,
|
||||
AgentID: agtID,
|
||||
WorkspaceID: ws.ID,
|
||||
AgentID: agt.ID,
|
||||
}
|
||||
|
||||
bun, err := support.Run(inv.Context(), &deps)
|
||||
@@ -184,16 +174,6 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
||||
_ = os.Remove(outputPath) // best effort
|
||||
return xerrors.Errorf("create support bundle: %w", err)
|
||||
}
|
||||
docsURL := bun.Deployment.Config.Values.DocsURL.String()
|
||||
deployHealthSummary := bun.Deployment.HealthReport.Summarize(docsURL)
|
||||
if len(deployHealthSummary) > 0 {
|
||||
cliui.Warn(inv.Stdout, "Deployment health issues detected:", deployHealthSummary...)
|
||||
}
|
||||
clientNetcheckSummary := bun.Network.Netcheck.Summarize("Client netcheck:", docsURL)
|
||||
if len(clientNetcheckSummary) > 0 {
|
||||
cliui.Warn(inv.Stdout, "Networking issues detected:", deployHealthSummary...)
|
||||
}
|
||||
|
||||
bun.CLILogs = cliLogBuf.Bytes()
|
||||
|
||||
if err := writeBundle(bun, zwr); err != nil {
|
||||
@@ -201,7 +181,6 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
||||
return xerrors.Errorf("write support bundle to %s: %w", outputPath, err)
|
||||
}
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Wrote support bundle to "+outputPath)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -243,21 +222,20 @@ func findAgent(agentName string, haystack []codersdk.WorkspaceResource) (*coders
|
||||
func writeBundle(src *support.Bundle, dest *zip.Writer) error {
|
||||
// We JSON-encode the following:
|
||||
for k, v := range map[string]any{
|
||||
"deployment/buildinfo.json": src.Deployment.BuildInfo,
|
||||
"deployment/config.json": src.Deployment.Config,
|
||||
"deployment/experiments.json": src.Deployment.Experiments,
|
||||
"deployment/health.json": src.Deployment.HealthReport,
|
||||
"network/netcheck.json": src.Network.Netcheck,
|
||||
"workspace/workspace.json": src.Workspace.Workspace,
|
||||
"agent/agent.json": src.Agent.Agent,
|
||||
"agent/listening_ports.json": src.Agent.ListeningPorts,
|
||||
"agent/manifest.json": src.Agent.Manifest,
|
||||
"agent/peer_diagnostics.json": src.Agent.PeerDiagnostics,
|
||||
"agent/ping_result.json": src.Agent.PingResult,
|
||||
"deployment/buildinfo.json": src.Deployment.BuildInfo,
|
||||
"deployment/config.json": src.Deployment.Config,
|
||||
"deployment/experiments.json": src.Deployment.Experiments,
|
||||
"deployment/health.json": src.Deployment.HealthReport,
|
||||
"network/connection_info.json": src.Network.ConnectionInfo,
|
||||
"network/netcheck.json": src.Network.Netcheck,
|
||||
"workspace/template.json": src.Workspace.Template,
|
||||
"workspace/template_version.json": src.Workspace.TemplateVersion,
|
||||
"workspace/parameters.json": src.Workspace.Parameters,
|
||||
"workspace/workspace.json": src.Workspace.Workspace,
|
||||
} {
|
||||
f, err := dest.Create(k)
|
||||
if err != nil {
|
||||
@@ -277,17 +255,17 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error {
|
||||
|
||||
// The below we just write as we have them:
|
||||
for k, v := range map[string]string{
|
||||
"network/coordinator_debug.html": src.Network.CoordinatorDebug,
|
||||
"network/tailnet_debug.html": src.Network.TailnetDebug,
|
||||
"workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs),
|
||||
"agent/logs.txt": string(src.Agent.Logs),
|
||||
"agent/agent_magicsock.html": string(src.Agent.AgentMagicsockHTML),
|
||||
"agent/client_magicsock.html": string(src.Agent.ClientMagicsockHTML),
|
||||
"agent/startup_logs.txt": humanizeAgentLogs(src.Agent.StartupLogs),
|
||||
"agent/prometheus.txt": string(src.Agent.Prometheus),
|
||||
"cli_logs.txt": string(src.CLILogs),
|
||||
"logs.txt": strings.Join(src.Logs, "\n"),
|
||||
"network/coordinator_debug.html": src.Network.CoordinatorDebug,
|
||||
"network/tailnet_debug.html": src.Network.TailnetDebug,
|
||||
"workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs),
|
||||
"workspace/template_file.zip": string(templateVersionBytes),
|
||||
"logs.txt": strings.Join(src.Logs, "\n"),
|
||||
"cli_logs.txt": string(src.CLILogs),
|
||||
} {
|
||||
f, err := dest.Create(k)
|
||||
if err != nil {
|
||||
|
||||
+25
-112
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/codersdk/healthsdk"
|
||||
@@ -96,50 +95,33 @@ func TestSupportBundle(t *testing.T) {
|
||||
clitest.SetupConfig(t, client, root)
|
||||
err = inv.Run()
|
||||
require.NoError(t, err)
|
||||
assertBundleContents(t, path, true, true, []string{secretValue})
|
||||
assertBundleContents(t, path, secretValue)
|
||||
})
|
||||
|
||||
t.Run("NoWorkspace", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var dc codersdk.DeploymentConfig
|
||||
secretValue := uuid.NewString()
|
||||
seedSecretDeploymentOptions(t, &dc, secretValue)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
DeploymentValues: dc.Values,
|
||||
})
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
d := t.TempDir()
|
||||
path := filepath.Join(d, "bundle.zip")
|
||||
inv, root := clitest.New(t, "support", "bundle", "--output-file", path, "--yes")
|
||||
inv, root := clitest.New(t, "support", "bundle", "--yes")
|
||||
//nolint: gocritic // requires owner privilege
|
||||
clitest.SetupConfig(t, client, root)
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
assertBundleContents(t, path, false, false, []string{secretValue})
|
||||
require.ErrorContains(t, err, "must specify workspace name")
|
||||
})
|
||||
|
||||
t.Run("NoAgent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var dc codersdk.DeploymentConfig
|
||||
secretValue := uuid.NewString()
|
||||
seedSecretDeploymentOptions(t, &dc, secretValue)
|
||||
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
DeploymentValues: dc.Values,
|
||||
})
|
||||
client, db := coderdtest.NewWithDatabase(t, nil)
|
||||
admin := coderdtest.CreateFirstUser(t, client)
|
||||
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
|
||||
OrganizationID: admin.OrganizationID,
|
||||
OwnerID: admin.UserID,
|
||||
}).Do() // without agent!
|
||||
d := t.TempDir()
|
||||
path := filepath.Join(d, "bundle.zip")
|
||||
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--output-file", path, "--yes")
|
||||
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--yes")
|
||||
//nolint: gocritic // requires owner privilege
|
||||
clitest.SetupConfig(t, client, root)
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
assertBundleContents(t, path, true, false, []string{secretValue})
|
||||
require.ErrorContains(t, err, "could not find agent")
|
||||
})
|
||||
|
||||
t.Run("NoPrivilege", func(t *testing.T) {
|
||||
@@ -158,8 +140,7 @@ func TestSupportBundle(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// nolint:revive // It's a control flag, but this is just a test.
|
||||
func assertBundleContents(t *testing.T, path string, wantWorkspace bool, wantAgent bool, badValues []string) {
|
||||
func assertBundleContents(t *testing.T, path string, badValues ...string) {
|
||||
t.Helper()
|
||||
r, err := zip.OpenReader(path)
|
||||
require.NoError(t, err, "open zip file")
|
||||
@@ -183,10 +164,6 @@ func assertBundleContents(t *testing.T, path string, wantWorkspace bool, wantAge
|
||||
var v healthsdk.HealthcheckReport
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "health report should not be empty")
|
||||
case "network/connection_info.json":
|
||||
var v workspacesdk.AgentConnectionInfo
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "agent connection info should not be empty")
|
||||
case "network/coordinator_debug.html":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "coordinator debug should not be empty")
|
||||
@@ -194,130 +171,66 @@ func assertBundleContents(t *testing.T, path string, wantWorkspace bool, wantAge
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "tailnet debug should not be empty")
|
||||
case "network/netcheck.json":
|
||||
var v derphealth.Report
|
||||
var v workspacesdk.AgentConnectionInfo
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "netcheck should not be empty")
|
||||
require.NotEmpty(t, v, "connection info should not be empty")
|
||||
case "workspace/workspace.json":
|
||||
var v codersdk.Workspace
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantWorkspace {
|
||||
require.Empty(t, v, "expected workspace to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, v, "workspace should not be empty")
|
||||
case "workspace/build_logs.txt":
|
||||
bs := readBytesFromZip(t, f)
|
||||
if !wantWorkspace || !wantAgent {
|
||||
require.Empty(t, bs, "expected workspace build logs to be empty")
|
||||
continue
|
||||
}
|
||||
require.Contains(t, string(bs), "provision done")
|
||||
case "workspace/template.json":
|
||||
var v codersdk.Template
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantWorkspace {
|
||||
require.Empty(t, v, "expected workspace template to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, v, "workspace template should not be empty")
|
||||
case "workspace/template_version.json":
|
||||
var v codersdk.TemplateVersion
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantWorkspace {
|
||||
require.Empty(t, v, "expected workspace template version to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, v, "workspace template version should not be empty")
|
||||
case "workspace/parameters.json":
|
||||
var v []codersdk.WorkspaceBuildParameter
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantWorkspace {
|
||||
require.Empty(t, v, "expected workspace parameters to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotNil(t, v, "workspace parameters should not be nil")
|
||||
case "workspace/template_file.zip":
|
||||
bs := readBytesFromZip(t, f)
|
||||
if !wantWorkspace {
|
||||
require.Empty(t, bs, "expected template file to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotNil(t, bs, "template file should not be nil")
|
||||
case "agent/agent.json":
|
||||
var v codersdk.WorkspaceAgent
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantAgent {
|
||||
require.Empty(t, v, "expected agent to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, v, "agent should not be empty")
|
||||
case "agent/listening_ports.json":
|
||||
var v codersdk.WorkspaceAgentListeningPortsResponse
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantAgent {
|
||||
require.Empty(t, v, "expected agent listening ports to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, v, "agent listening ports should not be empty")
|
||||
case "agent/logs.txt":
|
||||
bs := readBytesFromZip(t, f)
|
||||
if !wantAgent {
|
||||
require.Empty(t, bs, "expected agent logs to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, bs, "logs should not be empty")
|
||||
case "agent/agent_magicsock.html":
|
||||
bs := readBytesFromZip(t, f)
|
||||
if !wantAgent {
|
||||
require.Empty(t, bs, "expected agent magicsock to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, bs, "agent magicsock should not be empty")
|
||||
case "agent/client_magicsock.html":
|
||||
bs := readBytesFromZip(t, f)
|
||||
if !wantAgent {
|
||||
require.Empty(t, bs, "expected client magicsock to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, bs, "client magicsock should not be empty")
|
||||
case "agent/manifest.json":
|
||||
var v agentsdk.Manifest
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantAgent {
|
||||
require.Empty(t, v, "expected agent manifest to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, v, "agent manifest should not be empty")
|
||||
case "agent/peer_diagnostics.json":
|
||||
var v *tailnet.PeerDiagnostics
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantAgent {
|
||||
require.Empty(t, v, "expected peer diagnostics to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, v, "peer diagnostics should not be empty")
|
||||
case "agent/ping_result.json":
|
||||
var v *ipnstate.PingResult
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
if !wantAgent {
|
||||
require.Empty(t, v, "expected ping result to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, v, "ping result should not be empty")
|
||||
case "agent/prometheus.txt":
|
||||
bs := readBytesFromZip(t, f)
|
||||
if !wantAgent {
|
||||
require.Empty(t, bs, "expected agent prometheus metrics to be empty")
|
||||
continue
|
||||
}
|
||||
require.NotEmpty(t, bs, "agent prometheus metrics should not be empty")
|
||||
case "agent/startup_logs.txt":
|
||||
bs := readBytesFromZip(t, f)
|
||||
if !wantAgent {
|
||||
require.Empty(t, bs, "expected agent startup logs to be empty")
|
||||
continue
|
||||
}
|
||||
require.Contains(t, string(bs), "started up")
|
||||
case "workspace/template.json":
|
||||
var v codersdk.Template
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "workspace template should not be empty")
|
||||
case "workspace/template_version.json":
|
||||
var v codersdk.TemplateVersion
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "workspace template version should not be empty")
|
||||
case "workspace/parameters.json":
|
||||
var v []codersdk.WorkspaceBuildParameter
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotNil(t, v, "workspace parameters should not be nil")
|
||||
case "workspace/template_file.zip":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotNil(t, bs, "template file should not be nil")
|
||||
case "logs.txt":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "logs should not be empty")
|
||||
|
||||
-3
@@ -20,9 +20,6 @@ OPTIONS:
|
||||
--parameter string-array, $CODER_RICH_PARAMETER
|
||||
Rich parameter value in the format "name=value".
|
||||
|
||||
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
|
||||
Rich parameter default values 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.
|
||||
|
||||
-3
@@ -19,9 +19,6 @@ OPTIONS:
|
||||
--parameter string-array, $CODER_RICH_PARAMETER
|
||||
Rich parameter value in the format "name=value".
|
||||
|
||||
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
|
||||
Rich parameter default values 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.
|
||||
|
||||
-4
@@ -60,10 +60,6 @@ OPTIONS:
|
||||
--support-links struct[[]codersdk.LinkConfig], $CODER_SUPPORT_LINKS
|
||||
Support links to display in the top right drop down menu.
|
||||
|
||||
--terms-of-service-url string, $CODER_TERMS_OF_SERVICE_URL
|
||||
A URL to an external Terms of Service that must be accepted by users
|
||||
when logging in.
|
||||
|
||||
--update-check bool, $CODER_UPDATE_CHECK (default: false)
|
||||
Periodically check for new releases of Coder and inform the owner. The
|
||||
check is performed once per day.
|
||||
|
||||
Vendored
-3
@@ -9,9 +9,6 @@ OPTIONS:
|
||||
--disable-autostart bool, $CODER_SSH_DISABLE_AUTOSTART (default: false)
|
||||
Disable starting the workspace automatically when connecting via SSH.
|
||||
|
||||
-e, --env string-array, $CODER_SSH_ENV
|
||||
Set environment variable(s) for session (key1=value1,key2=value2,...).
|
||||
|
||||
-A, --forward-agent bool, $CODER_SSH_FORWARD_AGENT
|
||||
Specifies whether to forward the SSH agent specified in
|
||||
$SSH_AUTH_SOCK.
|
||||
|
||||
-3
@@ -19,9 +19,6 @@ OPTIONS:
|
||||
--parameter string-array, $CODER_RICH_PARAMETER
|
||||
Rich parameter value in the format "name=value".
|
||||
|
||||
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
|
||||
Rich parameter default values 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.
|
||||
|
||||
-3
@@ -21,9 +21,6 @@ OPTIONS:
|
||||
--parameter string-array, $CODER_RICH_PARAMETER
|
||||
Rich parameter value in the format "name=value".
|
||||
|
||||
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
|
||||
Rich parameter default values 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.
|
||||
|
||||
+4
-9
@@ -379,11 +379,10 @@ provisioning:
|
||||
# state for a long time, consider increasing this.
|
||||
# (default: 3, type: int)
|
||||
daemons: 3
|
||||
# The supported job types for the built-in provisioners. By default, this is only
|
||||
# the terraform type. Supported types: terraform,echo.
|
||||
# (default: terraform, type: string-array)
|
||||
daemonTypes:
|
||||
- terraform
|
||||
# Whether to use echo provisioner daemons instead of Terraform. This is for E2E
|
||||
# tests.
|
||||
# (default: false, type: bool)
|
||||
daemonsEcho: false
|
||||
# Deprecated and ignored.
|
||||
# (default: 1s, type: duration)
|
||||
daemonPollInterval: 1s
|
||||
@@ -415,10 +414,6 @@ inMemoryDatabase: false
|
||||
# Type of auth to use when connecting to postgres.
|
||||
# (default: password, type: enum[password\|awsiamrds])
|
||||
pgAuth: password
|
||||
# A URL to an external Terms of Service that must be accepted by users when
|
||||
# logging in.
|
||||
# (default: <unset>, type: string)
|
||||
termsOfServiceURL: ""
|
||||
# The algorithm to use for generating ssh keys. Accepted values are "ed25519",
|
||||
# "ecdsa", or "rsa4096".
|
||||
# (default: ed25519, type: string)
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ func (r *RootCmd) vscodeSSH() *serpent.Command {
|
||||
// will call this command after the workspace is started.
|
||||
autostart := false
|
||||
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, fmt.Sprintf("%s/%s", owner, name))
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, owner, name)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("find workspace and agent: %w", err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/autobuild"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||
@@ -83,7 +84,7 @@ func (a *StatsAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsR
|
||||
slog.Error(err),
|
||||
)
|
||||
} else {
|
||||
next, allowed := schedule.NextAutostart(now, workspace.AutostartSchedule.String, templateSchedule)
|
||||
next, allowed := autobuild.NextAutostartSchedule(now, workspace.AutostartSchedule.String, templateSchedule)
|
||||
if allowed {
|
||||
nextAutostart = next
|
||||
}
|
||||
|
||||
Generated
+17
-59
@@ -5904,7 +5904,6 @@ const docTemplate = `{
|
||||
],
|
||||
"summary": "Submit workspace agent stats",
|
||||
"operationId": "submit-workspace-agent-stats",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Stats request",
|
||||
@@ -8446,9 +8445,6 @@ const docTemplate = `{
|
||||
},
|
||||
"password": {
|
||||
"$ref": "#/definitions/codersdk.AuthMethod"
|
||||
},
|
||||
"terms_of_service_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8541,10 +8537,6 @@ const docTemplate = `{
|
||||
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
|
||||
"type": "string"
|
||||
},
|
||||
"deployment_id": {
|
||||
"description": "DeploymentID is the unique identifier for this deployment.",
|
||||
"type": "string"
|
||||
},
|
||||
"external_url": {
|
||||
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
|
||||
"type": "string"
|
||||
@@ -9303,6 +9295,9 @@ const docTemplate = `{
|
||||
"disable_path_apps": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_session_expiry_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"docs_url": {
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
},
|
||||
@@ -9340,6 +9335,12 @@ const docTemplate = `{
|
||||
"logging": {
|
||||
"$ref": "#/definitions/codersdk.LoggingConfig"
|
||||
},
|
||||
"max_session_expiry": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_token_lifetime": {
|
||||
"type": "integer"
|
||||
},
|
||||
"metrics_cache_refresh_interval": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -9391,9 +9392,6 @@ const docTemplate = `{
|
||||
"secure_auth_cookie": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"session_lifetime": {
|
||||
"$ref": "#/definitions/codersdk.SessionLifetime"
|
||||
},
|
||||
"ssh_keygen_algorithm": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9415,9 +9413,6 @@ const docTemplate = `{
|
||||
"telemetry": {
|
||||
"$ref": "#/definitions/codersdk.TelemetryConfig"
|
||||
},
|
||||
"terms_of_service_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"tls": {
|
||||
"$ref": "#/definitions/codersdk.TLSConfig"
|
||||
},
|
||||
@@ -9517,6 +9512,7 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"example",
|
||||
"shared-ports",
|
||||
"auto-fill-parameters"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
@@ -9525,6 +9521,7 @@ const docTemplate = `{
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"ExperimentExample",
|
||||
"ExperimentSharedPorts",
|
||||
"ExperimentAutoFillParameters"
|
||||
]
|
||||
},
|
||||
@@ -10484,16 +10481,12 @@ const docTemplate = `{
|
||||
"daemon_psk": {
|
||||
"type": "string"
|
||||
},
|
||||
"daemon_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"daemons": {
|
||||
"description": "Daemons is the number of built-in terraform provisioners.",
|
||||
"type": "integer"
|
||||
},
|
||||
"daemons_echo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"force_cancel_interval": {
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -11091,22 +11084,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.SessionLifetime": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default_duration": {
|
||||
"description": "DefaultDuration is for api keys, not tokens.",
|
||||
"type": "integer"
|
||||
},
|
||||
"disable_expiry_refresh": {
|
||||
"description": "DisableExpiryRefresh will disable automatically refreshing api\nkeys when they are used from the api. This means the api key lifetime at\ncreation is the lifetime of the api key.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_token_lifetime": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.SupportConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13846,16 +13823,7 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"severity": {
|
||||
"enum": [
|
||||
"ok",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
@@ -13938,7 +13906,7 @@ const docTemplate = `{
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/health.Message"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13953,20 +13921,10 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"healthy": {
|
||||
"description": "Healthy is deprecated and left for backward compatibility purposes, use ` + "`" + `Severity` + "`" + ` instead.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"severity": {
|
||||
"enum": [
|
||||
"ok",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
|
||||
Generated
+21
-53
@@ -5201,7 +5201,6 @@
|
||||
"tags": ["Agents"],
|
||||
"summary": "Submit workspace agent stats",
|
||||
"operationId": "submit-workspace-agent-stats",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Stats request",
|
||||
@@ -7515,9 +7514,6 @@
|
||||
},
|
||||
"password": {
|
||||
"$ref": "#/definitions/codersdk.AuthMethod"
|
||||
},
|
||||
"terms_of_service_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7599,10 +7595,6 @@
|
||||
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
|
||||
"type": "string"
|
||||
},
|
||||
"deployment_id": {
|
||||
"description": "DeploymentID is the unique identifier for this deployment.",
|
||||
"type": "string"
|
||||
},
|
||||
"external_url": {
|
||||
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
|
||||
"type": "string"
|
||||
@@ -8308,6 +8300,9 @@
|
||||
"disable_path_apps": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_session_expiry_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"docs_url": {
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
},
|
||||
@@ -8345,6 +8340,12 @@
|
||||
"logging": {
|
||||
"$ref": "#/definitions/codersdk.LoggingConfig"
|
||||
},
|
||||
"max_session_expiry": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_token_lifetime": {
|
||||
"type": "integer"
|
||||
},
|
||||
"metrics_cache_refresh_interval": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -8396,9 +8397,6 @@
|
||||
"secure_auth_cookie": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"session_lifetime": {
|
||||
"$ref": "#/definitions/codersdk.SessionLifetime"
|
||||
},
|
||||
"ssh_keygen_algorithm": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8420,9 +8418,6 @@
|
||||
"telemetry": {
|
||||
"$ref": "#/definitions/codersdk.TelemetryConfig"
|
||||
},
|
||||
"terms_of_service_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"tls": {
|
||||
"$ref": "#/definitions/codersdk.TLSConfig"
|
||||
},
|
||||
@@ -8516,12 +8511,16 @@
|
||||
},
|
||||
"codersdk.Experiment": {
|
||||
"type": "string",
|
||||
"enum": ["example", "auto-fill-parameters"],
|
||||
"enum": ["example", "shared-ports", "auto-fill-parameters"],
|
||||
"x-enum-comments": {
|
||||
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
||||
"ExperimentExample": "This isn't used for anything."
|
||||
},
|
||||
"x-enum-varnames": ["ExperimentExample", "ExperimentAutoFillParameters"]
|
||||
"x-enum-varnames": [
|
||||
"ExperimentExample",
|
||||
"ExperimentSharedPorts",
|
||||
"ExperimentAutoFillParameters"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
"type": "object",
|
||||
@@ -9418,16 +9417,12 @@
|
||||
"daemon_psk": {
|
||||
"type": "string"
|
||||
},
|
||||
"daemon_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"daemons": {
|
||||
"description": "Daemons is the number of built-in terraform provisioners.",
|
||||
"type": "integer"
|
||||
},
|
||||
"daemons_echo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"force_cancel_interval": {
|
||||
"type": "integer"
|
||||
}
|
||||
@@ -9991,22 +9986,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.SessionLifetime": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default_duration": {
|
||||
"description": "DefaultDuration is for api keys, not tokens.",
|
||||
"type": "integer"
|
||||
},
|
||||
"disable_expiry_refresh": {
|
||||
"description": "DisableExpiryRefresh will disable automatically refreshing api\nkeys when they are used from the api. This means the api key lifetime at\ncreation is the lifetime of the api key.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_token_lifetime": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.SupportConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12584,12 +12563,7 @@
|
||||
}
|
||||
},
|
||||
"severity": {
|
||||
"enum": ["ok", "warning", "error"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
@@ -12668,7 +12642,7 @@
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/health.Message"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12683,16 +12657,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"healthy": {
|
||||
"description": "Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"severity": {
|
||||
"enum": ["ok", "warning", "error"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
|
||||
+5
-5
@@ -84,7 +84,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
|
||||
cookie, key, err := api.createAPIKey(ctx, apikey.CreateParams{
|
||||
UserID: user.ID,
|
||||
LoginType: database.LoginTypeToken,
|
||||
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
|
||||
DefaultLifetime: api.DeploymentValues.SessionDuration.Value(),
|
||||
ExpiresAt: dbtime.Now().Add(lifeTime),
|
||||
Scope: scope,
|
||||
LifetimeSeconds: int64(lifeTime.Seconds()),
|
||||
@@ -128,7 +128,7 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
||||
lifeTime := time.Hour * 24 * 7
|
||||
cookie, _, err := api.createAPIKey(ctx, apikey.CreateParams{
|
||||
UserID: user.ID,
|
||||
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
|
||||
DefaultLifetime: api.DeploymentValues.SessionDuration.Value(),
|
||||
LoginType: database.LoginTypePassword,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
// All api generated keys will last 1 week. Browser login tokens have
|
||||
@@ -354,7 +354,7 @@ func (api *API) tokenConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(
|
||||
r.Context(), rw, http.StatusOK,
|
||||
codersdk.TokenConfig{
|
||||
MaxTokenLifetime: values.Sessions.MaximumTokenDuration.Value(),
|
||||
MaxTokenLifetime: values.MaxTokenLifetime.Value(),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -364,10 +364,10 @@ func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
|
||||
return xerrors.New("lifetime must be positive number greater than 0")
|
||||
}
|
||||
|
||||
if lifetime > api.DeploymentValues.Sessions.MaximumTokenDuration.Value() {
|
||||
if lifetime > api.DeploymentValues.MaxTokenLifetime.Value() {
|
||||
return xerrors.Errorf(
|
||||
"lifetime must be less than %v",
|
||||
api.DeploymentValues.Sessions.MaximumTokenDuration,
|
||||
api.DeploymentValues.MaxTokenLifetime,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestTokenUserSetMaxLifetime(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
dc := coderdtest.DeploymentValues(t)
|
||||
dc.Sessions.MaximumTokenDuration = serpent.Duration(time.Hour * 24 * 7)
|
||||
dc.MaxTokenLifetime = serpent.Duration(time.Hour * 24 * 7)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
DeploymentValues: dc,
|
||||
})
|
||||
@@ -165,7 +165,7 @@ func TestSessionExpiry(t *testing.T) {
|
||||
//
|
||||
// We don't support updating the deployment config after startup, but for
|
||||
// this test it works because we don't copy the value (and we use pointers).
|
||||
dc.Sessions.DefaultDuration = serpent.Duration(time.Second)
|
||||
dc.SessionDuration = serpent.Duration(time.Second)
|
||||
|
||||
userClient, _ := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID)
|
||||
|
||||
@@ -174,8 +174,8 @@ func TestSessionExpiry(t *testing.T) {
|
||||
apiKey, err := db.GetAPIKeyByID(ctx, strings.Split(token, "-")[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EqualValues(t, dc.Sessions.DefaultDuration.Value().Seconds(), apiKey.LifetimeSeconds)
|
||||
require.WithinDuration(t, apiKey.CreatedAt.Add(dc.Sessions.DefaultDuration.Value()), apiKey.ExpiresAt, 2*time.Second)
|
||||
require.EqualValues(t, dc.SessionDuration.Value().Seconds(), apiKey.LifetimeSeconds)
|
||||
require.WithinDuration(t, apiKey.CreatedAt.Add(dc.SessionDuration.Value()), apiKey.ExpiresAt, 2*time.Second)
|
||||
|
||||
// Update the session token to be expired so we can test that it is
|
||||
// rejected for extra points.
|
||||
|
||||
@@ -21,7 +21,7 @@ type AdditionalFields struct {
|
||||
BuildNumber string `json:"build_number"`
|
||||
BuildReason database.BuildReason `json:"build_reason"`
|
||||
WorkspaceOwner string `json:"workspace_owner"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id"`
|
||||
WorkspaceID uuid.UUID `json:"workpace_id"`
|
||||
}
|
||||
|
||||
func NewNop() Auditor {
|
||||
|
||||
+2
-2
@@ -190,9 +190,9 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
|
||||
// Only support referencing some resources by ID.
|
||||
switch v.Object.ResourceType.String() {
|
||||
case rbac.ResourceWorkspaceExecution.Type:
|
||||
workSpace, err := api.Database.GetWorkspaceByID(ctx, id)
|
||||
wrkSpace, err := api.Database.GetWorkspaceByID(ctx, id)
|
||||
if err == nil {
|
||||
dbObj = workSpace.ExecutionRBAC()
|
||||
dbObj = wrkSpace.ExecutionRBAC()
|
||||
}
|
||||
dbErr = err
|
||||
case rbac.ResourceWorkspace.Type:
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
|
||||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
"github.com/coder/coder/v2/coderd/wsbuilder"
|
||||
)
|
||||
|
||||
@@ -367,7 +368,7 @@ func isEligibleForAutostart(user database.User, ws database.Workspace, build dat
|
||||
return false
|
||||
}
|
||||
|
||||
nextTransition, allowed := schedule.NextAutostart(build.CreatedAt, ws.AutostartSchedule.String, templateSchedule)
|
||||
nextTransition, allowed := NextAutostartSchedule(build.CreatedAt, ws.AutostartSchedule.String, templateSchedule)
|
||||
if !allowed {
|
||||
return false
|
||||
}
|
||||
@@ -376,6 +377,29 @@ func isEligibleForAutostart(user database.User, ws database.Workspace, build dat
|
||||
return !currentTick.Before(nextTransition)
|
||||
}
|
||||
|
||||
// NextAutostartSchedule takes the workspace and template schedule and returns the next autostart schedule
|
||||
// after "at". The boolean returned is if the autostart should be allowed to start based on the template
|
||||
// schedule.
|
||||
func NextAutostartSchedule(at time.Time, wsSchedule string, templateSchedule schedule.TemplateScheduleOptions) (time.Time, bool) {
|
||||
sched, err := cron.Weekly(wsSchedule)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// Round down to the nearest minute, as this is the finest granularity cron supports.
|
||||
// Truncate is probably not necessary here, but doing it anyway to be sure.
|
||||
nextTransition := sched.Next(at).Truncate(time.Minute)
|
||||
|
||||
// The nextTransition is when the auto start should kick off. If it lands on a
|
||||
// forbidden day, do not allow the auto start. We use the time location of the
|
||||
// schedule to determine the weekday. So if "Saturday" is disallowed, the
|
||||
// definition of "Saturday" depends on the location of the schedule.
|
||||
zonedTransition := nextTransition.In(sched.Location())
|
||||
allowed := templateSchedule.AutostartRequirement.DaysMap()[zonedTransition.Weekday()]
|
||||
|
||||
return zonedTransition, allowed
|
||||
}
|
||||
|
||||
// isEligibleForAutostart returns true if the workspace should be autostopped.
|
||||
func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, currentTick time.Time) bool {
|
||||
if job.JobStatus == database.ProvisionerJobStatusFailed {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -15,11 +14,6 @@ import (
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
if runtime.GOOS == "darwin" {
|
||||
// This test fails on MacOS for some reason. See https://github.com/coder/coder/issues/12978
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
mustTime := func(layout string, value string) time.Time {
|
||||
ti, err := time.Parse(layout, value)
|
||||
require.NoError(t, err)
|
||||
@@ -64,7 +58,7 @@ func TestValidate(t *testing.T) {
|
||||
|
||||
func TestExpiresSoon(t *testing.T) {
|
||||
t.Parallel()
|
||||
const threshold = 1
|
||||
const threshold = 2
|
||||
|
||||
for _, c := range azureidentity.Certificates {
|
||||
block, rest := pem.Decode([]byte(c))
|
||||
|
||||
+15
-25
@@ -436,15 +436,6 @@ func New(options *Options) *API {
|
||||
|
||||
api.AppearanceFetcher.Store(&appearance.DefaultFetcher)
|
||||
api.PortSharer.Store(&portsharing.DefaultPortSharer)
|
||||
buildInfo := codersdk.BuildInfoResponse{
|
||||
ExternalURL: buildinfo.ExternalURL(),
|
||||
Version: buildinfo.Version(),
|
||||
AgentAPIVersion: AgentAPIVersionREST,
|
||||
DashboardURL: api.AccessURL.String(),
|
||||
WorkspaceProxy: false,
|
||||
UpgradeMessage: api.DeploymentValues.CLIUpgradeMessage.String(),
|
||||
DeploymentID: api.DeploymentID,
|
||||
}
|
||||
api.SiteHandler = site.New(&site.Options{
|
||||
BinFS: binFS,
|
||||
BinHashes: binHashes,
|
||||
@@ -453,7 +444,6 @@ func New(options *Options) *API {
|
||||
OAuth2Configs: oauthConfigs,
|
||||
DocsURL: options.DeploymentValues.DocsURL.String(),
|
||||
AppearanceFetcher: &api.AppearanceFetcher,
|
||||
BuildInfo: buildInfo,
|
||||
})
|
||||
api.SiteHandler.Experiments.Store(&experiments)
|
||||
|
||||
@@ -576,7 +566,7 @@ func New(options *Options) *API {
|
||||
DB: options.Database,
|
||||
OAuth2Configs: oauthConfigs,
|
||||
RedirectToLogin: false,
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.DisableSessionExpiryRefresh.Value(),
|
||||
Optional: false,
|
||||
SessionTokenFunc: nil, // Default behavior
|
||||
PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc,
|
||||
@@ -586,7 +576,7 @@ func New(options *Options) *API {
|
||||
DB: options.Database,
|
||||
OAuth2Configs: oauthConfigs,
|
||||
RedirectToLogin: true,
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.DisableSessionExpiryRefresh.Value(),
|
||||
Optional: false,
|
||||
SessionTokenFunc: nil, // Default behavior
|
||||
PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc,
|
||||
@@ -596,7 +586,7 @@ func New(options *Options) *API {
|
||||
DB: options.Database,
|
||||
OAuth2Configs: oauthConfigs,
|
||||
RedirectToLogin: false,
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.DisableSessionExpiryRefresh.Value(),
|
||||
Optional: true,
|
||||
SessionTokenFunc: nil, // Default behavior
|
||||
PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc,
|
||||
@@ -745,7 +735,7 @@ func New(options *Options) *API {
|
||||
// All CSP errors will be logged
|
||||
r.Post("/csp/reports", api.logReportCSPViolations)
|
||||
|
||||
r.Get("/buildinfo", buildInfoHandler(buildInfo))
|
||||
r.Get("/buildinfo", buildInfo(api.AccessURL, api.DeploymentValues.CLIUpgradeMessage.String()))
|
||||
// /regions is overridden in the enterprise version
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
@@ -1055,6 +1045,9 @@ func New(options *Options) *API {
|
||||
r.Put("/autoupdates", api.putWorkspaceAutoupdates)
|
||||
r.Get("/resolve-autostart", api.resolveAutostart)
|
||||
r.Route("/port-share", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentSharedPorts),
|
||||
)
|
||||
r.Get("/", api.workspaceAgentPortShares)
|
||||
r.Post("/", api.postWorkspaceAgentPortShare)
|
||||
r.Delete("/", api.deleteWorkspaceAgentPortShare)
|
||||
@@ -1348,7 +1341,7 @@ func compressHandler(h http.Handler) http.Handler {
|
||||
|
||||
// CreateInMemoryProvisionerDaemon is an in-memory connection to a provisionerd.
|
||||
// Useful when starting coderd and provisionerd in the same process.
|
||||
func (api *API) CreateInMemoryProvisionerDaemon(dialCtx context.Context, name string, provisionerTypes []codersdk.ProvisionerType) (client proto.DRPCProvisionerDaemonClient, err error) {
|
||||
func (api *API) CreateInMemoryProvisionerDaemon(dialCtx context.Context, name string) (client proto.DRPCProvisionerDaemonClient, err error) {
|
||||
tracer := api.TracerProvider.Tracer(tracing.TracerName)
|
||||
clientSession, serverSession := drpc.MemTransportPipe()
|
||||
defer func() {
|
||||
@@ -1365,21 +1358,18 @@ func (api *API) CreateInMemoryProvisionerDaemon(dialCtx context.Context, name st
|
||||
return nil, xerrors.Errorf("unable to fetch default org for in memory provisioner: %w", err)
|
||||
}
|
||||
|
||||
dbTypes := make([]database.ProvisionerType, 0, len(provisionerTypes))
|
||||
for _, tp := range provisionerTypes {
|
||||
dbTypes = append(dbTypes, database.ProvisionerType(tp))
|
||||
}
|
||||
|
||||
//nolint:gocritic // in-memory provisioners are owned by system
|
||||
daemon, err := api.Database.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(dialCtx), database.UpsertProvisionerDaemonParams{
|
||||
Name: name,
|
||||
OrganizationID: defaultOrg.ID,
|
||||
CreatedAt: dbtime.Now(),
|
||||
Provisioners: dbTypes,
|
||||
Tags: provisionersdk.MutateTags(uuid.Nil, nil),
|
||||
LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true},
|
||||
Version: buildinfo.Version(),
|
||||
APIVersion: proto.CurrentVersion.String(),
|
||||
Provisioners: []database.ProvisionerType{
|
||||
database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform,
|
||||
},
|
||||
Tags: provisionersdk.MutateTags(uuid.Nil, nil),
|
||||
LastSeenAt: sql.NullTime{Time: dbtime.Now(), Valid: true},
|
||||
Version: buildinfo.Version(),
|
||||
APIVersion: proto.CurrentVersion.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to create in-memory provisioner daemon: %w", err)
|
||||
|
||||
@@ -578,7 +578,7 @@ func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer {
|
||||
}()
|
||||
|
||||
daemon := provisionerd.New(func(dialCtx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
|
||||
return coderAPI.CreateInMemoryProvisionerDaemon(dialCtx, "test", []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho})
|
||||
return coderAPI.CreateInMemoryProvisionerDaemon(dialCtx, "test")
|
||||
}, &provisionerd.Options{
|
||||
Logger: coderAPI.Logger.Named("provisionerd").Leveled(slog.LevelDebug),
|
||||
UpdateInterval: 250 * time.Millisecond,
|
||||
|
||||
@@ -604,7 +604,7 @@ func (f *FakeIDP) CreateAuthCode(t testing.TB, state string) string {
|
||||
// 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 {
|
||||
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")
|
||||
@@ -625,7 +625,7 @@ func (f *FakeIDP) OIDCCallback(t testing.TB, state string, idTokenClaims jwt.Map
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
})
|
||||
return resp
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ProviderJSON is the .well-known/configuration JSON
|
||||
|
||||
@@ -54,12 +54,12 @@ func TestFakeIDPBasicFlow(t *testing.T) {
|
||||
token = oauthToken
|
||||
})
|
||||
|
||||
//nolint:bodyclose
|
||||
resp := fake.OIDCCallback(t, expectedState, jwt.MapClaims{})
|
||||
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))
|
||||
_, err = cfg.Provider.UserInfo(ctx, oauth2.StaticTokenSource(token))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now test it can refresh
|
||||
|
||||
@@ -103,7 +103,7 @@ func (q *sqlQuerier) InTx(function func(Store) error, txOpts *sql.TxOptions) err
|
||||
// Transaction succeeded.
|
||||
return nil
|
||||
}
|
||||
if !IsSerializedError(err) {
|
||||
if err != nil && !IsSerializedError(err) {
|
||||
// We should only retry if the error is a serialization error.
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -174,7 +174,6 @@ var (
|
||||
// When org scoped provisioner credentials are implemented,
|
||||
// this can be reduced to read a specific org.
|
||||
rbac.ResourceOrganization.Type: {rbac.ActionRead},
|
||||
rbac.ResourceGroup.Type: {rbac.ActionRead},
|
||||
}),
|
||||
Org: map[string][]rbac.Permission{},
|
||||
User: []rbac.Permission{},
|
||||
@@ -1142,10 +1141,6 @@ func (q *querier) GetGroupMembers(ctx context.Context, id uuid.UUID) ([]database
|
||||
return q.db.GetGroupMembers(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetGroupsByOrganizationAndUserID(ctx context.Context, arg database.GetGroupsByOrganizationAndUserIDParams) ([]database.Group, error) {
|
||||
return fetchWithPostFilter(q.auth, q.db.GetGroupsByOrganizationAndUserID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]database.Group, error) {
|
||||
return fetchWithPostFilter(q.auth, q.db.GetGroupsByOrganizationID)(ctx, organizationID)
|
||||
}
|
||||
@@ -2532,6 +2527,20 @@ func (q *querier) InsertWorkspaceAgentScripts(ctx context.Context, arg database.
|
||||
return q.db.InsertWorkspaceAgentScripts(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertWorkspaceAgentStat(ctx context.Context, arg database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
|
||||
// TODO: This is a workspace agent operation. Should users be able to query this?
|
||||
// Not really sure what this is for.
|
||||
workspace, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
|
||||
if err != nil {
|
||||
return database.WorkspaceAgentStat{}, err
|
||||
}
|
||||
err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace)
|
||||
if err != nil {
|
||||
return database.WorkspaceAgentStat{}, err
|
||||
}
|
||||
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
|
||||
|
||||
@@ -314,14 +314,6 @@ func (s *MethodTestSuite) TestGroup() {
|
||||
_ = dbgen.GroupMember(s.T(), db, database.GroupMember{})
|
||||
check.Args(g.ID).Asserts(g, rbac.ActionRead)
|
||||
}))
|
||||
s.Run("GetGroupsByOrganizationAndUserID", s.Subtest(func(db database.Store, check *expects) {
|
||||
g := dbgen.Group(s.T(), db, database.Group{})
|
||||
gm := dbgen.GroupMember(s.T(), db, database.GroupMember{GroupID: g.ID})
|
||||
check.Args(database.GetGroupsByOrganizationAndUserIDParams{
|
||||
OrganizationID: g.OrganizationID,
|
||||
UserID: gm.UserID,
|
||||
}).Asserts(g, rbac.ActionRead)
|
||||
}))
|
||||
s.Run("InsertAllUsersGroup", s.Subtest(func(db database.Store, check *expects) {
|
||||
o := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
check.Args(o.ID).Asserts(rbac.ResourceGroup.InOrg(o.ID), rbac.ActionCreate)
|
||||
@@ -1520,6 +1512,12 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||
AutomaticUpdates: database.AutomaticUpdatesAlways,
|
||||
}).Asserts(w, rbac.ActionUpdate)
|
||||
}))
|
||||
s.Run("InsertWorkspaceAgentStat", s.Subtest(func(db database.Store, check *expects) {
|
||||
ws := dbgen.Workspace(s.T(), db, database.Workspace{})
|
||||
check.Args(database.InsertWorkspaceAgentStatParams{
|
||||
WorkspaceID: ws.ID,
|
||||
}).Asserts(ws, rbac.ActionUpdate)
|
||||
}))
|
||||
s.Run("UpdateWorkspaceAppHealthByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
ws := dbgen.Workspace(s.T(), db, database.Workspace{})
|
||||
build := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{WorkspaceID: ws.ID, JobID: uuid.New()})
|
||||
|
||||
@@ -707,49 +707,27 @@ func WorkspaceAgentStat(t testing.TB, db database.Store, orig database.Workspace
|
||||
if orig.ConnectionsByProto == nil {
|
||||
orig.ConnectionsByProto = json.RawMessage([]byte("{}"))
|
||||
}
|
||||
jsonProto := []byte(fmt.Sprintf("[%s]", orig.ConnectionsByProto))
|
||||
|
||||
params := database.InsertWorkspaceAgentStatsParams{
|
||||
ID: []uuid.UUID{takeFirst(orig.ID, uuid.New())},
|
||||
CreatedAt: []time.Time{takeFirst(orig.CreatedAt, dbtime.Now())},
|
||||
UserID: []uuid.UUID{takeFirst(orig.UserID, uuid.New())},
|
||||
TemplateID: []uuid.UUID{takeFirst(orig.TemplateID, uuid.New())},
|
||||
WorkspaceID: []uuid.UUID{takeFirst(orig.WorkspaceID, uuid.New())},
|
||||
AgentID: []uuid.UUID{takeFirst(orig.AgentID, uuid.New())},
|
||||
ConnectionsByProto: jsonProto,
|
||||
ConnectionCount: []int64{takeFirst(orig.ConnectionCount, 0)},
|
||||
RxPackets: []int64{takeFirst(orig.RxPackets, 0)},
|
||||
RxBytes: []int64{takeFirst(orig.RxBytes, 0)},
|
||||
TxPackets: []int64{takeFirst(orig.TxPackets, 0)},
|
||||
TxBytes: []int64{takeFirst(orig.TxBytes, 0)},
|
||||
SessionCountVSCode: []int64{takeFirst(orig.SessionCountVSCode, 0)},
|
||||
SessionCountJetBrains: []int64{takeFirst(orig.SessionCountJetBrains, 0)},
|
||||
SessionCountReconnectingPTY: []int64{takeFirst(orig.SessionCountReconnectingPTY, 0)},
|
||||
SessionCountSSH: []int64{takeFirst(orig.SessionCountSSH, 0)},
|
||||
ConnectionMedianLatencyMS: []float64{takeFirst(orig.ConnectionMedianLatencyMS, 0)},
|
||||
}
|
||||
err := db.InsertWorkspaceAgentStats(genCtx, params)
|
||||
require.NoError(t, err, "insert workspace agent stat")
|
||||
|
||||
return database.WorkspaceAgentStat{
|
||||
ID: params.ID[0],
|
||||
CreatedAt: params.CreatedAt[0],
|
||||
UserID: params.UserID[0],
|
||||
AgentID: params.AgentID[0],
|
||||
WorkspaceID: params.WorkspaceID[0],
|
||||
TemplateID: params.TemplateID[0],
|
||||
scheme, err := db.InsertWorkspaceAgentStat(genCtx, database.InsertWorkspaceAgentStatParams{
|
||||
ID: takeFirst(orig.ID, uuid.New()),
|
||||
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
|
||||
UserID: takeFirst(orig.UserID, uuid.New()),
|
||||
TemplateID: takeFirst(orig.TemplateID, uuid.New()),
|
||||
WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()),
|
||||
AgentID: takeFirst(orig.AgentID, uuid.New()),
|
||||
ConnectionsByProto: orig.ConnectionsByProto,
|
||||
ConnectionCount: params.ConnectionCount[0],
|
||||
RxPackets: params.RxPackets[0],
|
||||
RxBytes: params.RxBytes[0],
|
||||
TxPackets: params.TxPackets[0],
|
||||
TxBytes: params.TxBytes[0],
|
||||
ConnectionMedianLatencyMS: params.ConnectionMedianLatencyMS[0],
|
||||
SessionCountVSCode: params.SessionCountVSCode[0],
|
||||
SessionCountJetBrains: params.SessionCountJetBrains[0],
|
||||
SessionCountReconnectingPTY: params.SessionCountReconnectingPTY[0],
|
||||
SessionCountSSH: params.SessionCountSSH[0],
|
||||
}
|
||||
ConnectionCount: takeFirst(orig.ConnectionCount, 0),
|
||||
RxPackets: takeFirst(orig.RxPackets, 0),
|
||||
RxBytes: takeFirst(orig.RxBytes, 0),
|
||||
TxPackets: takeFirst(orig.TxPackets, 0),
|
||||
TxBytes: takeFirst(orig.TxBytes, 0),
|
||||
SessionCountVSCode: takeFirst(orig.SessionCountVSCode, 0),
|
||||
SessionCountJetBrains: takeFirst(orig.SessionCountJetBrains, 0),
|
||||
SessionCountReconnectingPTY: takeFirst(orig.SessionCountReconnectingPTY, 0),
|
||||
SessionCountSSH: takeFirst(orig.SessionCountSSH, 0),
|
||||
ConnectionMedianLatencyMS: takeFirst(orig.ConnectionMedianLatencyMS, 0),
|
||||
})
|
||||
require.NoError(t, err, "insert workspace agent stat")
|
||||
return scheme
|
||||
}
|
||||
|
||||
func OAuth2ProviderApp(t testing.TB, db database.Store, seed database.OAuth2ProviderApp) database.OAuth2ProviderApp {
|
||||
|
||||
@@ -1506,65 +1506,13 @@ func (q *FakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
/*
|
||||
DELETE FROM
|
||||
workspace_agent_stats
|
||||
WHERE
|
||||
created_at < (
|
||||
SELECT
|
||||
COALESCE(
|
||||
-- When generating initial template usage stats, all the
|
||||
-- raw agent stats are needed, after that only ~30 mins
|
||||
-- from last rollup is needed. Deployment stats seem to
|
||||
-- use between 15 mins and 1 hour of data. We keep a
|
||||
-- little bit more (1 day) just in case.
|
||||
MAX(start_time) - '1 days'::interval,
|
||||
-- Fall back to 6 months ago if there are no template
|
||||
-- usage stats so that we don't delete the data before
|
||||
-- it's rolled up.
|
||||
NOW() - '6 months'::interval
|
||||
)
|
||||
FROM
|
||||
template_usage_stats
|
||||
)
|
||||
AND created_at < (
|
||||
-- Delete at most in batches of 3 days (with a batch size of 3 days, we
|
||||
-- can clear out the previous 6 months of data in ~60 iterations) whilst
|
||||
-- keeping the DB load relatively low.
|
||||
SELECT
|
||||
COALESCE(MIN(created_at) + '3 days'::interval, NOW())
|
||||
FROM
|
||||
workspace_agent_stats
|
||||
);
|
||||
*/
|
||||
|
||||
now := dbtime.Now()
|
||||
var limit time.Time
|
||||
// MAX
|
||||
for _, stat := range q.templateUsageStats {
|
||||
if stat.StartTime.After(limit) {
|
||||
limit = stat.StartTime.AddDate(0, 0, -1)
|
||||
}
|
||||
}
|
||||
// COALESCE
|
||||
if limit.IsZero() {
|
||||
limit = now.AddDate(0, -6, 0)
|
||||
}
|
||||
sixMonthInterval := 6 * 30 * 24 * time.Hour
|
||||
sixMonthsAgo := now.Add(-sixMonthInterval)
|
||||
|
||||
var validStats []database.WorkspaceAgentStat
|
||||
var batchLimit time.Time
|
||||
for _, stat := range q.workspaceAgentStats {
|
||||
if batchLimit.IsZero() || stat.CreatedAt.Before(batchLimit) {
|
||||
batchLimit = stat.CreatedAt
|
||||
}
|
||||
}
|
||||
if batchLimit.IsZero() {
|
||||
batchLimit = time.Now()
|
||||
} else {
|
||||
batchLimit = batchLimit.AddDate(0, 0, 3)
|
||||
}
|
||||
for _, stat := range q.workspaceAgentStats {
|
||||
if stat.CreatedAt.Before(limit) && stat.CreatedAt.Before(batchLimit) {
|
||||
if stat.CreatedAt.Before(sixMonthsAgo) {
|
||||
continue
|
||||
}
|
||||
validStats = append(validStats, stat)
|
||||
@@ -2302,30 +2250,6 @@ func (q *FakeQuerier) GetGroupMembers(_ context.Context, id uuid.UUID) ([]databa
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetGroupsByOrganizationAndUserID(_ context.Context, arg database.GetGroupsByOrganizationAndUserIDParams) ([]database.Group, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
var groupIds []uuid.UUID
|
||||
for _, member := range q.groupMembers {
|
||||
if member.UserID == arg.UserID {
|
||||
groupIds = append(groupIds, member.GroupID)
|
||||
}
|
||||
}
|
||||
groups := []database.Group{}
|
||||
for _, group := range q.groups {
|
||||
if slices.Contains(groupIds, group.ID) && group.OrganizationID == arg.OrganizationID {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetGroupsByOrganizationID(_ context.Context, id uuid.UUID) ([]database.Group, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -6533,6 +6457,37 @@ func (q *FakeQuerier) InsertWorkspaceAgentScripts(_ context.Context, arg databas
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
|
||||
if err := validateDatabaseType(p); err != nil {
|
||||
return database.WorkspaceAgentStat{}, err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
stat := database.WorkspaceAgentStat{
|
||||
ID: p.ID,
|
||||
CreatedAt: p.CreatedAt,
|
||||
WorkspaceID: p.WorkspaceID,
|
||||
AgentID: p.AgentID,
|
||||
UserID: p.UserID,
|
||||
ConnectionsByProto: p.ConnectionsByProto,
|
||||
ConnectionCount: p.ConnectionCount,
|
||||
RxPackets: p.RxPackets,
|
||||
RxBytes: p.RxBytes,
|
||||
TxPackets: p.TxPackets,
|
||||
TxBytes: p.TxBytes,
|
||||
TemplateID: p.TemplateID,
|
||||
SessionCountVSCode: p.SessionCountVSCode,
|
||||
SessionCountJetBrains: p.SessionCountJetBrains,
|
||||
SessionCountReconnectingPTY: p.SessionCountReconnectingPTY,
|
||||
SessionCountSSH: p.SessionCountSSH,
|
||||
ConnectionMedianLatencyMS: p.ConnectionMedianLatencyMS,
|
||||
}
|
||||
q.workspaceAgentStats = append(q.workspaceAgentStats, stat)
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) InsertWorkspaceAgentStats(_ context.Context, arg database.InsertWorkspaceAgentStatsParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
@@ -9141,6 +9096,7 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
var innerErr error
|
||||
index := slices.IndexFunc(params, func(buildParam database.WorkspaceBuildParameter) bool {
|
||||
// If hasParam matches, then we are done. This is a good match.
|
||||
if slices.ContainsFunc(arg.HasParam, func(name string) bool {
|
||||
@@ -9167,6 +9123,9 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
||||
|
||||
return match
|
||||
})
|
||||
if innerErr != nil {
|
||||
return nil, xerrors.Errorf("error searching workspace build params: %w", innerErr)
|
||||
}
|
||||
if index < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -179,9 +179,8 @@ func (m metricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.Contex
|
||||
|
||||
func (m metricsStore) DeleteCoordinator(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteCoordinator(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteCoordinator").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
defer m.queryLatencies.WithLabelValues("DeleteCoordinator").Observe(time.Since(start).Seconds())
|
||||
return m.s.DeleteCoordinator(ctx, id)
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error {
|
||||
@@ -284,16 +283,14 @@ func (m metricsStore) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt
|
||||
|
||||
func (m metricsStore) DeleteTailnetAgent(ctx context.Context, arg database.DeleteTailnetAgentParams) (database.DeleteTailnetAgentRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.DeleteTailnetAgent(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("DeleteTailnetAgent").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
defer m.queryLatencies.WithLabelValues("DeleteTailnetAgent").Observe(time.Since(start).Seconds())
|
||||
return m.s.DeleteTailnetAgent(ctx, arg)
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteTailnetClient(ctx context.Context, arg database.DeleteTailnetClientParams) (database.DeleteTailnetClientRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.DeleteTailnetClient(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("DeleteTailnetClient").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
defer m.queryLatencies.WithLabelValues("DeleteTailnetClient").Observe(time.Since(start).Seconds())
|
||||
return m.s.DeleteTailnetClient(ctx, arg)
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteTailnetClientSubscription(ctx context.Context, arg database.DeleteTailnetClientSubscriptionParams) error {
|
||||
@@ -562,13 +559,6 @@ func (m metricsStore) GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (m metricsStore) GetGroupsByOrganizationAndUserID(ctx context.Context, arg database.GetGroupsByOrganizationAndUserIDParams) ([]database.Group, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetGroupsByOrganizationAndUserID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetGroupsByOrganizationAndUserID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]database.Group, error) {
|
||||
start := time.Now()
|
||||
groups, err := m.s.GetGroupsByOrganizationID(ctx, organizationID)
|
||||
@@ -858,16 +848,14 @@ func (m metricsStore) GetServiceBanner(ctx context.Context) (string, error) {
|
||||
|
||||
func (m metricsStore) GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]database.TailnetAgent, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetTailnetAgents(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetTailnetAgents").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
defer m.queryLatencies.WithLabelValues("GetTailnetAgents").Observe(time.Since(start).Seconds())
|
||||
return m.s.GetTailnetAgents(ctx, id)
|
||||
}
|
||||
|
||||
func (m metricsStore) GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]database.TailnetClient, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetTailnetClientsForAgent(ctx, agentID)
|
||||
m.queryLatencies.WithLabelValues("GetTailnetClientsForAgent").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
defer m.queryLatencies.WithLabelValues("GetTailnetClientsForAgent").Observe(time.Since(start).Seconds())
|
||||
return m.s.GetTailnetClientsForAgent(ctx, agentID)
|
||||
}
|
||||
|
||||
func (m metricsStore) GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]database.TailnetPeer, error) {
|
||||
@@ -1654,6 +1642,13 @@ func (m metricsStore) InsertWorkspaceAgentScripts(ctx context.Context, arg datab
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) InsertWorkspaceAgentStat(ctx context.Context, arg database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
|
||||
start := time.Now()
|
||||
stat, err := m.s.InsertWorkspaceAgentStat(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertWorkspaceAgentStat").Observe(time.Since(start).Seconds())
|
||||
return stat, err
|
||||
}
|
||||
|
||||
func (m metricsStore) InsertWorkspaceAgentStats(ctx context.Context, arg database.InsertWorkspaceAgentStatsParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.InsertWorkspaceAgentStats(ctx, arg)
|
||||
@@ -2209,16 +2204,14 @@ func (m metricsStore) UpsertServiceBanner(ctx context.Context, value string) err
|
||||
|
||||
func (m metricsStore) UpsertTailnetAgent(ctx context.Context, arg database.UpsertTailnetAgentParams) (database.TailnetAgent, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpsertTailnetAgent(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpsertTailnetAgent").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
defer m.queryLatencies.WithLabelValues("UpsertTailnetAgent").Observe(time.Since(start).Seconds())
|
||||
return m.s.UpsertTailnetAgent(ctx, arg)
|
||||
}
|
||||
|
||||
func (m metricsStore) UpsertTailnetClient(ctx context.Context, arg database.UpsertTailnetClientParams) (database.TailnetClient, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpsertTailnetClient(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpsertTailnetClient").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
defer m.queryLatencies.WithLabelValues("UpsertTailnetClient").Observe(time.Since(start).Seconds())
|
||||
return m.s.UpsertTailnetClient(ctx, arg)
|
||||
}
|
||||
|
||||
func (m metricsStore) UpsertTailnetClientSubscription(ctx context.Context, arg database.UpsertTailnetClientSubscriptionParams) error {
|
||||
@@ -2230,9 +2223,8 @@ func (m metricsStore) UpsertTailnetClientSubscription(ctx context.Context, arg d
|
||||
|
||||
func (m metricsStore) UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (database.TailnetCoordinator, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpsertTailnetCoordinator(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("UpsertTailnetCoordinator").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
defer m.queryLatencies.WithLabelValues("UpsertTailnetCoordinator").Observe(time.Since(start).Seconds())
|
||||
return m.s.UpsertTailnetCoordinator(ctx, id)
|
||||
}
|
||||
|
||||
func (m metricsStore) UpsertTailnetPeer(ctx context.Context, arg database.UpsertTailnetPeerParams) (database.TailnetPeer, error) {
|
||||
|
||||
@@ -1095,21 +1095,6 @@ func (mr *MockStoreMockRecorder) GetGroupMembers(arg0, arg1 any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembers", reflect.TypeOf((*MockStore)(nil).GetGroupMembers), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetGroupsByOrganizationAndUserID mocks base method.
|
||||
func (m *MockStore) GetGroupsByOrganizationAndUserID(arg0 context.Context, arg1 database.GetGroupsByOrganizationAndUserIDParams) ([]database.Group, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetGroupsByOrganizationAndUserID", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.Group)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetGroupsByOrganizationAndUserID indicates an expected call of GetGroupsByOrganizationAndUserID.
|
||||
func (mr *MockStoreMockRecorder) GetGroupsByOrganizationAndUserID(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsByOrganizationAndUserID", reflect.TypeOf((*MockStore)(nil).GetGroupsByOrganizationAndUserID), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetGroupsByOrganizationID mocks base method.
|
||||
func (m *MockStore) GetGroupsByOrganizationID(arg0 context.Context, arg1 uuid.UUID) ([]database.Group, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -3471,6 +3456,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAgentScripts(arg0, arg1 any) *go
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentScripts", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentScripts), arg0, arg1)
|
||||
}
|
||||
|
||||
// InsertWorkspaceAgentStat mocks base method.
|
||||
func (m *MockStore) InsertWorkspaceAgentStat(arg0 context.Context, arg1 database.InsertWorkspaceAgentStatParams) (database.WorkspaceAgentStat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertWorkspaceAgentStat", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.WorkspaceAgentStat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertWorkspaceAgentStat indicates an expected call of InsertWorkspaceAgentStat.
|
||||
func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStat(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
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()
|
||||
|
||||
@@ -2,10 +2,11 @@ package dbpurge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
@@ -23,6 +24,7 @@ const (
|
||||
// This is for cleaning up old, unused resources from the database that take up space.
|
||||
func New(ctx context.Context, logger slog.Logger, db database.Store) io.Closer {
|
||||
closed := make(chan struct{})
|
||||
logger = logger.Named("dbpurge")
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(ctx)
|
||||
//nolint:gocritic // The system purges old db records without user input.
|
||||
@@ -34,37 +36,22 @@ func New(ctx context.Context, logger slog.Logger, db database.Store) io.Closer {
|
||||
doTick := func() {
|
||||
defer ticker.Reset(delay)
|
||||
|
||||
start := time.Now()
|
||||
// Start a transaction to grab advisory lock, we don't want to run
|
||||
// multiple purges at the same time (multiple replicas).
|
||||
if err := db.InTx(func(tx database.Store) error {
|
||||
// Acquire a lock to ensure that only one instance of the
|
||||
// purge is running at a time.
|
||||
ok, err := tx.TryAcquireLock(ctx, database.LockIDDBPurge)
|
||||
if err != nil {
|
||||
return err
|
||||
var eg errgroup.Group
|
||||
eg.Go(func() error {
|
||||
return db.DeleteOldWorkspaceAgentLogs(ctx)
|
||||
})
|
||||
eg.Go(func() error {
|
||||
return db.DeleteOldWorkspaceAgentStats(ctx)
|
||||
})
|
||||
eg.Go(func() error {
|
||||
return db.DeleteOldProvisionerDaemons(ctx)
|
||||
})
|
||||
err := eg.Wait()
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
logger.Debug(ctx, "unable to acquire lock for purging old database entries, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := tx.DeleteOldWorkspaceAgentLogs(ctx); err != nil {
|
||||
return xerrors.Errorf("failed to delete old workspace agent logs: %w", err)
|
||||
}
|
||||
if err := tx.DeleteOldWorkspaceAgentStats(ctx); err != nil {
|
||||
return xerrors.Errorf("failed to delete old workspace agent stats: %w", err)
|
||||
}
|
||||
if err := tx.DeleteOldProvisionerDaemons(ctx); err != nil {
|
||||
return xerrors.Errorf("failed to delete old provisioner daemons: %w", err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "purged old database entries", slog.F("duration", time.Since(start)))
|
||||
|
||||
return nil
|
||||
}, nil); err != nil {
|
||||
logger.Error(ctx, "failed to purge old database entries", slog.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,12 @@ import (
|
||||
"go.uber.org/goleak"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"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/dbmem"
|
||||
"github.com/coder/coder/v2/coderd/database/dbpurge"
|
||||
"github.com/coder/coder/v2/coderd/database/dbrollup"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/provisionerd/proto"
|
||||
@@ -42,62 +40,27 @@ func TestDeleteOldWorkspaceAgentStats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
|
||||
now := dbtime.Now()
|
||||
|
||||
defer func() {
|
||||
if t.Failed() {
|
||||
t.Logf("Test failed, printing rows...")
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
wasRows, err := db.GetWorkspaceAgentStats(ctx, now.AddDate(0, -7, 0))
|
||||
if err == nil {
|
||||
for _, row := range wasRows {
|
||||
t.Logf("workspace agent stat: %v", row)
|
||||
}
|
||||
}
|
||||
tusRows, err := db.GetTemplateUsageStats(context.Background(), database.GetTemplateUsageStatsParams{
|
||||
StartTime: now.AddDate(0, -7, 0),
|
||||
EndTime: now,
|
||||
})
|
||||
if err == nil {
|
||||
for _, row := range tusRows {
|
||||
t.Logf("template usage stat: %v", row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
now := dbtime.Now()
|
||||
|
||||
// given
|
||||
// Let's use RxBytes to identify stat entries.
|
||||
// Stat inserted 6 months + 1 hour ago, should be deleted.
|
||||
first := dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{
|
||||
CreatedAt: now.AddDate(0, -6, 0).Add(-time.Hour),
|
||||
ConnectionCount: 1,
|
||||
CreatedAt: now.Add(-6*30*24*time.Hour - time.Hour),
|
||||
ConnectionMedianLatencyMS: 1,
|
||||
RxBytes: 1111,
|
||||
SessionCountSSH: 1,
|
||||
})
|
||||
|
||||
// Stat inserted 6 months - 1 hour ago, should not be deleted before rollup.
|
||||
// Stat inserted 6 months - 1 hour ago, should not be deleted.
|
||||
second := dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{
|
||||
CreatedAt: now.AddDate(0, -6, 0).Add(time.Hour),
|
||||
ConnectionCount: 1,
|
||||
CreatedAt: now.Add(-5*30*24*time.Hour + time.Hour),
|
||||
ConnectionMedianLatencyMS: 1,
|
||||
RxBytes: 2222,
|
||||
SessionCountSSH: 1,
|
||||
})
|
||||
|
||||
// Stat inserted 6 months - 1 day - 2 hour ago, should not be deleted at all.
|
||||
third := dbgen.WorkspaceAgentStat(t, db, database.WorkspaceAgentStat{
|
||||
CreatedAt: now.AddDate(0, -6, 0).AddDate(0, 0, 1).Add(2 * time.Hour),
|
||||
ConnectionCount: 1,
|
||||
ConnectionMedianLatencyMS: 1,
|
||||
RxBytes: 3333,
|
||||
SessionCountSSH: 1,
|
||||
})
|
||||
|
||||
// when
|
||||
@@ -107,39 +70,15 @@ func TestDeleteOldWorkspaceAgentStats(t *testing.T) {
|
||||
// then
|
||||
var stats []database.GetWorkspaceAgentStatsRow
|
||||
var err error
|
||||
require.Eventuallyf(t, func() bool {
|
||||
require.Eventually(t, func() bool {
|
||||
// Query all stats created not earlier than 7 months ago
|
||||
stats, err = db.GetWorkspaceAgentStats(ctx, now.AddDate(0, -7, 0))
|
||||
stats, err = db.GetWorkspaceAgentStats(ctx, now.Add(-7*30*24*time.Hour))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !containsWorkspaceAgentStat(stats, first) &&
|
||||
containsWorkspaceAgentStat(stats, second)
|
||||
}, testutil.WaitShort, testutil.IntervalFast, "it should delete old stats: %v", stats)
|
||||
|
||||
// when
|
||||
events := make(chan dbrollup.Event)
|
||||
rolluper := dbrollup.New(logger, db, dbrollup.WithEventChannel(events))
|
||||
defer rolluper.Close()
|
||||
|
||||
_, _ = <-events, <-events
|
||||
|
||||
// Start a new purger to immediately trigger delete after rollup.
|
||||
_ = closer.Close()
|
||||
closer = dbpurge.New(ctx, logger, db)
|
||||
defer closer.Close()
|
||||
|
||||
// then
|
||||
require.Eventuallyf(t, func() bool {
|
||||
// Query all stats created not earlier than 7 months ago
|
||||
stats, err = db.GetWorkspaceAgentStats(ctx, now.AddDate(0, -7, 0))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !containsWorkspaceAgentStat(stats, first) &&
|
||||
!containsWorkspaceAgentStat(stats, second) &&
|
||||
containsWorkspaceAgentStat(stats, third)
|
||||
}, testutil.WaitShort, testutil.IntervalFast, "it should delete old stats after rollup: %v", stats)
|
||||
}, testutil.WaitShort, testutil.IntervalFast, stats)
|
||||
}
|
||||
|
||||
func containsWorkspaceAgentStat(stats []database.GetWorkspaceAgentStatsRow, needle database.WorkspaceAgentStat) bool {
|
||||
|
||||
@@ -143,8 +143,8 @@ func TestRollupTemplateUsageStats(t *testing.T) {
|
||||
db, ps := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
|
||||
anHourAgo := dbtime.Now().Add(-time.Hour).Truncate(time.Hour).UTC()
|
||||
anHourAndSixMonthsAgo := anHourAgo.AddDate(0, -6, 0).UTC()
|
||||
anHourAgo := dbtime.Now().Add(-time.Hour).Truncate(time.Hour)
|
||||
anHourAndSixMonthsAgo := anHourAgo.AddDate(0, -6, 0)
|
||||
|
||||
var (
|
||||
org = dbgen.Organization(t, db, database.Organization{})
|
||||
@@ -242,12 +242,6 @@ func TestRollupTemplateUsageStats(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stats, 1)
|
||||
|
||||
// I do not know a better way to do this. Our database runs in a *random*
|
||||
// timezone. So the returned time is in a random timezone and fails on the
|
||||
// equal even though they are the same time if converted back to the same timezone.
|
||||
stats[0].EndTime = stats[0].EndTime.UTC()
|
||||
stats[0].StartTime = stats[0].StartTime.UTC()
|
||||
|
||||
require.Equal(t, database.TemplateUsageStat{
|
||||
TemplateID: tpl.ID,
|
||||
UserID: user.ID,
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -185,21 +184,20 @@ func DumpOnFailure(t testing.TB, connectionURL string) {
|
||||
now := time.Now()
|
||||
timeSuffix := fmt.Sprintf("%d%d%d%d%d%d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
|
||||
outPath := filepath.Join(cwd, snakeCaseName+"."+timeSuffix+".test.sql")
|
||||
dump, err := PGDump(connectionURL)
|
||||
dump, err := pgDump(connectionURL)
|
||||
if err != nil {
|
||||
t.Errorf("dump on failure: failed to run pg_dump")
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(outPath, normalizeDump(dump), 0o600); err != nil {
|
||||
if err := os.WriteFile(outPath, filterDump(dump), 0o600); err != nil {
|
||||
t.Errorf("dump on failure: failed to write: %s", err.Error())
|
||||
return
|
||||
}
|
||||
t.Logf("Dumped database to %q due to failed test. I hope you find what you're looking for!", outPath)
|
||||
}
|
||||
|
||||
// PGDump runs pg_dump against dbURL and returns the output.
|
||||
// It is used by DumpOnFailure().
|
||||
func PGDump(dbURL string) ([]byte, error) {
|
||||
// pgDump runs pg_dump against dbURL and returns the output.
|
||||
func pgDump(dbURL string) ([]byte, error) {
|
||||
if _, err := exec.LookPath("pg_dump"); err != nil {
|
||||
return nil, xerrors.Errorf("could not find pg_dump in path: %w", err)
|
||||
}
|
||||
@@ -232,79 +230,16 @@ func PGDump(dbURL string) ([]byte, error) {
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
const minimumPostgreSQLVersion = 13
|
||||
// Unfortunately, some insert expressions span multiple lines.
|
||||
// The below may be over-permissive but better that than truncating data.
|
||||
var insertExpr = regexp.MustCompile(`(?s)\bINSERT[^;]+;`)
|
||||
|
||||
// PGDumpSchemaOnly is for use by gen/dump only.
|
||||
// It runs pg_dump against dbURL and sets a consistent timezone and encoding.
|
||||
func PGDumpSchemaOnly(dbURL string) ([]byte, error) {
|
||||
hasPGDump := false
|
||||
if _, err := exec.LookPath("pg_dump"); err == nil {
|
||||
out, err := exec.Command("pg_dump", "--version").Output()
|
||||
if err == nil {
|
||||
// Parse output:
|
||||
// pg_dump (PostgreSQL) 14.5 (Ubuntu 14.5-0ubuntu0.22.04.1)
|
||||
parts := strings.Split(string(out), " ")
|
||||
if len(parts) > 2 {
|
||||
version, err := strconv.Atoi(strings.Split(parts[2], ".")[0])
|
||||
if err == nil && version >= minimumPostgreSQLVersion {
|
||||
hasPGDump = true
|
||||
}
|
||||
}
|
||||
}
|
||||
func filterDump(dump []byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
matches := insertExpr.FindAll(dump, -1)
|
||||
for _, m := range matches {
|
||||
_, _ = buf.Write(m)
|
||||
_, _ = buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
cmdArgs := []string{
|
||||
"pg_dump",
|
||||
"--schema-only",
|
||||
dbURL,
|
||||
"--no-privileges",
|
||||
"--no-owner",
|
||||
"--no-privileges",
|
||||
"--no-publication",
|
||||
"--no-security-labels",
|
||||
"--no-subscriptions",
|
||||
"--no-tablespaces",
|
||||
|
||||
// We never want to manually generate
|
||||
// queries executing against this table.
|
||||
"--exclude-table=schema_migrations",
|
||||
}
|
||||
|
||||
if !hasPGDump {
|
||||
cmdArgs = append([]string{
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--network=host",
|
||||
fmt.Sprintf("gcr.io/coder-dev-1/postgres:%d", minimumPostgreSQLVersion),
|
||||
}, cmdArgs...)
|
||||
}
|
||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) //#nosec
|
||||
cmd.Env = append(os.Environ(), []string{
|
||||
"PGTZ=UTC",
|
||||
"PGCLIENTENCODING=UTF8",
|
||||
}...)
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return normalizeDump(output.Bytes()), nil
|
||||
}
|
||||
|
||||
func normalizeDump(schema []byte) []byte {
|
||||
// Remove all comments.
|
||||
schema = regexp.MustCompile(`(?im)^(--.*)$`).ReplaceAll(schema, []byte{})
|
||||
// Public is implicit in the schema.
|
||||
schema = regexp.MustCompile(`(?im)( |::|'|\()public\.`).ReplaceAll(schema, []byte(`$1`))
|
||||
// Remove database settings.
|
||||
schema = regexp.MustCompile(`(?im)^(SET.*;)`).ReplaceAll(schema, []byte(``))
|
||||
// Remove select statements
|
||||
schema = regexp.MustCompile(`(?im)^(SELECT.*;)`).ReplaceAll(schema, []byte(``))
|
||||
// Removes multiple newlines.
|
||||
schema = regexp.MustCompile(`(?im)\n{3,}`).ReplaceAll(schema, []byte("\n\n"))
|
||||
|
||||
return schema
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -695,7 +695,7 @@ CREATE TABLE replicas (
|
||||
|
||||
CREATE TABLE site_configs (
|
||||
key character varying(256) NOT NULL,
|
||||
value text NOT NULL
|
||||
value character varying(8192) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE tailnet_agents (
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/database/migrations"
|
||||
)
|
||||
|
||||
var preamble = []byte("-- Code generated by 'make coderd/database/generate'. DO NOT EDIT.")
|
||||
const minimumPostgreSQLVersion = 13
|
||||
|
||||
func main() {
|
||||
connection, closeFn, err := dbtestutil.Open()
|
||||
@@ -23,23 +28,95 @@ func main() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
err = migrations.Up(db)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dumpBytes, err := dbtestutil.PGDumpSchemaOnly(connection)
|
||||
hasPGDump := false
|
||||
if _, err = exec.LookPath("pg_dump"); err == nil {
|
||||
out, err := exec.Command("pg_dump", "--version").Output()
|
||||
if err == nil {
|
||||
// Parse output:
|
||||
// pg_dump (PostgreSQL) 14.5 (Ubuntu 14.5-0ubuntu0.22.04.1)
|
||||
parts := strings.Split(string(out), " ")
|
||||
if len(parts) > 2 {
|
||||
version, err := strconv.Atoi(strings.Split(parts[2], ".")[0])
|
||||
if err == nil && version >= minimumPostgreSQLVersion {
|
||||
hasPGDump = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmdArgs := []string{
|
||||
"pg_dump",
|
||||
"--schema-only",
|
||||
connection,
|
||||
"--no-privileges",
|
||||
"--no-owner",
|
||||
|
||||
// We never want to manually generate
|
||||
// queries executing against this table.
|
||||
"--exclude-table=schema_migrations",
|
||||
}
|
||||
|
||||
if !hasPGDump {
|
||||
cmdArgs = append([]string{
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--network=host",
|
||||
fmt.Sprintf("gcr.io/coder-dev-1/postgres:%d", minimumPostgreSQLVersion),
|
||||
}, cmdArgs...)
|
||||
}
|
||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) //#nosec
|
||||
cmd.Env = append(os.Environ(), []string{
|
||||
"PGTZ=UTC",
|
||||
"PGCLIENTENCODING=UTF8",
|
||||
}...)
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, sed := range []string{
|
||||
// Remove all comments.
|
||||
"/^--/d",
|
||||
// Public is implicit in the schema.
|
||||
"s/ public\\./ /g",
|
||||
"s/::public\\./::/g",
|
||||
"s/'public\\./'/g",
|
||||
"s/(public\\./(/g",
|
||||
// Remove database settings.
|
||||
"s/SET .* = .*;//g",
|
||||
// Remove select statements. These aren't useful
|
||||
// to a reader of the dump.
|
||||
"s/SELECT.*;//g",
|
||||
// Removes multiple newlines.
|
||||
"/^$/N;/^\\n$/D",
|
||||
} {
|
||||
cmd := exec.Command("sed", "-e", sed)
|
||||
cmd.Stdin = bytes.NewReader(output.Bytes())
|
||||
output = bytes.Buffer{}
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
dump := fmt.Sprintf("-- Code generated by 'make coderd/database/generate'. DO NOT EDIT.\n%s", output.Bytes())
|
||||
_, mainPath, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("couldn't get caller path")
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(mainPath, "..", "..", "..", "dump.sql"), append(preamble, dumpBytes...), 0o600)
|
||||
err = os.WriteFile(filepath.Join(mainPath, "..", "..", "..", "dump.sql"), []byte(dump), 0o600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ const (
|
||||
LockIDDeploymentSetup = iota + 1
|
||||
LockIDEnterpriseDeploymentSetup
|
||||
LockIDDBRollup
|
||||
LockIDDBPurge
|
||||
)
|
||||
|
||||
// GenLockID generates a unique and consistent lock ID from a given string.
|
||||
|
||||
@@ -11,15 +11,11 @@ CREATE OR REPLACE FUNCTION revert_migrate_external_auth_providers_to_jsonb(jsonb
|
||||
DECLARE
|
||||
result text[];
|
||||
BEGIN
|
||||
IF jsonb_typeof($1) = 'null' THEN
|
||||
result := '{}';
|
||||
ELSE
|
||||
SELECT
|
||||
array_agg(id::text) INTO result
|
||||
FROM (
|
||||
SELECT
|
||||
jsonb_array_elements($1) ->> 'id' AS id) AS external_auth_provider_ids;
|
||||
END IF;
|
||||
SELECT
|
||||
array_agg(id::text) INTO result
|
||||
FROM (
|
||||
SELECT
|
||||
jsonb_array_elements($1) ->> 'id' AS id) AS external_auth_provider_ids;
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "site_configs" ALTER COLUMN "value" TYPE character varying(8192);
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "site_configs" ALTER COLUMN "value" TYPE text;
|
||||
@@ -17,12 +17,9 @@ import (
|
||||
//go:embed *.sql
|
||||
var migrations embed.FS
|
||||
|
||||
func setup(db *sql.DB, migs fs.FS) (source.Driver, *migrate.Migrate, error) {
|
||||
if migs == nil {
|
||||
migs = migrations
|
||||
}
|
||||
func setup(db *sql.DB) (source.Driver, *migrate.Migrate, error) {
|
||||
ctx := context.Background()
|
||||
sourceDriver, err := iofs.New(migs, ".")
|
||||
sourceDriver, err := iofs.New(migrations, ".")
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("create iofs: %w", err)
|
||||
}
|
||||
@@ -50,13 +47,8 @@ func setup(db *sql.DB, migs fs.FS) (source.Driver, *migrate.Migrate, error) {
|
||||
}
|
||||
|
||||
// Up runs SQL migrations to ensure the database schema is up-to-date.
|
||||
func Up(db *sql.DB) error {
|
||||
return UpWithFS(db, migrations)
|
||||
}
|
||||
|
||||
// UpWithFS runs SQL migrations in the given fs.
|
||||
func UpWithFS(db *sql.DB, migs fs.FS) (retErr error) {
|
||||
_, m, err := setup(db, migs)
|
||||
func Up(db *sql.DB) (retErr error) {
|
||||
_, m, err := setup(db)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("migrate setup: %w", err)
|
||||
}
|
||||
@@ -87,7 +79,7 @@ func UpWithFS(db *sql.DB, migs fs.FS) (retErr error) {
|
||||
|
||||
// Down runs all down SQL migrations.
|
||||
func Down(db *sql.DB) error {
|
||||
_, m, err := setup(db, migrations)
|
||||
_, m, err := setup(db)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("migrate setup: %w", err)
|
||||
}
|
||||
@@ -109,7 +101,7 @@ func Down(db *sql.DB) error {
|
||||
// applied, without making any changes to the database. If not, returns a
|
||||
// non-nil error.
|
||||
func EnsureClean(db *sql.DB) error {
|
||||
sourceDriver, m, err := setup(db, migrations)
|
||||
sourceDriver, m, err := setup(db)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("migrate setup: %w", err)
|
||||
}
|
||||
@@ -175,7 +167,7 @@ func CheckLatestVersion(sourceDriver source.Driver, currentVersion uint) error {
|
||||
// Stepper cannot be closed pre-emptively, it must be run to completion
|
||||
// (or until an error is encountered).
|
||||
func Stepper(db *sql.DB) (next func() (version uint, more bool, err error), err error) {
|
||||
_, m, err := setup(db, migrations)
|
||||
_, m, err := setup(db)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("migrate setup: %w", err)
|
||||
}
|
||||
|
||||
@@ -123,7 +123,6 @@ type sqlcQuerier interface {
|
||||
// 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)
|
||||
GetGroupsByOrganizationAndUserID(ctx context.Context, arg GetGroupsByOrganizationAndUserIDParams) ([]Group, error)
|
||||
GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error)
|
||||
GetHealthSettings(ctx context.Context) (string, error)
|
||||
GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error)
|
||||
@@ -336,6 +335,7 @@ type sqlcQuerier interface {
|
||||
InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error)
|
||||
InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error
|
||||
InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, 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
|
||||
|
||||
@@ -1484,67 +1484,6 @@ func (q *sqlQuerier) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrg
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getGroupsByOrganizationAndUserID = `-- name: GetGroupsByOrganizationAndUserID :many
|
||||
SELECT
|
||||
groups.id, groups.name, groups.organization_id, groups.avatar_url, groups.quota_allowance, groups.display_name, groups.source
|
||||
FROM
|
||||
groups
|
||||
-- If the group is a user made group, then we need to check the group_members table.
|
||||
LEFT JOIN
|
||||
group_members
|
||||
ON
|
||||
group_members.group_id = groups.id AND
|
||||
group_members.user_id = $1
|
||||
-- If it is the "Everyone" group, then we need to check the organization_members table.
|
||||
LEFT JOIN
|
||||
organization_members
|
||||
ON
|
||||
organization_members.organization_id = groups.id AND
|
||||
organization_members.user_id = $1
|
||||
WHERE
|
||||
-- In either case, the group_id will only match an org or a group.
|
||||
(group_members.user_id = $1 OR organization_members.user_id = $1)
|
||||
AND
|
||||
-- Ensure the group or organization is the specified organization.
|
||||
groups.organization_id = $2
|
||||
`
|
||||
|
||||
type GetGroupsByOrganizationAndUserIDParams struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetGroupsByOrganizationAndUserID(ctx context.Context, arg GetGroupsByOrganizationAndUserIDParams) ([]Group, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getGroupsByOrganizationAndUserID, arg.UserID, arg.OrganizationID)
|
||||
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 getGroupsByOrganizationID = `-- name: GetGroupsByOrganizationID :many
|
||||
SELECT
|
||||
id, name, organization_id, avatar_url, quota_allowance, display_name, source
|
||||
@@ -10111,35 +10050,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg Up
|
||||
}
|
||||
|
||||
const deleteOldWorkspaceAgentStats = `-- name: DeleteOldWorkspaceAgentStats :exec
|
||||
DELETE FROM
|
||||
workspace_agent_stats
|
||||
WHERE
|
||||
created_at < (
|
||||
SELECT
|
||||
COALESCE(
|
||||
-- When generating initial template usage stats, all the
|
||||
-- raw agent stats are needed, after that only ~30 mins
|
||||
-- from last rollup is needed. Deployment stats seem to
|
||||
-- use between 15 mins and 1 hour of data. We keep a
|
||||
-- little bit more (1 day) just in case.
|
||||
MAX(start_time) - '1 days'::interval,
|
||||
-- Fall back to 6 months ago if there are no template
|
||||
-- usage stats so that we don't delete the data before
|
||||
-- it's rolled up.
|
||||
NOW() - '6 months'::interval
|
||||
)
|
||||
FROM
|
||||
template_usage_stats
|
||||
)
|
||||
AND created_at < (
|
||||
-- Delete at most in batches of 4 hours (with this batch size, assuming
|
||||
-- 1 iteration / 10 minutes, we can clear out the previous 6 months of
|
||||
-- data in 7.5 days) whilst keeping the DB load low.
|
||||
SELECT
|
||||
COALESCE(MIN(created_at) + '4 hours'::interval, NOW())
|
||||
FROM
|
||||
workspace_agent_stats
|
||||
)
|
||||
DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '180 days'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteOldWorkspaceAgentStats(ctx context.Context) error {
|
||||
@@ -10475,6 +10386,94 @@ func (q *sqlQuerier) GetWorkspaceAgentStatsAndLabels(ctx context.Context, create
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertWorkspaceAgentStat = `-- name: InsertWorkspaceAgentStat :one
|
||||
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
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh
|
||||
`
|
||||
|
||||
type InsertWorkspaceAgentStatParams 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) InsertWorkspaceAgentStat(ctx context.Context, arg InsertWorkspaceAgentStatParams) (WorkspaceAgentStat, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertWorkspaceAgentStat,
|
||||
arg.ID,
|
||||
arg.CreatedAt,
|
||||
arg.UserID,
|
||||
arg.WorkspaceID,
|
||||
arg.TemplateID,
|
||||
arg.AgentID,
|
||||
arg.ConnectionsByProto,
|
||||
arg.ConnectionCount,
|
||||
arg.RxPackets,
|
||||
arg.RxBytes,
|
||||
arg.TxPackets,
|
||||
arg.TxBytes,
|
||||
arg.SessionCountVSCode,
|
||||
arg.SessionCountJetBrains,
|
||||
arg.SessionCountReconnectingPTY,
|
||||
arg.SessionCountSSH,
|
||||
arg.ConnectionMedianLatencyMS,
|
||||
)
|
||||
var i WorkspaceAgentStat
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UserID,
|
||||
&i.AgentID,
|
||||
&i.WorkspaceID,
|
||||
&i.TemplateID,
|
||||
&i.ConnectionsByProto,
|
||||
&i.ConnectionCount,
|
||||
&i.RxPackets,
|
||||
&i.RxBytes,
|
||||
&i.TxPackets,
|
||||
&i.TxBytes,
|
||||
&i.ConnectionMedianLatencyMS,
|
||||
&i.SessionCountVSCode,
|
||||
&i.SessionCountJetBrains,
|
||||
&i.SessionCountReconnectingPTY,
|
||||
&i.SessionCountSSH,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertWorkspaceAgentStats = `-- name: InsertWorkspaceAgentStats :exec
|
||||
INSERT INTO
|
||||
workspace_agent_stats (
|
||||
|
||||
@@ -28,31 +28,6 @@ FROM
|
||||
WHERE
|
||||
organization_id = $1;
|
||||
|
||||
-- name: GetGroupsByOrganizationAndUserID :many
|
||||
SELECT
|
||||
groups.*
|
||||
FROM
|
||||
groups
|
||||
-- If the group is a user made group, then we need to check the group_members table.
|
||||
LEFT JOIN
|
||||
group_members
|
||||
ON
|
||||
group_members.group_id = groups.id AND
|
||||
group_members.user_id = @user_id
|
||||
-- If it is the "Everyone" group, then we need to check the organization_members table.
|
||||
LEFT JOIN
|
||||
organization_members
|
||||
ON
|
||||
organization_members.organization_id = groups.id AND
|
||||
organization_members.user_id = @user_id
|
||||
WHERE
|
||||
-- In either case, the group_id will only match an org or a group.
|
||||
(group_members.user_id = @user_id OR organization_members.user_id = @user_id)
|
||||
AND
|
||||
-- Ensure the group or organization is the specified organization.
|
||||
groups.organization_id = @organization_id;
|
||||
|
||||
|
||||
-- name: InsertGroup :one
|
||||
INSERT INTO groups (
|
||||
id,
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
-- name: InsertWorkspaceAgentStat :one
|
||||
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
|
||||
)
|
||||
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 (
|
||||
@@ -66,35 +90,7 @@ ORDER BY
|
||||
date ASC;
|
||||
|
||||
-- name: DeleteOldWorkspaceAgentStats :exec
|
||||
DELETE FROM
|
||||
workspace_agent_stats
|
||||
WHERE
|
||||
created_at < (
|
||||
SELECT
|
||||
COALESCE(
|
||||
-- When generating initial template usage stats, all the
|
||||
-- raw agent stats are needed, after that only ~30 mins
|
||||
-- from last rollup is needed. Deployment stats seem to
|
||||
-- use between 15 mins and 1 hour of data. We keep a
|
||||
-- little bit more (1 day) just in case.
|
||||
MAX(start_time) - '1 days'::interval,
|
||||
-- Fall back to 6 months ago if there are no template
|
||||
-- usage stats so that we don't delete the data before
|
||||
-- it's rolled up.
|
||||
NOW() - '6 months'::interval
|
||||
)
|
||||
FROM
|
||||
template_usage_stats
|
||||
)
|
||||
AND created_at < (
|
||||
-- Delete at most in batches of 4 hours (with this batch size, assuming
|
||||
-- 1 iteration / 10 minutes, we can clear out the previous 6 months of
|
||||
-- data in 7.5 days) whilst keeping the DB load low.
|
||||
SELECT
|
||||
COALESCE(MIN(created_at) + '4 hours'::interval, NOW())
|
||||
FROM
|
||||
workspace_agent_stats
|
||||
);
|
||||
DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '180 days';
|
||||
|
||||
-- name: GetDeploymentWorkspaceAgentStats :one
|
||||
WITH agent_stats AS (
|
||||
|
||||
+11
-3
@@ -2,7 +2,9 @@ package coderd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"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"
|
||||
@@ -66,10 +68,16 @@ func (api *API) deploymentStats(rw http.ResponseWriter, r *http.Request) {
|
||||
// @Tags General
|
||||
// @Success 200 {object} codersdk.BuildInfoResponse
|
||||
// @Router /buildinfo [get]
|
||||
func buildInfoHandler(resp codersdk.BuildInfoResponse) http.HandlerFunc {
|
||||
// This is in a handler so that we can generate API docs info.
|
||||
func buildInfo(accessURL *url.URL, upgradeMessage string) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, resp)
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.BuildInfoResponse{
|
||||
ExternalURL: buildinfo.ExternalURL(),
|
||||
Version: buildinfo.Version(),
|
||||
AgentAPIVersion: AgentAPIVersionREST,
|
||||
DashboardURL: accessURL.String(),
|
||||
WorkspaceProxy: false,
|
||||
UpgradeMessage: upgradeMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -563,9 +563,6 @@ func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) {
|
||||
|
||||
// Dynamic defaults
|
||||
switch codersdk.EnhancedExternalAuthProvider(config.Type) {
|
||||
case codersdk.EnhancedExternalAuthProviderGitLab:
|
||||
copyDefaultSettings(config, gitlabDefaults(config))
|
||||
return
|
||||
case codersdk.EnhancedExternalAuthProviderBitBucketServer:
|
||||
copyDefaultSettings(config, bitbucketServerDefaults(config))
|
||||
return
|
||||
@@ -670,44 +667,6 @@ func bitbucketServerDefaults(config *codersdk.ExternalAuthConfig) codersdk.Exter
|
||||
return defaults
|
||||
}
|
||||
|
||||
// gitlabDefaults returns a static config if using the gitlab cloud offering.
|
||||
// The values are dynamic if using a self-hosted gitlab.
|
||||
// When the decision is not obvious, just defer to the cloud defaults.
|
||||
// Any user specific fields will override this if provided.
|
||||
func gitlabDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthConfig {
|
||||
cloud := codersdk.ExternalAuthConfig{
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
ValidateURL: "https://gitlab.com/oauth/token/info",
|
||||
DisplayName: "GitLab",
|
||||
DisplayIcon: "/icon/gitlab.svg",
|
||||
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
|
||||
Scopes: []string{"write_repository"},
|
||||
}
|
||||
|
||||
if config.AuthURL == "" || config.AuthURL == cloud.AuthURL {
|
||||
return cloud
|
||||
}
|
||||
|
||||
au, err := url.Parse(config.AuthURL)
|
||||
if err != nil || au.Host == "gitlab.com" {
|
||||
// If the AuthURL is not a valid URL or is using the cloud,
|
||||
// use the cloud static defaults.
|
||||
return cloud
|
||||
}
|
||||
|
||||
// At this point, assume it is self-hosted and use the AuthURL
|
||||
return codersdk.ExternalAuthConfig{
|
||||
DisplayName: cloud.DisplayName,
|
||||
Scopes: cloud.Scopes,
|
||||
DisplayIcon: cloud.DisplayIcon,
|
||||
AuthURL: au.ResolveReference(&url.URL{Path: "/oauth/authorize"}).String(),
|
||||
TokenURL: au.ResolveReference(&url.URL{Path: "/oauth/token"}).String(),
|
||||
ValidateURL: au.ResolveReference(&url.URL{Path: "/oauth/token/info"}).String(),
|
||||
Regex: fmt.Sprintf(`^(https?://)?%s(/.*)?$`, strings.ReplaceAll(au.Host, ".", `\.`)),
|
||||
}
|
||||
}
|
||||
|
||||
func jfrogArtifactoryDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthConfig {
|
||||
defaults := codersdk.ExternalAuthConfig{
|
||||
DisplayName: "JFrog Artifactory",
|
||||
@@ -830,6 +789,15 @@ var staticDefaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.External
|
||||
Regex: `^(https?://)?bitbucket\.org(/.*)?$`,
|
||||
Scopes: []string{"account", "repository:write"},
|
||||
},
|
||||
codersdk.EnhancedExternalAuthProviderGitLab: {
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
ValidateURL: "https://gitlab.com/oauth/token/info",
|
||||
DisplayName: "GitLab",
|
||||
DisplayIcon: "/icon/gitlab.svg",
|
||||
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
|
||||
Scopes: []string{"write_repository"},
|
||||
},
|
||||
codersdk.EnhancedExternalAuthProviderGitHub: {
|
||||
AuthURL: xgithub.Endpoint.AuthURL,
|
||||
TokenURL: xgithub.Endpoint.TokenURL,
|
||||
|
||||
@@ -8,112 +8,6 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestGitlabDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// The default cloud setup. Copying this here as hard coded
|
||||
// values.
|
||||
cloud := codersdk.ExternalAuthConfig{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
ID: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
ValidateURL: "https://gitlab.com/oauth/token/info",
|
||||
DisplayName: "GitLab",
|
||||
DisplayIcon: "/icon/gitlab.svg",
|
||||
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
|
||||
Scopes: []string{"write_repository"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input codersdk.ExternalAuthConfig
|
||||
expected codersdk.ExternalAuthConfig
|
||||
mutateExpected func(*codersdk.ExternalAuthConfig)
|
||||
}{
|
||||
// Cloud
|
||||
{
|
||||
name: "OnlyType",
|
||||
input: codersdk.ExternalAuthConfig{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
},
|
||||
expected: cloud,
|
||||
},
|
||||
{
|
||||
// If someone was to manually configure the gitlab cli.
|
||||
name: "CloudByConfig",
|
||||
input: codersdk.ExternalAuthConfig{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
},
|
||||
expected: cloud,
|
||||
},
|
||||
{
|
||||
// Changing some of the defaults of the cloud option
|
||||
name: "CloudWithChanges",
|
||||
input: codersdk.ExternalAuthConfig{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
// Adding an extra query param intentionally to break simple
|
||||
// string comparisons.
|
||||
AuthURL: "https://gitlab.com/oauth/authorize?foo=bar",
|
||||
DisplayName: "custom",
|
||||
Regex: ".*",
|
||||
},
|
||||
expected: cloud,
|
||||
mutateExpected: func(config *codersdk.ExternalAuthConfig) {
|
||||
config.AuthURL = "https://gitlab.com/oauth/authorize?foo=bar"
|
||||
config.DisplayName = "custom"
|
||||
config.Regex = ".*"
|
||||
},
|
||||
},
|
||||
// Self-hosted
|
||||
{
|
||||
// Dynamically figures out the Validate, Token, and Regex fields.
|
||||
name: "SelfHostedOnlyAuthURL",
|
||||
input: codersdk.ExternalAuthConfig{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
AuthURL: "https://gitlab.company.org/oauth/authorize?foo=bar",
|
||||
},
|
||||
expected: cloud,
|
||||
mutateExpected: func(config *codersdk.ExternalAuthConfig) {
|
||||
config.AuthURL = "https://gitlab.company.org/oauth/authorize?foo=bar"
|
||||
config.ValidateURL = "https://gitlab.company.org/oauth/token/info"
|
||||
config.TokenURL = "https://gitlab.company.org/oauth/token"
|
||||
config.Regex = `^(https?://)?gitlab\.company\.org(/.*)?$`
|
||||
},
|
||||
},
|
||||
{
|
||||
// Strange values
|
||||
name: "RandomValues",
|
||||
input: codersdk.ExternalAuthConfig{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
AuthURL: "https://auth.com/auth",
|
||||
ValidateURL: "https://validate.com/validate",
|
||||
TokenURL: "https://token.com/token",
|
||||
Regex: "random",
|
||||
},
|
||||
expected: cloud,
|
||||
mutateExpected: func(config *codersdk.ExternalAuthConfig) {
|
||||
config.AuthURL = "https://auth.com/auth"
|
||||
config.ValidateURL = "https://validate.com/validate"
|
||||
config.TokenURL = "https://token.com/token"
|
||||
config.Regex = `random`
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range tests {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
applyDefaultsToConfig(&c.input)
|
||||
if c.mutateExpected != nil {
|
||||
c.mutateExpected(&c.expected)
|
||||
}
|
||||
require.Equal(t, c.input, c.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_bitbucketServerConfigDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@ package externalauth_test
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -15,7 +13,6 @@ import (
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
@@ -420,78 +417,6 @@ func TestConvertYAML(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestConstantQueryParams verifies a constant query parameter can be set in the
|
||||
// "authenticate" url for external auth applications, and it will be carried forward
|
||||
// to actual auth requests.
|
||||
// This unit test was specifically created for Auth0 which can set an
|
||||
// audience query parameter in it's /authorize endpoint.
|
||||
func TestConstantQueryParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
const constantQueryParamKey = "audience"
|
||||
const constantQueryParamValue = "foobar"
|
||||
constantQueryParam := fmt.Sprintf("%s=%s", constantQueryParamKey, constantQueryParamValue)
|
||||
fake, config, _ := setupOauth2Test(t, testConfig{
|
||||
FakeIDPOpts: []oidctest.FakeIDPOpt{
|
||||
oidctest.WithMiddlewares(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if strings.Contains(request.URL.Path, "authorize") {
|
||||
// Assert has the audience query param
|
||||
assert.Equal(t, request.URL.Query().Get(constantQueryParamKey), constantQueryParamValue)
|
||||
}
|
||||
next.ServeHTTP(writer, request)
|
||||
})
|
||||
}),
|
||||
},
|
||||
CoderOIDCConfigOpts: []func(cfg *coderd.OIDCConfig){
|
||||
func(cfg *coderd.OIDCConfig) {
|
||||
// Include a constant query parameter.
|
||||
authURL, err := url.Parse(cfg.OAuth2Config.(*oauth2.Config).Endpoint.AuthURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
authURL.RawQuery = url.Values{constantQueryParamKey: []string{constantQueryParamValue}}.Encode()
|
||||
cfg.OAuth2Config.(*oauth2.Config).Endpoint.AuthURL = authURL.String()
|
||||
require.Contains(t, cfg.OAuth2Config.(*oauth2.Config).Endpoint.AuthURL, constantQueryParam)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
callbackCalled := false
|
||||
fake.SetCoderdCallbackHandler(func(writer http.ResponseWriter, request *http.Request) {
|
||||
// Just record the callback was hit, and the auth succeeded.
|
||||
callbackCalled = true
|
||||
})
|
||||
|
||||
// Verify the AuthURL endpoint contains the constant query parameter and is a valid URL.
|
||||
// It should look something like:
|
||||
// http://127.0.0.1:<port>>/oauth2/authorize?
|
||||
// audience=foobar&
|
||||
// client_id=d<uuid>&
|
||||
// redirect_uri=<redirect>&
|
||||
// response_type=code&
|
||||
// scope=openid+email+profile&
|
||||
// state=state
|
||||
const state = "state"
|
||||
rawAuthURL := config.AuthCodeURL(state)
|
||||
// Parsing the url is not perfect. It allows imperfections like the query
|
||||
// params having 2 question marks '?a=foo?b=bar'.
|
||||
// So use it to validate, then verify the raw url is as expected.
|
||||
authURL, err := url.Parse(rawAuthURL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, authURL.Query().Get(constantQueryParamKey), constantQueryParamValue)
|
||||
// We are not using a real server, so it fakes https://coder.com
|
||||
require.Equal(t, authURL.Scheme, "https")
|
||||
// Validate the raw URL.
|
||||
// Double check only 1 '?' exists. Url parsing allows multiple '?' in the query string.
|
||||
require.Equal(t, strings.Count(rawAuthURL, "?"), 1)
|
||||
|
||||
// Actually run an auth request. Although it says OIDC, the flow is the same
|
||||
// for oauth2.
|
||||
//nolint:bodyclose
|
||||
resp := fake.OIDCCallback(t, state, jwt.MapClaims{})
|
||||
require.True(t, callbackCalled)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
type testConfig struct {
|
||||
FakeIDPOpts []oidctest.FakeIDPOpt
|
||||
CoderOIDCConfigOpts []func(cfg *coderd.OIDCConfig)
|
||||
@@ -508,10 +433,6 @@ type testConfig struct {
|
||||
func setupOauth2Test(t *testing.T, settings testConfig) (*oidctest.FakeIDP, *externalauth.Config, database.ExternalAuthLink) {
|
||||
t.Helper()
|
||||
|
||||
if settings.ExternalAuthOpt == nil {
|
||||
settings.ExternalAuthOpt = func(_ *externalauth.Config) {}
|
||||
}
|
||||
|
||||
const providerID = "test-idp"
|
||||
fake := oidctest.NewFakeIDP(t,
|
||||
append([]oidctest.FakeIDPOpt{}, settings.FakeIDPOpts...)...,
|
||||
|
||||
@@ -32,8 +32,6 @@ const (
|
||||
warningNodeUsesWebsocket = `Node uses WebSockets because the "Upgrade: DERP" header may be blocked on the load balancer.`
|
||||
oneNodeUnhealthy = "Region is operational, but performance might be degraded as one node is unhealthy."
|
||||
missingNodeReport = "Missing node health report, probably a developer error."
|
||||
noSTUN = "No STUN servers are available."
|
||||
stunMapVaryDest = "STUN returned different addresses; you may be behind a hard NAT."
|
||||
)
|
||||
|
||||
type ReportOptions struct {
|
||||
@@ -109,30 +107,9 @@ func (r *Report) Run(ctx context.Context, opts *ReportOptions) {
|
||||
ncReport, netcheckErr := nc.GetReport(ctx, opts.DERPMap)
|
||||
r.Netcheck = ncReport
|
||||
r.NetcheckErr = convertError(netcheckErr)
|
||||
if mapVaryDest, _ := r.Netcheck.MappingVariesByDestIP.Get(); mapVaryDest {
|
||||
r.Warnings = append(r.Warnings, health.Messagef(health.CodeSTUNMapVaryDest, stunMapVaryDest))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Count the number of STUN-capable nodes.
|
||||
var stunCapableNodes int
|
||||
var stunTotalNodes int
|
||||
for _, region := range r.Regions {
|
||||
for _, node := range region.NodeReports {
|
||||
if node.STUN.Enabled {
|
||||
stunTotalNodes++
|
||||
}
|
||||
if node.STUN.CanSTUN {
|
||||
stunCapableNodes++
|
||||
}
|
||||
}
|
||||
}
|
||||
if stunCapableNodes == 0 && stunTotalNodes > 0 {
|
||||
r.Severity = health.SeverityWarning
|
||||
r.Warnings = append(r.Warnings, health.Messagef(health.CodeSTUNNoNodes, noSTUN))
|
||||
}
|
||||
|
||||
// Review region reports and select the highest severity.
|
||||
for _, regionReport := range r.Regions {
|
||||
if regionReport.Severity.Value() > r.Severity.Value() {
|
||||
@@ -156,8 +133,8 @@ func (r *RegionReport) Run(ctx context.Context) {
|
||||
node = node
|
||||
nodeReport = NodeReport{
|
||||
DERPNodeReport: healthsdk.DERPNodeReport{
|
||||
Healthy: true,
|
||||
Node: node,
|
||||
Healthy: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -129,67 +129,9 @@ func TestDERP(t *testing.T) {
|
||||
assert.True(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityWarning, report.Severity)
|
||||
assert.True(t, report.Dismissed)
|
||||
if assert.Len(t, report.Warnings, 1) {
|
||||
if assert.NotEmpty(t, report.Warnings) {
|
||||
assert.Contains(t, report.Warnings[0].Code, health.CodeDERPOneNodeUnhealthy)
|
||||
}
|
||||
for _, region := range report.Regions {
|
||||
assert.True(t, region.Healthy)
|
||||
assert.True(t, region.NodeReports[0].Healthy)
|
||||
assert.Empty(t, region.NodeReports[0].Warnings)
|
||||
assert.Equal(t, health.SeverityOK, region.NodeReports[0].Severity)
|
||||
assert.False(t, region.NodeReports[1].Healthy)
|
||||
assert.Equal(t, health.SeverityError, region.NodeReports[1].Severity)
|
||||
assert.Len(t, region.Warnings, 1)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HealthyWithNoSTUN", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
healthyDerpSrv := derp.NewServer(key.NewNode(), func(format string, args ...any) { t.Logf(format, args...) })
|
||||
defer healthyDerpSrv.Close()
|
||||
healthySrv := httptest.NewServer(derphttp.Handler(healthyDerpSrv))
|
||||
defer healthySrv.Close()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
report = derphealth.Report{}
|
||||
derpURL, _ = url.Parse(healthySrv.URL)
|
||||
opts = &derphealth.ReportOptions{
|
||||
DERPMap: &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
EmbeddedRelay: true,
|
||||
RegionID: 999,
|
||||
Nodes: []*tailcfg.DERPNode{{
|
||||
Name: "1a",
|
||||
RegionID: 999,
|
||||
HostName: derpURL.Host,
|
||||
IPv4: derpURL.Host,
|
||||
STUNPort: -1,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}, {
|
||||
Name: "badstun",
|
||||
RegionID: 999,
|
||||
HostName: derpURL.Host,
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
}
|
||||
)
|
||||
|
||||
report.Run(ctx, opts)
|
||||
|
||||
assert.True(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityWarning, report.Severity)
|
||||
if assert.Len(t, report.Warnings, 2) {
|
||||
assert.EqualValues(t, report.Warnings[1].Code, health.CodeSTUNNoNodes)
|
||||
assert.EqualValues(t, report.Warnings[0].Code, health.CodeDERPOneNodeUnhealthy)
|
||||
}
|
||||
for _, region := range report.Regions {
|
||||
assert.True(t, region.Healthy)
|
||||
assert.True(t, region.NodeReports[0].Healthy)
|
||||
@@ -349,10 +291,8 @@ func TestDERP(t *testing.T) {
|
||||
report.Run(ctx, opts)
|
||||
|
||||
assert.True(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityOK, report.Severity)
|
||||
for _, region := range report.Regions {
|
||||
assert.True(t, region.Healthy)
|
||||
assert.Equal(t, health.SeverityOK, region.Severity)
|
||||
for _, node := range region.NodeReports {
|
||||
assert.True(t, node.Healthy)
|
||||
assert.False(t, node.CanExchangeMessages)
|
||||
@@ -364,107 +304,6 @@ func TestDERP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("STUNOnly/OneBadOneGood", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
report = derphealth.Report{}
|
||||
opts = &derphealth.ReportOptions{
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
EmbeddedRelay: true,
|
||||
RegionID: 999,
|
||||
Nodes: []*tailcfg.DERPNode{{
|
||||
Name: "badstun",
|
||||
RegionID: 999,
|
||||
HostName: "badstun.example.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}, {
|
||||
Name: "goodstun",
|
||||
RegionID: 999,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
report.Run(ctx, opts)
|
||||
assert.True(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityWarning, report.Severity)
|
||||
if assert.Len(t, report.Warnings, 1) {
|
||||
assert.Equal(t, health.CodeDERPOneNodeUnhealthy, report.Warnings[0].Code)
|
||||
}
|
||||
for _, region := range report.Regions {
|
||||
assert.True(t, region.Healthy)
|
||||
assert.Equal(t, health.SeverityWarning, region.Severity)
|
||||
// badstun
|
||||
assert.False(t, region.NodeReports[0].Healthy)
|
||||
assert.True(t, region.NodeReports[0].STUN.Enabled)
|
||||
assert.False(t, region.NodeReports[0].STUN.CanSTUN)
|
||||
assert.NotNil(t, region.NodeReports[0].STUN.Error)
|
||||
// goodstun
|
||||
assert.True(t, region.NodeReports[1].Healthy)
|
||||
assert.True(t, region.NodeReports[1].STUN.Enabled)
|
||||
assert.True(t, region.NodeReports[1].STUN.CanSTUN)
|
||||
assert.Nil(t, region.NodeReports[1].STUN.Error)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("STUNOnly/NoStun", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
report = derphealth.Report{}
|
||||
opts = &derphealth.ReportOptions{
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
EmbeddedRelay: true,
|
||||
RegionID: 999,
|
||||
Nodes: []*tailcfg.DERPNode{{
|
||||
Name: "badstun",
|
||||
RegionID: 999,
|
||||
HostName: "badstun.example.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
InsecureForTests: true,
|
||||
ForceHTTP: true,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
report.Run(ctx, opts)
|
||||
assert.False(t, report.Healthy)
|
||||
assert.Equal(t, health.SeverityError, report.Severity)
|
||||
for _, region := range report.Regions {
|
||||
assert.False(t, region.Healthy)
|
||||
assert.Equal(t, health.SeverityError, region.Severity)
|
||||
for _, node := range region.NodeReports {
|
||||
assert.False(t, node.Healthy)
|
||||
assert.False(t, node.CanExchangeMessages)
|
||||
assert.Empty(t, node.ClientLogs)
|
||||
assert.True(t, node.STUN.Enabled)
|
||||
assert.False(t, node.STUN.CanSTUN)
|
||||
assert.NotNil(t, node.STUN.Error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func tsDERPMap(ctx context.Context, t testing.TB) *tailcfg.DERPMap {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
)
|
||||
|
||||
@@ -37,19 +36,12 @@ const (
|
||||
|
||||
CodeDERPNodeUsesWebsocket Code = `EDERP01`
|
||||
CodeDERPOneNodeUnhealthy Code = `EDERP02`
|
||||
CodeSTUNNoNodes = `ESTUN01`
|
||||
CodeSTUNMapVaryDest = `ESTUN02`
|
||||
|
||||
CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01`
|
||||
CodeProvisionerDaemonVersionMismatch Code = `EPD02`
|
||||
CodeProvisionerDaemonAPIMajorVersionDeprecated Code = `EPD03`
|
||||
)
|
||||
|
||||
// Default docs URL
|
||||
var (
|
||||
docsURLDefault = "https://coder.com/docs/v2"
|
||||
)
|
||||
|
||||
// @typescript-generate Severity
|
||||
type Severity string
|
||||
|
||||
@@ -78,30 +70,6 @@ func (m Message) String() string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// URL returns a link to the admin/healthcheck docs page for the given Message.
|
||||
// NOTE: if using a custom docs URL, specify base.
|
||||
func (m Message) URL(base string) string {
|
||||
var codeAnchor string
|
||||
if m.Code == "" {
|
||||
codeAnchor = strings.ToLower(string(CodeUnknown))
|
||||
} else {
|
||||
codeAnchor = strings.ToLower(string(m.Code))
|
||||
}
|
||||
|
||||
if base == "" {
|
||||
base = docsURLDefault
|
||||
versionPath := buildinfo.Version()
|
||||
if buildinfo.IsDev() {
|
||||
// for development versions, just use latest
|
||||
versionPath = "latest"
|
||||
}
|
||||
return fmt.Sprintf("%s/%s/admin/healthcheck#%s", base, versionPath, codeAnchor)
|
||||
}
|
||||
|
||||
// We don't assume that custom docs URLs are versioned.
|
||||
return fmt.Sprintf("%s/admin/healthcheck#%s", base, codeAnchor)
|
||||
}
|
||||
|
||||
// Code is a stable identifier used to link to documentation.
|
||||
// @typescript-generate Code
|
||||
type Code string
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/healthcheck/health"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_MessageURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
code health.Code
|
||||
base string
|
||||
expected string
|
||||
}{
|
||||
{"empty", "", "", "https://coder.com/docs/v2/latest/admin/healthcheck#eunknown"},
|
||||
{"default", health.CodeAccessURLFetch, "", "https://coder.com/docs/v2/latest/admin/healthcheck#eacs03"},
|
||||
{"custom docs base", health.CodeAccessURLFetch, "https://example.com/docs", "https://example.com/docs/admin/healthcheck#eacs03"},
|
||||
} {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
uut := health.Message{Code: tt.code}
|
||||
actual := uut.URL(tt.base)
|
||||
assert.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -58,39 +58,27 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "OK",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
healthy: true,
|
||||
@@ -100,39 +88,27 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "DERPFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
healthy: false,
|
||||
@@ -142,40 +118,28 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "DERPWarning",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
Severity: health.SeverityWarning,
|
||||
},
|
||||
Healthy: true,
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
Severity: health.SeverityWarning,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
healthy: true,
|
||||
@@ -185,39 +149,27 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "AccessURLFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityWarning,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityWarning,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
healthy: false,
|
||||
@@ -227,39 +179,27 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "WebsocketFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
healthy: false,
|
||||
@@ -269,39 +209,27 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "DatabaseFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
healthy: false,
|
||||
@@ -311,39 +239,27 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "ProxyFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
severity: health.SeverityError,
|
||||
@@ -353,40 +269,28 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "ProxyWarn",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
Severity: health.SeverityWarning,
|
||||
},
|
||||
Healthy: true,
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
Severity: health.SeverityWarning,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
},
|
||||
severity: health.SeverityWarning,
|
||||
@@ -396,39 +300,27 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "ProvisionerDaemonsFail",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
severity: health.SeverityError,
|
||||
@@ -438,40 +330,28 @@ func TestHealthcheck(t *testing.T) {
|
||||
name: "ProvisionerDaemonsWarn",
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: true,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
Healthy: true,
|
||||
Severity: health.SeverityOK,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityWarning,
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
},
|
||||
Severity: health.SeverityWarning,
|
||||
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
|
||||
},
|
||||
},
|
||||
severity: health.SeverityWarning,
|
||||
@@ -482,39 +362,27 @@ func TestHealthcheck(t *testing.T) {
|
||||
healthy: false,
|
||||
checker: &testChecker{
|
||||
DERPReport: healthsdk.DERPHealthReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
AccessURLReport: healthsdk.AccessURLReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
WebsocketReport: healthsdk.WebsocketReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
DatabaseReport: healthsdk.DatabaseReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
WorkspaceProxyReport: healthsdk.WorkspaceProxyReport{
|
||||
Healthy: false,
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Healthy: false,
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
ProvisionerDaemonsReport: healthsdk.ProvisionerDaemonsReport{
|
||||
BaseReport: healthsdk.BaseReport{
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
Severity: health.SeverityError,
|
||||
},
|
||||
},
|
||||
severity: health.SeverityError,
|
||||
|
||||
@@ -31,7 +31,7 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions)
|
||||
defer cancel()
|
||||
|
||||
r.Severity = health.SeverityOK
|
||||
r.Warnings = []health.Message{}
|
||||
r.Warnings = []string{}
|
||||
r.Dismissed = opts.Dismissed
|
||||
|
||||
u, err := opts.AccessURL.Parse("/api/v2/debug/ws")
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -74,11 +75,7 @@ func extractTokenParams(r *http.Request, callbackURL *url.URL) (tokenParams, []c
|
||||
return params, nil, nil
|
||||
}
|
||||
|
||||
// Tokens
|
||||
// TODO: the sessions lifetime config passed is for coder api tokens.
|
||||
// Should there be a separate config for oauth2 tokens? They are related,
|
||||
// but they are not the same.
|
||||
func Tokens(db database.Store, lifetimes codersdk.SessionLifetime) http.HandlerFunc {
|
||||
func Tokens(db database.Store, defaultLifetime time.Duration) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
app := httpmw.OAuth2ProviderApp(r)
|
||||
@@ -107,9 +104,9 @@ func Tokens(db database.Store, lifetimes codersdk.SessionLifetime) http.HandlerF
|
||||
switch params.grantType {
|
||||
// TODO: Client creds, device code.
|
||||
case codersdk.OAuth2ProviderGrantTypeRefreshToken:
|
||||
token, err = refreshTokenGrant(ctx, db, app, lifetimes, params)
|
||||
token, err = refreshTokenGrant(ctx, db, app, defaultLifetime, params)
|
||||
case codersdk.OAuth2ProviderGrantTypeAuthorizationCode:
|
||||
token, err = authorizationCodeGrant(ctx, db, app, lifetimes, params)
|
||||
token, err = authorizationCodeGrant(ctx, db, app, defaultLifetime, params)
|
||||
default:
|
||||
// Grant types are validated by the parser, so getting through here means
|
||||
// the developer added a type but forgot to add a case here.
|
||||
@@ -140,7 +137,7 @@ func Tokens(db database.Store, lifetimes codersdk.SessionLifetime) http.HandlerF
|
||||
}
|
||||
}
|
||||
|
||||
func authorizationCodeGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, lifetimes codersdk.SessionLifetime, params tokenParams) (oauth2.Token, error) {
|
||||
func authorizationCodeGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, defaultLifetime time.Duration, params tokenParams) (oauth2.Token, error) {
|
||||
// Validate the client secret.
|
||||
secret, err := parseSecret(params.clientSecret)
|
||||
if err != nil {
|
||||
@@ -198,9 +195,11 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
|
||||
// TODO: We are ignoring scopes for now.
|
||||
tokenName := fmt.Sprintf("%s_%s_oauth_session_token", dbCode.UserID, app.ID)
|
||||
key, sessionToken, err := apikey.Generate(apikey.CreateParams{
|
||||
UserID: dbCode.UserID,
|
||||
LoginType: database.LoginTypeOAuth2ProviderApp,
|
||||
DefaultLifetime: lifetimes.DefaultDuration.Value(),
|
||||
UserID: dbCode.UserID,
|
||||
LoginType: database.LoginTypeOAuth2ProviderApp,
|
||||
// TODO: This is just the lifetime for api keys, maybe have its own config
|
||||
// settings. #11693
|
||||
DefaultLifetime: defaultLifetime,
|
||||
// For now, we allow only one token per app and user at a time.
|
||||
TokenName: tokenName,
|
||||
})
|
||||
@@ -272,7 +271,7 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
|
||||
}, nil
|
||||
}
|
||||
|
||||
func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, lifetimes codersdk.SessionLifetime, params tokenParams) (oauth2.Token, error) {
|
||||
func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAuth2ProviderApp, defaultLifetime time.Duration, params tokenParams) (oauth2.Token, error) {
|
||||
// Validate the token.
|
||||
token, err := parseSecret(params.refreshToken)
|
||||
if err != nil {
|
||||
@@ -327,9 +326,11 @@ func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAut
|
||||
// TODO: We are ignoring scopes for now.
|
||||
tokenName := fmt.Sprintf("%s_%s_oauth_session_token", prevKey.UserID, app.ID)
|
||||
key, sessionToken, err := apikey.Generate(apikey.CreateParams{
|
||||
UserID: prevKey.UserID,
|
||||
LoginType: database.LoginTypeOAuth2ProviderApp,
|
||||
DefaultLifetime: lifetimes.DefaultDuration.Value(),
|
||||
UserID: prevKey.UserID,
|
||||
LoginType: database.LoginTypeOAuth2ProviderApp,
|
||||
// TODO: This is just the lifetime for api keys, maybe have its own config
|
||||
// settings. #11693
|
||||
DefaultLifetime: defaultLifetime,
|
||||
// For now, we allow only one token per app and user at a time.
|
||||
TokenName: tokenName,
|
||||
})
|
||||
|
||||
@@ -162,7 +162,6 @@ func (c *Cache) refreshDeploymentStats(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *Cache) run(ctx context.Context, name string, interval time.Duration, refresh func(context.Context) error) {
|
||||
logger := c.log.With(slog.F("name", name), slog.F("interval", interval))
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -174,13 +173,15 @@ func (c *Cache) run(ctx context.Context, name string, interval time.Duration, re
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
break
|
||||
}
|
||||
logger.Error(ctx, "refresh metrics failed", slog.Error(err))
|
||||
c.log.Error(ctx, "refresh", slog.Error(err))
|
||||
continue
|
||||
}
|
||||
logger.Debug(ctx, "metrics refreshed", slog.F("took", time.Since(start)))
|
||||
c.log.Debug(
|
||||
ctx,
|
||||
name+" metrics refreshed",
|
||||
slog.F("took", time.Since(start)),
|
||||
slog.F("interval", interval),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package metricscache_test
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -281,25 +280,14 @@ func TestCache_DeploymentStats(t *testing.T) {
|
||||
})
|
||||
defer cache.Close()
|
||||
|
||||
err := db.InsertWorkspaceAgentStats(context.Background(), database.InsertWorkspaceAgentStatsParams{
|
||||
ID: []uuid.UUID{uuid.New()},
|
||||
CreatedAt: []time.Time{dbtime.Now()},
|
||||
WorkspaceID: []uuid.UUID{uuid.New()},
|
||||
UserID: []uuid.UUID{uuid.New()},
|
||||
TemplateID: []uuid.UUID{uuid.New()},
|
||||
AgentID: []uuid.UUID{uuid.New()},
|
||||
ConnectionsByProto: json.RawMessage(`[{}]`),
|
||||
|
||||
RxPackets: []int64{0},
|
||||
RxBytes: []int64{1},
|
||||
TxPackets: []int64{0},
|
||||
TxBytes: []int64{1},
|
||||
ConnectionCount: []int64{1},
|
||||
SessionCountVSCode: []int64{1},
|
||||
SessionCountJetBrains: []int64{0},
|
||||
SessionCountReconnectingPTY: []int64{0},
|
||||
SessionCountSSH: []int64{0},
|
||||
ConnectionMedianLatencyMS: []float64{10},
|
||||
_, err := db.InsertWorkspaceAgentStat(context.Background(), database.InsertWorkspaceAgentStatParams{
|
||||
ID: uuid.New(),
|
||||
AgentID: uuid.New(),
|
||||
CreatedAt: dbtime.Now(),
|
||||
ConnectionCount: 1,
|
||||
RxBytes: 1,
|
||||
TxBytes: 1,
|
||||
SessionCountVSCode: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
+1
-1
@@ -354,7 +354,7 @@ func (api *API) getOAuth2ProviderAppAuthorize() http.HandlerFunc {
|
||||
// @Success 200 {object} oauth2.Token
|
||||
// @Router /oauth2/tokens [post]
|
||||
func (api *API) postOAuth2ProviderAppToken() http.HandlerFunc {
|
||||
return identityprovider.Tokens(api.Database, api.DeploymentValues.Sessions)
|
||||
return identityprovider.Tokens(api.Database, api.DeploymentValues.SessionDuration.Value())
|
||||
}
|
||||
|
||||
// @Summary Delete OAuth2 application tokens.
|
||||
|
||||
@@ -79,23 +79,10 @@ func Workspaces(ctx context.Context, logger slog.Logger, registerer prometheus.R
|
||||
duration = defaultRefreshRate
|
||||
}
|
||||
|
||||
// TODO: deprecated: remove in the future
|
||||
// See: https://github.com/coder/coder/issues/12999
|
||||
// Deprecation reason: gauge metrics should avoid suffix `_total``
|
||||
workspaceLatestBuildTotalsDeprecated := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "coderd",
|
||||
Subsystem: "api",
|
||||
Name: "workspace_latest_build_total",
|
||||
Help: "DEPRECATED: use coderd_api_workspace_latest_build instead",
|
||||
}, []string{"status"})
|
||||
if err := registerer.Register(workspaceLatestBuildTotalsDeprecated); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workspaceLatestBuildTotals := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "coderd",
|
||||
Subsystem: "api",
|
||||
Name: "workspace_latest_build",
|
||||
Name: "workspace_latest_build_total",
|
||||
Help: "The current number of workspace builds by status.",
|
||||
}, []string{"status"})
|
||||
if err := registerer.Register(workspaceLatestBuildTotals); err != nil {
|
||||
@@ -144,8 +131,6 @@ func Workspaces(ctx context.Context, logger slog.Logger, registerer prometheus.R
|
||||
for _, job := range jobs {
|
||||
status := codersdk.ProvisionerJobStatus(job.JobStatus)
|
||||
workspaceLatestBuildTotals.WithLabelValues(string(status)).Add(1)
|
||||
// TODO: deprecated: remove in the future
|
||||
workspaceLatestBuildTotalsDeprecated.WithLabelValues(string(status)).Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -31,7 +32,6 @@ import (
|
||||
"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/cryptorand"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
@@ -159,7 +159,7 @@ func TestWorkspaceLatestBuildTotals(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
sum := 0
|
||||
for _, m := range metrics {
|
||||
if m.GetName() != "coderd_api_workspace_latest_build" {
|
||||
if m.GetName() != "coderd_api_workspace_latest_build_total" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -511,29 +511,23 @@ func TestAgentStats(t *testing.T) {
|
||||
func TestExperimentsMetric(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if len(codersdk.ExperimentsAll) == 0 {
|
||||
t.Skip("No experiments are currently defined; skipping test.")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
experiments codersdk.Experiments
|
||||
expected map[codersdk.Experiment]float64
|
||||
}{
|
||||
{
|
||||
name: "Enabled experiment is exported in metrics",
|
||||
experiments: codersdk.Experiments{
|
||||
codersdk.ExperimentsAll[0],
|
||||
},
|
||||
name: "Enabled experiment is exported in metrics",
|
||||
experiments: codersdk.Experiments{codersdk.ExperimentSharedPorts},
|
||||
expected: map[codersdk.Experiment]float64{
|
||||
codersdk.ExperimentsAll[0]: 1,
|
||||
codersdk.ExperimentSharedPorts: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Disabled experiment is exported in metrics",
|
||||
experiments: codersdk.Experiments{},
|
||||
expected: map[codersdk.Experiment]float64{
|
||||
codersdk.ExperimentsAll[0]: 0,
|
||||
codersdk.ExperimentSharedPorts: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -62,11 +62,9 @@ type metrics struct {
|
||||
|
||||
// if the oauth supports it, rate limit metrics.
|
||||
// rateLimit is the defined limit per interval
|
||||
rateLimit *prometheus.GaugeVec
|
||||
// TODO: remove deprecated metrics in the future release
|
||||
rateLimitDeprecated *prometheus.GaugeVec
|
||||
rateLimitRemaining *prometheus.GaugeVec
|
||||
rateLimitUsed *prometheus.GaugeVec
|
||||
rateLimit *prometheus.GaugeVec
|
||||
rateLimitRemaining *prometheus.GaugeVec
|
||||
rateLimitUsed *prometheus.GaugeVec
|
||||
// rateLimitReset is unix time of the next interval (when the rate limit resets).
|
||||
rateLimitReset *prometheus.GaugeVec
|
||||
// rateLimitResetIn is the time in seconds until the rate limit resets.
|
||||
@@ -93,7 +91,7 @@ func NewFactory(registry prometheus.Registerer) *Factory {
|
||||
rateLimit: factory.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "coderd",
|
||||
Subsystem: "oauth2",
|
||||
Name: "external_requests_rate_limit",
|
||||
Name: "external_requests_rate_limit_total",
|
||||
Help: "The total number of allowed requests per interval.",
|
||||
}, []string{
|
||||
"name",
|
||||
@@ -101,18 +99,6 @@ func NewFactory(registry prometheus.Registerer) *Factory {
|
||||
// Some IDPs have different buckets for different rate limits.
|
||||
"resource",
|
||||
}),
|
||||
// TODO: deprecated: remove in the future
|
||||
// See: https://github.com/coder/coder/issues/12999
|
||||
// Deprecation reason: gauge metrics should avoid suffix `_total``
|
||||
rateLimitDeprecated: factory.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "coderd",
|
||||
Subsystem: "oauth2",
|
||||
Name: "external_requests_rate_limit_total",
|
||||
Help: "DEPRECATED: use coderd_oauth2_external_requests_rate_limit instead",
|
||||
}, []string{
|
||||
"name",
|
||||
"resource",
|
||||
}),
|
||||
rateLimitRemaining: factory.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "coderd",
|
||||
Subsystem: "oauth2",
|
||||
@@ -190,8 +176,6 @@ func (f *Factory) NewGithub(name string, under OAuth2Config) *Config {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this metric in v3
|
||||
f.metrics.rateLimitDeprecated.With(labels).Set(float64(limits.Limit))
|
||||
f.metrics.rateLimit.With(labels).Set(float64(limits.Limit))
|
||||
f.metrics.rateLimitRemaining.With(labels).Set(float64(limits.Remaining))
|
||||
f.metrics.rateLimitUsed.With(labels).Set(float64(limits.Used))
|
||||
|
||||
@@ -246,7 +246,7 @@ func (s *server) heartbeatLoop() {
|
||||
start := s.timeNow()
|
||||
hbCtx, hbCancel := context.WithTimeout(s.lifecycleCtx, s.heartbeatInterval)
|
||||
if err := s.heartbeat(hbCtx); err != nil && !database.IsQueryCanceledError(err) {
|
||||
s.Logger.Warn(hbCtx, "heartbeat failed", slog.Error(err))
|
||||
s.Logger.Error(hbCtx, "heartbeat failed", slog.Error(err))
|
||||
}
|
||||
hbCancel()
|
||||
elapsed := s.timeNow().Sub(start)
|
||||
@@ -467,17 +467,6 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo
|
||||
if err != nil {
|
||||
return nil, failJob(fmt.Sprintf("get owner: %s", err))
|
||||
}
|
||||
ownerGroups, err := s.Database.GetGroupsByOrganizationAndUserID(ctx, database.GetGroupsByOrganizationAndUserIDParams{
|
||||
UserID: owner.ID,
|
||||
OrganizationID: s.OrganizationID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, failJob(fmt.Sprintf("get owner group names: %s", err))
|
||||
}
|
||||
ownerGroupNames := []string{}
|
||||
for _, group := range ownerGroups {
|
||||
ownerGroupNames = append(ownerGroupNames, group.Name)
|
||||
}
|
||||
err = s.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspace.ID), []byte{})
|
||||
if err != nil {
|
||||
return nil, failJob(fmt.Sprintf("publish workspace update: %s", err))
|
||||
@@ -578,7 +567,6 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo
|
||||
WorkspaceOwner: owner.Username,
|
||||
WorkspaceOwnerEmail: owner.Email,
|
||||
WorkspaceOwnerName: owner.Name,
|
||||
WorkspaceOwnerGroups: ownerGroupNames,
|
||||
WorkspaceOwnerOidcAccessToken: workspaceOwnerOIDCAccessToken,
|
||||
WorkspaceId: workspace.ID.String(),
|
||||
WorkspaceOwnerId: owner.ID.String(),
|
||||
@@ -1257,8 +1245,6 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
|
||||
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
|
||||
Now: now,
|
||||
Workspace: workspace,
|
||||
// Allowed to be the empty string.
|
||||
WorkspaceAutostart: workspace.AutostartSchedule.String,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("calculate auto stop: %w", err)
|
||||
@@ -1739,9 +1725,9 @@ func (s *server) regenerateSessionToken(ctx context.Context, user database.User,
|
||||
newkey, sessionToken, err := apikey.Generate(apikey.CreateParams{
|
||||
UserID: user.ID,
|
||||
LoginType: user.LoginType,
|
||||
DefaultLifetime: s.DeploymentValues.SessionDuration.Value(),
|
||||
TokenName: workspaceSessionTokenName(workspace),
|
||||
DefaultLifetime: s.DeploymentValues.Sessions.DefaultDuration.Value(),
|
||||
LifetimeSeconds: int64(s.DeploymentValues.Sessions.MaximumTokenDuration.Value().Seconds()),
|
||||
LifetimeSeconds: int64(s.DeploymentValues.MaxTokenLifetime.Value().Seconds()),
|
||||
})
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("generate API key: %w", err)
|
||||
|
||||
@@ -166,11 +166,7 @@ func TestAcquireJob(t *testing.T) {
|
||||
// Set the max session token lifetime so we can assert we
|
||||
// create an API key with an expiration within the bounds of the
|
||||
// deployment config.
|
||||
dv := &codersdk.DeploymentValues{
|
||||
Sessions: codersdk.SessionLifetime{
|
||||
MaximumTokenDuration: serpent.Duration(time.Hour),
|
||||
},
|
||||
}
|
||||
dv := &codersdk.DeploymentValues{MaxTokenLifetime: serpent.Duration(time.Hour)}
|
||||
gitAuthProvider := &sdkproto.ExternalAuthProviderResource{
|
||||
Id: "github",
|
||||
}
|
||||
@@ -186,15 +182,6 @@ func TestAcquireJob(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
user := dbgen.User(t, db, database.User{})
|
||||
group1 := dbgen.Group(t, db, database.Group{
|
||||
Name: "group1",
|
||||
OrganizationID: pd.OrganizationID,
|
||||
})
|
||||
err := db.InsertGroupMember(ctx, database.InsertGroupMemberParams{
|
||||
UserID: user.ID,
|
||||
GroupID: group1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
link := dbgen.UserLink(t, db, database.UserLink{
|
||||
LoginType: database.LoginTypeOIDC,
|
||||
UserID: user.ID,
|
||||
@@ -323,8 +310,8 @@ func TestAcquireJob(t *testing.T) {
|
||||
require.Len(t, toks, 2, "invalid api key")
|
||||
key, err := db.GetAPIKeyByID(ctx, toks[0])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(dv.Sessions.MaximumTokenDuration.Value().Seconds()), key.LifetimeSeconds)
|
||||
require.WithinDuration(t, time.Now().Add(dv.Sessions.MaximumTokenDuration.Value()), key.ExpiresAt, time.Minute)
|
||||
require.Equal(t, int64(dv.MaxTokenLifetime.Value().Seconds()), key.LifetimeSeconds)
|
||||
require.WithinDuration(t, time.Now().Add(dv.MaxTokenLifetime.Value()), key.ExpiresAt, time.Minute)
|
||||
|
||||
want, err := json.Marshal(&proto.AcquiredJob_WorkspaceBuild_{
|
||||
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
|
||||
@@ -353,7 +340,6 @@ func TestAcquireJob(t *testing.T) {
|
||||
WorkspaceOwnerEmail: user.Email,
|
||||
WorkspaceOwnerName: user.Name,
|
||||
WorkspaceOwnerOidcAccessToken: link.OAuthAccessToken,
|
||||
WorkspaceOwnerGroups: []string{group1.Name},
|
||||
WorkspaceId: workspace.ID.String(),
|
||||
WorkspaceOwnerId: user.ID.String(),
|
||||
TemplateId: template.ID.String(),
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
)
|
||||
|
||||
// NextAutostart takes the workspace and template schedule and returns the next autostart schedule
|
||||
// after "at". The boolean returned is if the autostart should be allowed to start based on the template
|
||||
// schedule.
|
||||
func NextAutostart(at time.Time, wsSchedule string, templateSchedule TemplateScheduleOptions) (time.Time, bool) {
|
||||
sched, err := cron.Weekly(wsSchedule)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// Round down to the nearest minute, as this is the finest granularity cron supports.
|
||||
// Truncate is probably not necessary here, but doing it anyway to be sure.
|
||||
nextTransition := sched.Next(at).Truncate(time.Minute)
|
||||
|
||||
// The nextTransition is when the auto start should kick off. If it lands on a
|
||||
// forbidden day, do not allow the auto start. We use the time location of the
|
||||
// schedule to determine the weekday. So if "Saturday" is disallowed, the
|
||||
// definition of "Saturday" depends on the location of the schedule.
|
||||
zonedTransition := nextTransition.In(sched.Location())
|
||||
allowed := templateSchedule.AutostartRequirement.DaysMap()[zonedTransition.Weekday()]
|
||||
|
||||
return zonedTransition, allowed
|
||||
}
|
||||
@@ -44,11 +44,6 @@ type CalculateAutostopParams struct {
|
||||
Database database.Store
|
||||
TemplateScheduleStore TemplateScheduleStore
|
||||
UserQuietHoursScheduleStore UserQuietHoursScheduleStore
|
||||
// WorkspaceAutostart can be the empty string if no workspace autostart
|
||||
// is configured.
|
||||
// If configured, this is expected to be a cron weekly event parsable
|
||||
// by autobuild.NextAutostart
|
||||
WorkspaceAutostart string
|
||||
|
||||
Now time.Time
|
||||
Workspace database.Workspace
|
||||
@@ -95,14 +90,6 @@ func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (Aut
|
||||
autostop AutostopTime
|
||||
)
|
||||
|
||||
var ttl time.Duration
|
||||
if workspace.Ttl.Valid {
|
||||
// When the workspace is made it copies the template's TTL, and the user
|
||||
// can unset it to disable it (unless the template has
|
||||
// UserAutoStopEnabled set to false, see below).
|
||||
ttl = time.Duration(workspace.Ttl.Int64)
|
||||
}
|
||||
|
||||
if workspace.Ttl.Valid {
|
||||
// When the workspace is made it copies the template's TTL, and the user
|
||||
// can unset it to disable it (unless the template has
|
||||
@@ -117,30 +104,9 @@ func CalculateAutostop(ctx context.Context, params CalculateAutostopParams) (Aut
|
||||
if !templateSchedule.UserAutostopEnabled {
|
||||
// The user is not permitted to set their own TTL, so use the template
|
||||
// default.
|
||||
ttl = 0
|
||||
autostop.Deadline = time.Time{}
|
||||
if templateSchedule.DefaultTTL > 0 {
|
||||
ttl = templateSchedule.DefaultTTL
|
||||
}
|
||||
}
|
||||
|
||||
if ttl > 0 {
|
||||
// Only apply non-zero TTLs.
|
||||
autostop.Deadline = now.Add(ttl)
|
||||
if params.WorkspaceAutostart != "" {
|
||||
// If the deadline passes the next autostart, we need to extend the deadline to
|
||||
// autostart + deadline. ActivityBumpWorkspace already covers this case
|
||||
// when extending the deadline.
|
||||
//
|
||||
// Situation this is solving.
|
||||
// 1. User has workspace with auto-start at 9:00am, 12 hour auto-stop.
|
||||
// 2. Coder stops workspace at 9pm
|
||||
// 3. User starts workspace at 9:45pm.
|
||||
// - The initial deadline is calculated to be 9:45am
|
||||
// - This crosses the autostart deadline, so the deadline is extended to 9pm
|
||||
nextAutostart, ok := NextAutostart(params.Now, params.WorkspaceAutostart, templateSchedule)
|
||||
if ok && autostop.Deadline.After(nextAutostart) {
|
||||
autostop.Deadline = nextAutostart.Add(ttl)
|
||||
}
|
||||
autostop.Deadline = now.Add(templateSchedule.DefaultTTL)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,12 +25,6 @@ func TestCalculateAutoStop(t *testing.T) {
|
||||
|
||||
now := time.Now()
|
||||
|
||||
chicago, err := time.LoadLocation("America/Chicago")
|
||||
require.NoError(t, err, "loading chicago time location")
|
||||
|
||||
// pastDateNight is 9:45pm on a wednesday
|
||||
pastDateNight := time.Date(2024, 2, 14, 21, 45, 0, 0, chicago)
|
||||
|
||||
// Wednesday the 8th of February 2023 at midnight. This date was
|
||||
// specifically chosen as it doesn't fall on a applicable week for both
|
||||
// fortnightly and triweekly autostop requirements.
|
||||
@@ -76,12 +70,8 @@ func TestCalculateAutoStop(t *testing.T) {
|
||||
t.Log("saturdayMidnightAfterDstOut", saturdayMidnightAfterDstOut)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
now time.Time
|
||||
|
||||
wsAutostart string
|
||||
templateAutoStart schedule.TemplateAutostartRequirement
|
||||
|
||||
name string
|
||||
now time.Time
|
||||
templateAllowAutostop bool
|
||||
templateDefaultTTL time.Duration
|
||||
templateAutostopRequirement schedule.TemplateAutostopRequirement
|
||||
@@ -374,115 +364,6 @@ func TestCalculateAutoStop(t *testing.T) {
|
||||
// expectedDeadline is copied from expectedMaxDeadline.
|
||||
expectedMaxDeadline: dstOutQuietHoursExpectedTime,
|
||||
},
|
||||
{
|
||||
// A user expects this workspace to be online from 9am -> 9pm.
|
||||
// So if a deadline is going to land in the middle of this range,
|
||||
// we should bump it to the end.
|
||||
// This is already done on `ActivityBumpWorkspace`, but that requires
|
||||
// activity on the workspace.
|
||||
name: "AutostopCrossAutostartBorder",
|
||||
// Starting at 9:45pm, with the autostart at 9am.
|
||||
now: pastDateNight,
|
||||
templateAllowAutostop: false,
|
||||
templateDefaultTTL: time.Hour * 12,
|
||||
workspaceTTL: time.Hour * 12,
|
||||
// At 9am every morning
|
||||
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *",
|
||||
|
||||
// No quiet hours
|
||||
templateAutoStart: schedule.TemplateAutostartRequirement{
|
||||
// Just allow all days of the week
|
||||
DaysOfWeek: 0b01111111,
|
||||
},
|
||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||
userQuietHoursSchedule: "",
|
||||
|
||||
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 21, 0, 0, 0, chicago),
|
||||
expectedMaxDeadline: time.Time{},
|
||||
errContains: "",
|
||||
},
|
||||
{
|
||||
// Same as AutostopCrossAutostartBorder, but just misses the autostart.
|
||||
name: "AutostopCrossMissAutostartBorder",
|
||||
// Starting at 8:45pm, with the autostart at 9am.
|
||||
now: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day(), 20, 30, 0, 0, chicago),
|
||||
templateAllowAutostop: false,
|
||||
templateDefaultTTL: time.Hour * 12,
|
||||
workspaceTTL: time.Hour * 12,
|
||||
// At 9am every morning
|
||||
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *",
|
||||
|
||||
// No quiet hours
|
||||
templateAutoStart: schedule.TemplateAutostartRequirement{
|
||||
// Just allow all days of the week
|
||||
DaysOfWeek: 0b01111111,
|
||||
},
|
||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||||
userQuietHoursSchedule: "",
|
||||
|
||||
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 8, 30, 0, 0, chicago),
|
||||
expectedMaxDeadline: time.Time{},
|
||||
errContains: "",
|
||||
},
|
||||
{
|
||||
// Same as AutostopCrossAutostartBorderMaxEarlyDeadline with max deadline to limit it.
|
||||
// The autostop deadline is before the autostart threshold.
|
||||
name: "AutostopCrossAutostartBorderMaxEarlyDeadline",
|
||||
// Starting at 9:45pm, with the autostart at 9am.
|
||||
now: pastDateNight,
|
||||
templateAllowAutostop: false,
|
||||
templateDefaultTTL: time.Hour * 12,
|
||||
workspaceTTL: time.Hour * 12,
|
||||
// At 9am every morning
|
||||
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *",
|
||||
|
||||
// No quiet hours
|
||||
templateAutoStart: schedule.TemplateAutostartRequirement{
|
||||
// Just allow all days of the week
|
||||
DaysOfWeek: 0b01111111,
|
||||
},
|
||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
|
||||
// Autostop every day
|
||||
DaysOfWeek: 0b01111111,
|
||||
Weeks: 0,
|
||||
},
|
||||
// 6am quiet hours
|
||||
userQuietHoursSchedule: "CRON_TZ=America/Chicago 0 6 * * *",
|
||||
|
||||
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 6, 0, 0, 0, chicago),
|
||||
expectedMaxDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 6, 0, 0, 0, chicago),
|
||||
errContains: "",
|
||||
},
|
||||
{
|
||||
// Same as AutostopCrossAutostartBorder with max deadline to limit it.
|
||||
// The autostop deadline is after autostart threshold.
|
||||
// So the deadline is > 12 hours, but stops at the max deadline.
|
||||
name: "AutostopCrossAutostartBorderMaxDeadline",
|
||||
// Starting at 9:45pm, with the autostart at 9am.
|
||||
now: pastDateNight,
|
||||
templateAllowAutostop: false,
|
||||
templateDefaultTTL: time.Hour * 12,
|
||||
workspaceTTL: time.Hour * 12,
|
||||
// At 9am every morning
|
||||
wsAutostart: "CRON_TZ=America/Chicago 0 9 * * *",
|
||||
|
||||
// No quiet hours
|
||||
templateAutoStart: schedule.TemplateAutostartRequirement{
|
||||
// Just allow all days of the week
|
||||
DaysOfWeek: 0b01111111,
|
||||
},
|
||||
templateAutostopRequirement: schedule.TemplateAutostopRequirement{
|
||||
// Autostop every day
|
||||
DaysOfWeek: 0b01111111,
|
||||
Weeks: 0,
|
||||
},
|
||||
// 11am quiet hours, yea this is werid case.
|
||||
userQuietHoursSchedule: "CRON_TZ=America/Chicago 0 11 * * *",
|
||||
|
||||
expectedDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 11, 0, 0, 0, chicago),
|
||||
expectedMaxDeadline: time.Date(pastDateNight.Year(), pastDateNight.Month(), pastDateNight.Day()+1, 11, 0, 0, 0, chicago),
|
||||
errContains: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@@ -501,7 +382,6 @@ func TestCalculateAutoStop(t *testing.T) {
|
||||
UserAutostopEnabled: c.templateAllowAutostop,
|
||||
DefaultTTL: c.templateDefaultTTL,
|
||||
AutostopRequirement: c.templateAutostopRequirement,
|
||||
AutostartRequirement: c.templateAutoStart,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
@@ -553,20 +433,11 @@ func TestCalculateAutoStop(t *testing.T) {
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
autostart := sql.NullString{}
|
||||
if c.wsAutostart != "" {
|
||||
autostart = sql.NullString{
|
||||
String: c.wsAutostart,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
workspace := dbgen.Workspace(t, db, database.Workspace{
|
||||
TemplateID: template.ID,
|
||||
OrganizationID: org.ID,
|
||||
OwnerID: user.ID,
|
||||
Ttl: workspaceTTL,
|
||||
AutostartSchedule: autostart,
|
||||
TemplateID: template.ID,
|
||||
OrganizationID: org.ID,
|
||||
OwnerID: user.ID,
|
||||
Ttl: workspaceTTL,
|
||||
})
|
||||
|
||||
autostop, err := schedule.CalculateAutostop(ctx, schedule.CalculateAutostopParams{
|
||||
@@ -575,7 +446,6 @@ func TestCalculateAutoStop(t *testing.T) {
|
||||
UserQuietHoursScheduleStore: userQuietHoursScheduleStore,
|
||||
Now: c.now,
|
||||
Workspace: workspace,
|
||||
WorkspaceAutostart: c.wsAutostart,
|
||||
})
|
||||
if c.errContains != "" {
|
||||
require.Error(t, err)
|
||||
|
||||
+7
-44
@@ -4,14 +4,11 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -26,7 +23,6 @@ import (
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
"github.com/coder/coder/v2/site"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
@@ -345,7 +341,7 @@ type ServerTailnet struct {
|
||||
totalConns *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID uuid.UUID, app appurl.ApplicationURL, wildcardHostname string) *httputil.ReverseProxy {
|
||||
func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID uuid.UUID) *httputil.ReverseProxy {
|
||||
// Rewrite the targetURL's Host to point to the agent's IP. This is
|
||||
// necessary because due to TCP connection caching, each agent needs to be
|
||||
// addressed invidivually. Otherwise, all connections get dialed as
|
||||
@@ -355,46 +351,13 @@ func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID u
|
||||
tgt.Host = net.JoinHostPort(tailnet.IPFromUUID(agentID).String(), port)
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(&tgt)
|
||||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, theErr error) {
|
||||
var (
|
||||
desc = "Failed to proxy request to application: " + theErr.Error()
|
||||
additionalInfo = ""
|
||||
additionalButtonLink = ""
|
||||
additionalButtonText = ""
|
||||
)
|
||||
|
||||
var tlsError tls.RecordHeaderError
|
||||
if (errors.As(theErr, &tlsError) && tlsError.Msg == "first record does not look like a TLS handshake") ||
|
||||
errors.Is(theErr, http.ErrSchemeMismatch) {
|
||||
// If the error is due to an HTTP/HTTPS mismatch, we can provide a
|
||||
// more helpful error message with redirect buttons.
|
||||
switchURL := url.URL{
|
||||
Scheme: dashboardURL.Scheme,
|
||||
}
|
||||
_, protocol, isPort := app.PortInfo()
|
||||
if isPort {
|
||||
targetProtocol := "https"
|
||||
if protocol == "https" {
|
||||
targetProtocol = "http"
|
||||
}
|
||||
app = app.ChangePortProtocol(targetProtocol)
|
||||
|
||||
switchURL.Host = fmt.Sprintf("%s%s", app.String(), strings.TrimPrefix(wildcardHostname, "*"))
|
||||
additionalButtonLink = switchURL.String()
|
||||
additionalButtonText = fmt.Sprintf("Switch to %s", strings.ToUpper(targetProtocol))
|
||||
additionalInfo += fmt.Sprintf("This error seems to be due to an app protocol mismatch, try switching to %s.", strings.ToUpper(targetProtocol))
|
||||
}
|
||||
}
|
||||
|
||||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
site.RenderStaticErrorPage(w, r, site.ErrorPageData{
|
||||
Status: http.StatusBadGateway,
|
||||
Title: "Bad Gateway",
|
||||
Description: desc,
|
||||
RetryEnabled: true,
|
||||
DashboardURL: dashboardURL.String(),
|
||||
AdditionalInfo: additionalInfo,
|
||||
AdditionalButtonLink: additionalButtonLink,
|
||||
AdditionalButtonText: additionalButtonText,
|
||||
Status: http.StatusBadGateway,
|
||||
Title: "Bad Gateway",
|
||||
Description: "Failed to proxy request to application: " + err.Error(),
|
||||
RetryEnabled: true,
|
||||
DashboardURL: dashboardURL.String(),
|
||||
})
|
||||
}
|
||||
proxy.Director = s.director(agentID, proxy.Director)
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/coder/coder/v2/agent/agenttest"
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
@@ -82,7 +81,7 @@ func TestServerTailnet_ReverseProxy_ProxyEnv(t *testing.T) {
|
||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
@@ -113,7 +112,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
@@ -144,7 +143,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
@@ -178,7 +177,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
@@ -223,7 +222,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||
u, err := url.Parse("http://127.0.0.1" + port)
|
||||
require.NoError(t, err)
|
||||
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
rw := httptest.NewRecorder()
|
||||
@@ -280,7 +279,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, ag := range agents {
|
||||
rp := serverTailnet.ReverseProxy(u, u, ag.id, appurl.ApplicationURL{}, "")
|
||||
rp := serverTailnet.ReverseProxy(u, u, ag.id)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
@@ -318,7 +317,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||
uri, err := url.Parse(s.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
rp := serverTailnet.ReverseProxy(uri, uri, a.id, appurl.ApplicationURL{}, "")
|
||||
rp := serverTailnet.ReverseProxy(uri, uri, a.id)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
@@ -348,7 +347,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
|
||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
|
||||
require.NoError(t, err)
|
||||
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
|
||||
rp := serverTailnet.ReverseProxy(u, u, a.id)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
|
||||
@@ -624,7 +624,6 @@ func ConvertWorkspaceResource(resource database.WorkspaceResource) WorkspaceReso
|
||||
return WorkspaceResource{
|
||||
ID: resource.ID,
|
||||
JobID: resource.JobID,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
Transition: resource.Transition,
|
||||
Type: resource.Type,
|
||||
InstanceType: resource.InstanceType.String,
|
||||
@@ -834,7 +833,6 @@ type User struct {
|
||||
|
||||
type WorkspaceResource struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
Transition database.WorkspaceTransition `json:"transition"`
|
||||
Type string `json:"type"`
|
||||
|
||||
+2
-3
@@ -252,7 +252,7 @@ func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
|
||||
UserID: user.ID,
|
||||
LoginType: database.LoginTypePassword,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
|
||||
DefaultLifetime: api.DeploymentValues.SessionDuration.Value(),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(ctx, "unable to create API key", slog.Error(err))
|
||||
@@ -472,7 +472,6 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AuthMethods{
|
||||
TermsOfServiceURL: api.DeploymentValues.TermsOfServiceURL.Value(),
|
||||
Password: codersdk.AuthMethod{
|
||||
Enabled: !api.DeploymentValues.DisablePasswordAuth.Value(),
|
||||
},
|
||||
@@ -1613,7 +1612,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
|
||||
cookie, newKey, err := api.createAPIKey(dbauthz.AsSystemRestricted(ctx), apikey.CreateParams{
|
||||
UserID: user.ID,
|
||||
LoginType: params.LoginType,
|
||||
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
|
||||
DefaultLifetime: api.DeploymentValues.SessionDuration.Value(),
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -19,7 +19,11 @@ func TestPostWorkspaceAgentPortShare(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
|
||||
dep := coderdtest.DeploymentValues(t)
|
||||
dep.Experiments = append(dep.Experiments, string(codersdk.ExperimentSharedPorts))
|
||||
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
DeploymentValues: dep,
|
||||
})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
|
||||
@@ -136,7 +140,11 @@ func TestGetWorkspaceAgentPortShares(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
|
||||
dep := coderdtest.DeploymentValues(t)
|
||||
dep.Experiments = append(dep.Experiments, string(codersdk.ExperimentSharedPorts))
|
||||
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
DeploymentValues: dep,
|
||||
})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
|
||||
@@ -172,7 +180,11 @@ func TestDeleteWorkspaceAgentPortShare(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
ownerClient, db := coderdtest.NewWithDatabase(t, nil)
|
||||
dep := coderdtest.DeploymentValues(t)
|
||||
dep.Experiments = append(dep.Experiments, string(codersdk.ExperimentSharedPorts))
|
||||
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
DeploymentValues: dep,
|
||||
})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"cdr.dev/slog"
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/agentapi"
|
||||
"github.com/coder/coder/v2/coderd/autobuild"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
@@ -36,7 +37,6 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/prometheusmetrics"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"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/codersdk/workspacesdk"
|
||||
@@ -1132,7 +1132,6 @@ func convertScripts(dbScripts []database.WorkspaceAgentScript) []codersdk.Worksp
|
||||
// @Param request body agentsdk.Stats true "Stats request"
|
||||
// @Success 200 {object} agentsdk.StatsResponse
|
||||
// @Router /workspaceagents/me/report-stats [post]
|
||||
// @Deprecated Uses agent API v2 endpoint instead.
|
||||
func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -1186,7 +1185,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
|
||||
slog.Error(err),
|
||||
)
|
||||
} else {
|
||||
next, allowed := schedule.NextAutostart(time.Now(), workspace.AutostartSchedule.String, templateSchedule)
|
||||
next, allowed := autobuild.NextAutostartSchedule(time.Now(), workspace.AutostartSchedule.String, templateSchedule)
|
||||
if allowed {
|
||||
nextAutostart = next
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user