Compare commits

..

10 Commits

Author SHA1 Message Date
Colin Adler 84ec2dc1db chore: update git to v2.43.4
Fixes 3 CVEs recently reported:
  - [CVE-2024-32002](https://avd.aquasec.com/nvd/2024/cve-2024-32002/)
  - [CVE-2024-32004](https://avd.aquasec.com/nvd/2024/cve-2024-32004/)
  - [CVE-2024-32465](https://avd.aquasec.com/nvd/2024/cve-2024-32465/)
2024-05-22 19:36:30 +00:00
Stephen Kirby a11b169029 v2.10.2 changelog 2024-04-22 20:27:37 +00:00
Kyle Carberry 2a98123701 chore: add generate script for azure instance identity (#13028)
* chore: add generate script for azure instance identity

This also adds new issuing certificates from:
https://learn.microsoft.com/en-us/azure/security/fundamentals/azure-ca-details?tabs=certificate-authority-chains

* Fix shell lint

* Fix shell fmt

* Fix RSA issuing certificate
2024-04-22 19:43:21 +00:00
Stephen Kirby 2ed7226e85 added mainline disclaimer 2024-04-17 22:38:41 +00:00
Stephen Kirby 2101dbce03 updated version flags in kube install 2024-04-17 22:35:02 +00:00
Stephen Kirby cdeba67944 v2.10.1 changelog 2024-04-17 22:33:20 +00:00
Dean Sheather bda13a2818 fix: make terminal raw in ssh command on windows (#12990)
(cherry picked from commit d426569d4a)
2024-04-17 21:01:58 +00:00
Spike Curtis 353888a5d8 feat: add src_id and dst_id indexes to tailnet_tunnels (#12911)
Fixes #12780

Adds indexes to the `tailnet_tunnels` table to speed up `GetTailnetTunnelPeerIDs` and `GetTailnetTunnelPeerBindings` queries, which match on `src_id` and `dst_id`.

(cherry picked from commit a231b5aef5)
2024-04-17 21:01:39 +00:00
Spike Curtis 3fc6111994 fix: stop sending DeleteTailnetPeer when coordinator is unhealthy (#12925)
fixes #12923

Prevents Coordinate peer connections from generating spurious database queries like DeleteTailnetPeer when the coordinator is unhealthy.

It does this by checking the health of the querier before accepting a connection, rather than unconditionally accepting it only for it to get swatted down later.

(cherry picked from commit 06eae954c9)
2024-04-17 21:01:24 +00:00
Colin Adler 3eb9abcbd3 fix(coderd): prevent agent reverse proxy from using HTTP[S]_PROXY envs (#12875)
Updates https://github.com/coder/coder/issues/12790

(cherry picked from commit a2b28f80d7)
2024-04-17 21:01:12 +00:00
382 changed files with 3377 additions and 11814 deletions
+1 -1
View File
@@ -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 }}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+43
View File
@@ -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
View File
@@ -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"
-3
View File
@@ -17,9 +17,6 @@
},
{
"pattern": "tailscale.com"
},
{
"pattern": "wireguard.com"
}
],
"aliveStatusCodes": [200, 0]
+14 -91
View File
@@ -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
-1
View File
@@ -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 = [
-5
View File
@@ -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:
+4 -13
View File
@@ -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
-6
View File
@@ -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
View File
@@ -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.
+4 -9
View File
@@ -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
View File
@@ -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
-62
View File
@@ -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()
+9 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+4 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
)
-43
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
-3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+2 -1
View File
@@ -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
}
+17 -59
View File
@@ -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",
+21 -53
View File
@@ -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
View File
@@ -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,
)
}
+4 -4
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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:
+25 -1
View File
@@ -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 {
+1 -7
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
}
+14 -5
View File
@@ -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
+6 -8
View File
@@ -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()})
+20 -42
View File
@@ -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 {
+38 -79
View File
@@ -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
}
+23 -31
View File
@@ -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) {
+15 -15
View File
@@ -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()
+17 -30
View File
@@ -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
}
}
+9 -70
View File
@@ -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 {
+2 -8
View File
@@ -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,
+14 -79
View File
@@ -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()
}
+1 -1
View File
@@ -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 (
+81 -4
View File
@@ -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)
}
-1
View File
@@ -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;
+7 -15
View File
@@ -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)
}
+1 -1
View File
@@ -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
+89 -90
View File
@@ -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 (
-25
View File
@@ -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,
+25 -29
View File
@@ -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
View File
@@ -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,
})
}
}
+9 -41
View File
@@ -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()
-79
View File
@@ -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...)...,
+1 -24
View File
@@ -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,
},
}
)
+1 -162
View File
@@ -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 {
-32
View File
@@ -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
-32
View File
@@ -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)
})
}
}
+124 -256
View File
@@ -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,
+1 -1
View File
@@ -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")
+16 -15
View File
@@ -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,
})
+7 -6
View File
@@ -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
}
+8 -20
View File
@@ -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
View File
@@ -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.
+1 -16
View File
@@ -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,
},
},
{
+4 -20
View File
@@ -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(),
-30
View File
@@ -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
}
+2 -36
View File
@@ -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)
}
}
+6 -136
View File
@@ -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
View File
@@ -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)
+8 -9
View File
@@ -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(
-2
View File
@@ -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
View File
@@ -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 {
+15 -3
View File
@@ -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)
+2 -3
View File
@@ -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