Compare commits
318 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d65eea8132 | |||
| 2f0c2d77dc | |||
| 82ed9e4dc7 | |||
| 534d4ea752 | |||
| 8ce8700424 | |||
| b9779af5b2 | |||
| e54ff57a9a | |||
| ae220f52e7 | |||
| 90f82da311 | |||
| 201cb1cbed | |||
| b701620a01 | |||
| 0703fc6888 | |||
| a9e5648557 | |||
| 3fbfb534d0 | |||
| 5e69a9d18b | |||
| ba0bf43de4 | |||
| 40af6206cc | |||
| b248f125e1 | |||
| de8149fbfd | |||
| 19530c6b44 | |||
| 4758952ebc | |||
| bee4ece1b9 | |||
| 7569cccc51 | |||
| 59ab5053b1 | |||
| e176867d77 | |||
| 7cc96f5d40 | |||
| 7a7bef0dab | |||
| a1671a633c | |||
| 6730c24c58 | |||
| 5aea80381c | |||
| 9eb797eb5a | |||
| 5fb231774c | |||
| 9ae825ebae | |||
| 374f0a0fd1 | |||
| bc8126fa45 | |||
| 5789ea5397 | |||
| afd9d3b35f | |||
| b69f6358f0 | |||
| cca3cb1c55 | |||
| b7edf5bbc7 | |||
| 84b3121777 | |||
| a551aa51ab | |||
| ec78f54941 | |||
| ef4ed64a29 | |||
| 02c36868b2 | |||
| 7ea510e091 | |||
| 18692058a9 | |||
| 5a8a254c93 | |||
| 00f6cfe3cf | |||
| 9299e9f6ba | |||
| e5d848f19d | |||
| 1edd46dd5f | |||
| 762cb84f4a | |||
| 6293c33746 | |||
| 5b78ec97b6 | |||
| 79d73f77f5 | |||
| a1d3b82dd1 | |||
| 47f8f5d963 | |||
| 60224fa216 | |||
| 87dd878779 | |||
| ff617cc545 | |||
| a0962ba089 | |||
| e5bb0a7a00 | |||
| 1b4ca00428 | |||
| d748c6d718 | |||
| 98fa823c79 | |||
| b43344b672 | |||
| c67eba10d5 | |||
| c2837a62e4 | |||
| fa9edc1f42 | |||
| a40e954afc | |||
| 3364abecdd | |||
| ed6ee9aaa8 | |||
| 390ff9ac05 | |||
| 7ea4a89a20 | |||
| 78deaba481 | |||
| f27f5c0002 | |||
| 3f1e9c038a | |||
| 0a86d6d176 | |||
| c61b64be61 | |||
| 8e78b9495d | |||
| 273209432d | |||
| b8b80fe6d2 | |||
| 45b45f1107 | |||
| a63d427efd | |||
| 4af0f093ee | |||
| d8bb5a05db | |||
| f176ff532f | |||
| f23d4802b5 | |||
| f66d0445da | |||
| 0998cedb5c | |||
| 92c5dfa266 | |||
| 80538c079d | |||
| ad8c314130 | |||
| 85de0e966d | |||
| cf91eff7cf | |||
| 194be12133 | |||
| a0fce363cd | |||
| 63e06853eb | |||
| 114fb31fbb | |||
| fc6f18aa96 | |||
| 1f5788feff | |||
| cb6b5e8fbd | |||
| f14927955d | |||
| a8a0be98b8 | |||
| 721ab2a1b4 | |||
| 2b29559984 | |||
| 9ced001570 | |||
| ebee9288ae | |||
| a5a64948cd | |||
| 8412450ae3 | |||
| c41d0efff9 | |||
| 7358c1b1ac | |||
| 4e7381341f | |||
| 228b99d9c2 | |||
| f13b1c9af6 | |||
| 5ddbeddf85 | |||
| 3d707cbe5a | |||
| ee817b4d80 | |||
| c557c25b3d | |||
| 82c1562f82 | |||
| 8c9560ddb8 | |||
| 7eb228e3ff | |||
| 6182ee90f0 | |||
| 989575c5b6 | |||
| 4671ebb330 | |||
| e14f8fb64b | |||
| 679099373b | |||
| d8e0be6ee6 | |||
| a4bd50c985 | |||
| 1832a755e1 | |||
| 35cb572888 | |||
| 24448e79fe | |||
| c73d5a2617 | |||
| 06dd656e08 | |||
| b7a921a2bf | |||
| 30227dae97 | |||
| 96f2cec541 | |||
| 3905e2c541 | |||
| 421c0d1242 | |||
| 677be9aab2 | |||
| 72f2efe048 | |||
| 5e8f97d8c3 | |||
| b56c9c438f | |||
| 6f5c183c80 | |||
| 3e3118794f | |||
| 05facc971b | |||
| e7c87a806b | |||
| dfd27f559e | |||
| deee9492e3 | |||
| 619ec927e9 | |||
| e76b595052 | |||
| d51c6912a7 | |||
| 2efb46a10e | |||
| 7c3ec51997 | |||
| 3e77f5b512 | |||
| d956af0a3a | |||
| 886a97b425 | |||
| 13dd526f11 | |||
| b20c63c185 | |||
| 060f023174 | |||
| 205c43da99 | |||
| a3c23ed313 | |||
| 34a3bdc4ec | |||
| 09f00c08df | |||
| 94a3e3a563 | |||
| 7873c961e3 | |||
| ed0ca76b0b | |||
| 7779c0a1dc | |||
| 699e187d55 | |||
| 565b45deba | |||
| c550d0641d | |||
| c2cb0e9fe2 | |||
| 3de737fdc8 | |||
| 93d8812284 | |||
| 845407fe7a | |||
| 71a03a8b1d | |||
| f2dd0a8e5d | |||
| 3ff9cef498 | |||
| 53f7e9e0a1 | |||
| 47993e3fcf | |||
| d302570091 | |||
| 4e5960660e | |||
| fbb98b950a | |||
| 1bda8a0856 | |||
| 0e3dc2a80f | |||
| 053c56cc1a | |||
| ed07921752 | |||
| 4a83e84a23 | |||
| f2a21c604b | |||
| 74b921cf81 | |||
| 1b3185c047 | |||
| 8269124ab7 | |||
| 15157c1c40 | |||
| 73ba36c9d2 | |||
| 8ba05a9052 | |||
| 848ea7e9f1 | |||
| f1ef9fd673 | |||
| d50a31ef62 | |||
| 365231b1e5 | |||
| 74f27719b8 | |||
| 341114a020 | |||
| 99dda4a43a | |||
| c24b562199 | |||
| 46dced9cfe | |||
| c933c75aa7 | |||
| b82a782619 | |||
| a6af7a5e3d | |||
| 3f21cb8a2f | |||
| dd27a8a634 | |||
| 39ccff97c1 | |||
| 8b6227d031 | |||
| a518047f10 | |||
| 4682355eed | |||
| 5780050493 | |||
| a04c76ce40 | |||
| 215dd7b152 | |||
| a69fc657f2 | |||
| 2f7f9d022a | |||
| e57ca3cdaa | |||
| 81fcdf717b | |||
| fab5591cf6 | |||
| d3f3ace220 | |||
| 8d1220e0c8 | |||
| 7bd1b3bdb8 | |||
| 3af317317a | |||
| 2e49fa94d4 | |||
| ea472c5388 | |||
| d2acb6776e | |||
| 3adcccb618 | |||
| 8a1216254e | |||
| e17e8aa3c9 | |||
| 4a6693a171 | |||
| b40f54f603 | |||
| 3d7740bd32 | |||
| 3aa0d73811 | |||
| 319fd5bf1d | |||
| 75223dfd8b | |||
| f5a32b3f27 | |||
| d426569d4a | |||
| 92190443ff | |||
| 6b4eb03192 | |||
| 3338cdca77 | |||
| 227e632053 | |||
| b85d5d8491 | |||
| cb8c576c93 | |||
| ee7dda8111 | |||
| 0c993566dd | |||
| 80f5978124 | |||
| 777dfbe965 | |||
| 942e90270e | |||
| 231fc26c92 | |||
| ba52a4fbe2 | |||
| 8e1e0f04a4 | |||
| b598aef543 | |||
| 407e61ecd4 | |||
| 06e042acfa | |||
| 41ca6e4f7f | |||
| 3ab5a51ec2 | |||
| 49689162bb | |||
| 9a4703a311 | |||
| c13909a1a2 | |||
| d9da054c9d | |||
| 7cf8577f1c | |||
| d3790bb5be | |||
| 00fcf36999 | |||
| cf2d2a98bd | |||
| b71af32113 | |||
| dcf1d3a9ae | |||
| b163bc7f01 | |||
| c5367c201b | |||
| 93b46fe1f6 | |||
| 2ad7fcc0b7 | |||
| 22785a307c | |||
| b9936a4671 | |||
| fad97a14f9 | |||
| a231b5aef5 | |||
| ab116af543 | |||
| 8da8b89af7 | |||
| e801e878ba | |||
| 9cf2358114 | |||
| 7fd9a75ad9 | |||
| 566f8f231d | |||
| 06eae954c9 | |||
| a607d5610e | |||
| 838e8df5be | |||
| 4dc293d930 | |||
| e266ecf91b | |||
| acaa254099 | |||
| 2f2a395ba9 | |||
| b6359b0a89 | |||
| 5469011018 | |||
| 0a8c8ce5cc | |||
| 1d4bf30c0d | |||
| 189b8626d0 | |||
| 08451ce80c | |||
| 0178bfe134 | |||
| 28754a79e5 | |||
| d82f2fd416 | |||
| 7179c86df3 | |||
| 11123018a2 | |||
| 589434e8d8 | |||
| 9a7d8034cb | |||
| f99fd807b1 | |||
| 8ba8ec2f19 | |||
| 24135a2d0f | |||
| 3b7380fa00 | |||
| f96ce80ab9 | |||
| c4b26f335a | |||
| a2b28f80d7 | |||
| b06452ee88 | |||
| 7c0fac9906 | |||
| c243210ae5 | |||
| 61e5721caa | |||
| 3fbcdb0ddc | |||
| bc9ea61eb4 | |||
| 90efa1b846 | |||
| 41b8ff3e81 |
@@ -4,12 +4,12 @@ description: |
|
||||
inputs:
|
||||
version:
|
||||
description: "The Go version to use."
|
||||
default: "1.21.9"
|
||||
default: "1.22.3"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: buildjet/setup-go@v4
|
||||
uses: buildjet/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ inputs.version }}
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
- name: Setup Node
|
||||
uses: buildjet/setup-node@v3
|
||||
uses: buildjet/setup-node@v4.0.1
|
||||
with:
|
||||
node-version: 18.19.0
|
||||
# See https://github.com/actions/setup-node#caching-global-packages-data
|
||||
|
||||
@@ -7,5 +7,5 @@ runs:
|
||||
- name: Install Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: 1.5.7
|
||||
terraform_version: 1.8.4
|
||||
terraform_wrapper: false
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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
|
||||
@@ -112,17 +112,3 @@ updates:
|
||||
offlinedocs:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
# Update dogfood.
|
||||
- package-ecosystem: "terraform"
|
||||
directory: "/dogfood/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
time: "06:00"
|
||||
timezone: "America/Chicago"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
labels: []
|
||||
ignore:
|
||||
# We likely want to update this ourselves.
|
||||
- dependency-name: "coder/coder"
|
||||
|
||||
@@ -86,6 +86,7 @@ provider "kubernetes" {
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
os = "linux"
|
||||
@@ -175,21 +176,21 @@ resource "coder_app" "code-server" {
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "home" {
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}-home"
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-pvc"
|
||||
"app.kubernetes.io/instance" = "coder-pvc-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}"
|
||||
"app.kubernetes.io/instance" = "coder-pvc-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
"app.kubernetes.io/part-of" = "coder"
|
||||
//Coder-specific labels.
|
||||
"com.coder.resource" = "true"
|
||||
"com.coder.workspace.id" = data.coder_workspace.me.id
|
||||
"com.coder.workspace.name" = data.coder_workspace.me.name
|
||||
"com.coder.user.id" = data.coder_workspace.me.owner_id
|
||||
"com.coder.user.username" = data.coder_workspace.me.owner
|
||||
"com.coder.user.id" = data.coder_workspace_owner.me.id
|
||||
"com.coder.user.username" = data.coder_workspace_owner.me.name
|
||||
}
|
||||
annotations = {
|
||||
"com.coder.user.email" = data.coder_workspace.me.owner_email
|
||||
"com.coder.user.email" = data.coder_workspace_owner.me.email
|
||||
}
|
||||
}
|
||||
wait_until_bound = false
|
||||
@@ -210,20 +211,20 @@ resource "kubernetes_deployment" "main" {
|
||||
]
|
||||
wait_for_rollout = false
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}"
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-workspace"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
"app.kubernetes.io/part-of" = "coder"
|
||||
"com.coder.resource" = "true"
|
||||
"com.coder.workspace.id" = data.coder_workspace.me.id
|
||||
"com.coder.workspace.name" = data.coder_workspace.me.name
|
||||
"com.coder.user.id" = data.coder_workspace.me.owner_id
|
||||
"com.coder.user.username" = data.coder_workspace.me.owner
|
||||
"com.coder.user.id" = data.coder_workspace_owner.me.id
|
||||
"com.coder.user.username" = data.coder_workspace_owner.me.name
|
||||
}
|
||||
annotations = {
|
||||
"com.coder.user.email" = data.coder_workspace.me.owner_email
|
||||
"com.coder.user.email" = data.coder_workspace_owner.me.email
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+125
-76
@@ -37,8 +37,10 @@ jobs:
|
||||
k8s: ${{ steps.filter.outputs.k8s }}
|
||||
ci: ${{ steps.filter.outputs.ci }}
|
||||
db: ${{ steps.filter.outputs.db }}
|
||||
gomod: ${{ steps.filter.outputs.gomod }}
|
||||
offlinedocs-only: ${{ steps.filter.outputs.offlinedocs_count == steps.filter.outputs.all_count }}
|
||||
offlinedocs: ${{ steps.filter.outputs.offlinedocs }}
|
||||
tailnet-integration: ${{ steps.filter.outputs.tailnet-integration }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -90,6 +92,9 @@ jobs:
|
||||
- "scaletest/**"
|
||||
- "tailnet/**"
|
||||
- "testutil/**"
|
||||
gomod:
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
ts:
|
||||
- "site/**"
|
||||
- "Makefile"
|
||||
@@ -103,11 +108,34 @@ jobs:
|
||||
- ".github/workflows/ci.yaml"
|
||||
offlinedocs:
|
||||
- "offlinedocs/**"
|
||||
tailnet-integration:
|
||||
- "tailnet/**"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
|
||||
- id: debug
|
||||
run: |
|
||||
echo "${{ toJSON(steps.filter )}}"
|
||||
|
||||
update-flake:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.gomod == 'true'
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Update Nix Flake SRI Hash
|
||||
run: ./scripts/update-flake.sh
|
||||
|
||||
- name: Ensure No Changes
|
||||
run: git diff --exit-code
|
||||
|
||||
lint:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.offlinedocs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
|
||||
@@ -142,7 +170,7 @@ jobs:
|
||||
|
||||
# Check for any typos
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.19.0
|
||||
uses: crate-ci/typos@v1.21.0
|
||||
with:
|
||||
config: .github/workflows/typos.toml
|
||||
|
||||
@@ -183,6 +211,9 @@ jobs:
|
||||
- name: Setup sqlc
|
||||
uses: ./.github/actions/setup-sqlc
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
|
||||
- name: go install tools
|
||||
run: |
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
|
||||
@@ -269,16 +300,6 @@ 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?
|
||||
@@ -297,7 +318,7 @@ jobs:
|
||||
fi
|
||||
export TS_DEBUG_DISCO=true
|
||||
gotestsum --junitfile="gotests.xml" --jsonfile="gotests.json" \
|
||||
--packages="./..." -- $PARALLEL_FLAG -short -failfast $COVERAGE_FLAGS
|
||||
--packages="./..." -- $PARALLEL_FLAG -short -failfast
|
||||
|
||||
- name: Upload test stats to Datadog
|
||||
timeout-minutes: 1
|
||||
@@ -307,19 +328,6 @@ 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:
|
||||
@@ -355,19 +363,6 @@ 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
|
||||
@@ -397,6 +392,34 @@ jobs:
|
||||
with:
|
||||
api-key: ${{ secrets.DATADOG_API_KEY }}
|
||||
|
||||
# Tailnet integration tests only run when the `tailnet` directory or `go.sum`
|
||||
# and `go.mod` are changed. These tests are to ensure we don't add regressions
|
||||
# to tailnet, either due to our code or due to updating dependencies.
|
||||
#
|
||||
# These tests are skipped in the main go test jobs because they require root
|
||||
# and mess with networking.
|
||||
test-go-tailnet-integration:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
needs: changes
|
||||
# Unnecessary to run on main for now
|
||||
if: needs.changes.outputs.tailnet-integration == 'true' || needs.changes.outputs.ci == 'true'
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
# Used by some integration tests.
|
||||
- name: Install Nginx
|
||||
run: sudo apt-get update && sudo apt-get install -y nginx
|
||||
|
||||
- name: Run Tests
|
||||
run: make test-tailnet-integration
|
||||
|
||||
test-js:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
needs: changes
|
||||
@@ -414,24 +437,20 @@ 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
|
||||
@@ -444,52 +463,40 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
# Assume that the checked-in versions are up-to-date
|
||||
- run: make gen/mark-fresh
|
||||
name: make gen
|
||||
|
||||
- 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 build
|
||||
working-directory: site
|
||||
|
||||
- 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
|
||||
name: failed-test-videos${{ matrix.variant.enterprise && '-enterprise' || '-agpl' }}
|
||||
path: ./site/test-results/**/*.webm
|
||||
retention-days: 7
|
||||
|
||||
@@ -497,7 +504,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
|
||||
name: debug-pprof-dumps${{ matrix.variant.enterprise && '-enterprise' || '-agpl' }}
|
||||
path: ./site/test-results/**/debug-pprof-*.txt
|
||||
retention-days: 7
|
||||
|
||||
@@ -643,6 +650,7 @@ 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()
|
||||
@@ -659,6 +667,7 @@ 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.
|
||||
@@ -899,3 +908,43 @@ 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' && github.actor != 'dependabot[bot]'
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
id: review
|
||||
uses: actions/dependency-review-action@v4.3.2
|
||||
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/coder/wgtunnel@0.1.13-0.20240522110300-ade90dfb2da0"
|
||||
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"
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
steps:
|
||||
- name: cla
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
uses: contributor-assistant/github-action@v2.3.2
|
||||
uses: contributor-assistant/github-action@v2.4.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
|
||||
@@ -8,6 +8,11 @@ on:
|
||||
- scripts/Dockerfile.base
|
||||
- scripts/Dockerfile
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- scripts/Dockerfile.base
|
||||
- .github/workflows/docker-base.yaml
|
||||
|
||||
schedule:
|
||||
# Run every week at 09:43 on Monday, Wednesday and Friday. We build this
|
||||
# frequently to ensure that packages are up-to-date.
|
||||
@@ -57,11 +62,12 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
pull: true
|
||||
no-cache: true
|
||||
push: true
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
ghcr.io/coder/coder-base:latest
|
||||
|
||||
- name: Verify that images are pushed properly
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
# retry 10 times with a 5 second delay as the images may not be
|
||||
# available immediately
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
},
|
||||
{
|
||||
"pattern": "tailscale.com"
|
||||
},
|
||||
{
|
||||
"pattern": "wireguard.com"
|
||||
}
|
||||
],
|
||||
"aliveStatusCodes": [200, 0]
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
# 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
|
||||
@@ -28,6 +33,8 @@ 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:
|
||||
@@ -62,21 +69,45 @@ jobs:
|
||||
echo "CODER_FORCE_VERSION=$version" >> $GITHUB_ENV
|
||||
echo "$version"
|
||||
|
||||
- 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"
|
||||
# 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
|
||||
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)"
|
||||
./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" > "$release_notes_file"
|
||||
echo CODER_RELEASE_NOTES_FILE="$release_notes_file" >> $GITHUB_ENV
|
||||
|
||||
- name: Show release notes
|
||||
@@ -97,6 +128,13 @@ 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
|
||||
|
||||
@@ -130,6 +168,32 @@ 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
|
||||
@@ -144,16 +208,26 @@ 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: |
|
||||
@@ -261,6 +335,9 @@ 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
|
||||
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
echo "image=$(cat "$image_job")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55
|
||||
uses: aquasecurity/trivy-action@fd25fed6972e341ff0007ddb61f77e88103953c2
|
||||
with:
|
||||
image-ref: ${{ steps.build.outputs.image }}
|
||||
format: sarif
|
||||
|
||||
@@ -15,6 +15,7 @@ Hashi = "Hashi"
|
||||
trialer = "trialer"
|
||||
encrypter = "encrypter"
|
||||
hel = "hel" # as in helsinki
|
||||
pn = "pn" # this is used as proto node
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
@@ -32,4 +33,5 @@ extend-exclude = [
|
||||
"**/pnpm-lock.yaml",
|
||||
"tailnet/testdata/**",
|
||||
"site/src/pages/SetupPage/countries.tsx",
|
||||
"provisioner/terraform/testdata/**",
|
||||
]
|
||||
|
||||
@@ -4,6 +4,11 @@ on:
|
||||
schedule:
|
||||
- cron: "0 9 * * 1"
|
||||
workflow_dispatch: # allows to run manually for testing
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**"
|
||||
|
||||
jobs:
|
||||
check-docs:
|
||||
@@ -24,7 +29,7 @@ jobs:
|
||||
file-path: "./README.md"
|
||||
|
||||
- name: Send Slack notification
|
||||
if: failure() && github.event_name != 'workflow_dispatch'
|
||||
if: failure() && github.event_name == 'schedule'
|
||||
run: |
|
||||
curl -X POST -H 'Content-type: application/json' -d '{"msg":"Broken links found in the documentation. Please check the logs at ${{ env.LOGS_URL }}"}' ${{ secrets.DOCS_LINK_SLACK_WEBHOOK }}
|
||||
echo "Sent Slack notification"
|
||||
|
||||
Vendored
+3
-2
@@ -195,7 +195,6 @@
|
||||
"**.pb.go": true,
|
||||
"**/*.gen.json": true,
|
||||
"**/testdata/*": true,
|
||||
"**Generated.ts": true,
|
||||
"coderd/apidoc/**": true,
|
||||
"docs/api/*.md": true,
|
||||
"docs/templates/*.md": true,
|
||||
@@ -222,5 +221,7 @@
|
||||
"go.testFlags": ["-short", "-coverpkg=./..."],
|
||||
// We often use a version of TypeScript that's ahead of the version shipped
|
||||
// with VS Code.
|
||||
"typescript.tsdk": "./site/node_modules/typescript/lib"
|
||||
"typescript.tsdk": "./site/node_modules/typescript/lib",
|
||||
// Playwright tests in VSCode will open a browser to live "view" the test.
|
||||
"playwright.reuseBrowser": true
|
||||
}
|
||||
|
||||
@@ -56,6 +56,9 @@ GO_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.go' -not -nam
|
||||
# All the shell files in the repo, excluding ignored files.
|
||||
SHELL_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.sh')
|
||||
|
||||
# Ensure we don't use the user's git configs which might cause side-effects
|
||||
GIT_FLAGS = GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
|
||||
|
||||
# All ${OS}_${ARCH} combos we build for. Windows binaries have the .exe suffix.
|
||||
OS_ARCHES := \
|
||||
linux_amd64 linux_arm64 linux_armv7 \
|
||||
@@ -200,7 +203,8 @@ endef
|
||||
# calling this manually.
|
||||
$(CODER_ALL_BINARIES): go.mod go.sum \
|
||||
$(GO_SRC_FILES) \
|
||||
$(shell find ./examples/templates)
|
||||
$(shell find ./examples/templates) \
|
||||
site/static/error.html
|
||||
|
||||
$(get-mode-os-arch-ext)
|
||||
if [[ "$$os" != "windows" ]] && [[ "$$ext" != "" ]]; then
|
||||
@@ -382,9 +386,9 @@ install: build/coder_$(VERSION)_$(GOOS)_$(GOARCH)$(GOOS_BIN_EXT)
|
||||
cp "$<" "$$output_file"
|
||||
.PHONY: install
|
||||
|
||||
BOLD := $(shell tput bold)
|
||||
GREEN := $(shell tput setaf 2)
|
||||
RESET := $(shell tput sgr0)
|
||||
BOLD := $(shell tput bold 2>/dev/null)
|
||||
GREEN := $(shell tput setaf 2 2>/dev/null)
|
||||
RESET := $(shell tput sgr0 2>/dev/null)
|
||||
|
||||
fmt: fmt/eslint fmt/prettier fmt/terraform fmt/shfmt fmt/go
|
||||
.PHONY: fmt
|
||||
@@ -482,12 +486,14 @@ gen: \
|
||||
$(DB_GEN_FILES) \
|
||||
site/src/api/typesGenerated.ts \
|
||||
coderd/rbac/object_gen.go \
|
||||
codersdk/rbacresources_gen.go \
|
||||
docs/admin/prometheus.md \
|
||||
docs/cli.md \
|
||||
docs/admin/audit-logs.md \
|
||||
coderd/apidoc/swagger.json \
|
||||
.prettierignore.include \
|
||||
.prettierignore \
|
||||
provisioner/terraform/testdata/version \
|
||||
site/.prettierrc.yaml \
|
||||
site/.prettierignore \
|
||||
site/.eslintignore \
|
||||
@@ -553,6 +559,9 @@ coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $
|
||||
coderd/database/dbmock/dbmock.go: coderd/database/db.go coderd/database/querier.go
|
||||
go generate ./coderd/database/dbmock/
|
||||
|
||||
coderd/database/pubsub/psmock/psmock.go: coderd/database/pubsub/pubsub.go
|
||||
go generate ./coderd/database/pubsub/psmock
|
||||
|
||||
tailnet/tailnettest/coordinatormock.go tailnet/tailnettest/multiagentmock.go tailnet/tailnettest/coordinateemock.go: tailnet/coordinator.go tailnet/multiagent.go
|
||||
go generate ./tailnet/tailnettest/
|
||||
|
||||
@@ -607,7 +616,10 @@ examples/examples.gen.json: scripts/examplegen/main.go examples/examples.go $(sh
|
||||
go run ./scripts/examplegen/main.go > examples/examples.gen.json
|
||||
|
||||
coderd/rbac/object_gen.go: scripts/rbacgen/main.go coderd/rbac/object.go
|
||||
go run scripts/rbacgen/main.go ./coderd/rbac > coderd/rbac/object_gen.go
|
||||
go run scripts/rbacgen/main.go rbac > coderd/rbac/object_gen.go
|
||||
|
||||
codersdk/rbacresources_gen.go: scripts/rbacgen/main.go coderd/rbac/object.go
|
||||
go run scripts/rbacgen/main.go codersdk > codersdk/rbacresources_gen.go
|
||||
|
||||
docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
|
||||
go run scripts/metricsdocgen/main.go
|
||||
@@ -673,6 +685,12 @@ provisioner/terraform/testdata/.gen-golden: $(wildcard provisioner/terraform/tes
|
||||
go test ./provisioner/terraform -run="Test.*Golden$$" -update
|
||||
touch "$@"
|
||||
|
||||
provisioner/terraform/testdata/version:
|
||||
if [[ "$(shell cat provisioner/terraform/testdata/version.txt)" != "$(shell terraform version -json | jq -r '.terraform_version')" ]]; then
|
||||
./provisioner/terraform/testdata/generate.sh
|
||||
fi
|
||||
.PHONY: provisioner/terraform/testdata/version
|
||||
|
||||
scripts/ci-report/testdata/.gen-golden: $(wildcard scripts/ci-report/testdata/*) $(wildcard scripts/ci-report/*.go)
|
||||
go test ./scripts/ci-report -run=TestOutputMatchesGoldenFile -update
|
||||
touch "$@"
|
||||
@@ -738,7 +756,7 @@ site/.eslintignore site/.prettierignore: .prettierignore Makefile
|
||||
done < "$<"
|
||||
|
||||
test:
|
||||
gotestsum --format standard-quiet -- -v -short -count=1 ./...
|
||||
$(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./...
|
||||
.PHONY: test
|
||||
|
||||
# sqlc-cloud-is-setup will fail if no SQLc auth token is set. Use this as a
|
||||
@@ -774,7 +792,7 @@ sqlc-vet: test-postgres-docker
|
||||
test-postgres: test-postgres-docker
|
||||
# The postgres test is prone to failure, so we limit parallelism for
|
||||
# more consistent execution.
|
||||
DB=ci DB_FROM=$(shell go run scripts/migrate-ci/main.go) gotestsum \
|
||||
$(GIT_FLAGS) DB=ci DB_FROM=$(shell go run scripts/migrate-ci/main.go) gotestsum \
|
||||
--junitfile="gotests.xml" \
|
||||
--jsonfile="gotests.json" \
|
||||
--packages="./..." -- \
|
||||
@@ -783,6 +801,17 @@ 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 log -1 --format='%h' HEAD)
|
||||
echo "COMMIT_FROM=$${COMMIT_FROM}"
|
||||
COMMIT_TO=$(shell git log -1 --format='%h' origin/main)
|
||||
echo "COMMIT_TO=$${COMMIT_TO}"
|
||||
if [[ "$${COMMIT_FROM}" == "$${COMMIT_TO}" ]]; then echo "Nothing to do!"; exit 0; fi
|
||||
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
|
||||
@@ -815,9 +844,21 @@ test-postgres-docker:
|
||||
|
||||
# Make sure to keep this in sync with test-go-race from .github/workflows/ci.yaml.
|
||||
test-race:
|
||||
gotestsum --junitfile="gotests.xml" -- -race -count=1 ./...
|
||||
$(GIT_FLAGS) gotestsum --junitfile="gotests.xml" -- -race -count=1 ./...
|
||||
.PHONY: test-race
|
||||
|
||||
test-tailnet-integration:
|
||||
env \
|
||||
CODER_TAILNET_TESTS=true \
|
||||
CODER_MAGICSOCK_DEBUG_LOGGING=true \
|
||||
TS_DEBUG_NETCHECK=true \
|
||||
GOTRACEBACK=single \
|
||||
go test \
|
||||
-exec "sudo -E" \
|
||||
-timeout=5m \
|
||||
-count=1 \
|
||||
./tailnet/test/integration
|
||||
|
||||
# Note: we used to add this to the test target, but it's not necessary and we can
|
||||
# achieve the desired result by specifying -count=1 in the go test invocation
|
||||
# instead. Keeping it here for convenience.
|
||||
|
||||
@@ -116,3 +116,13 @@ 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!
|
||||
|
||||
## Hiring
|
||||
|
||||
Apply [here](https://cdr.co/github-apply) if you're interested in joining our team.
|
||||
|
||||
+104
-99
@@ -155,35 +155,35 @@ func New(options Options) Agent {
|
||||
hardCtx, hardCancel := context.WithCancel(context.Background())
|
||||
gracefulCtx, gracefulCancel := context.WithCancel(hardCtx)
|
||||
a := &agent{
|
||||
tailnetListenPort: options.TailnetListenPort,
|
||||
reconnectingPTYTimeout: options.ReconnectingPTYTimeout,
|
||||
logger: options.Logger,
|
||||
gracefulCtx: gracefulCtx,
|
||||
gracefulCancel: gracefulCancel,
|
||||
hardCtx: hardCtx,
|
||||
hardCancel: hardCancel,
|
||||
coordDisconnected: make(chan struct{}),
|
||||
environmentVariables: options.EnvironmentVariables,
|
||||
client: options.Client,
|
||||
exchangeToken: options.ExchangeToken,
|
||||
filesystem: options.Filesystem,
|
||||
logDir: options.LogDir,
|
||||
tempDir: options.TempDir,
|
||||
scriptDataDir: options.ScriptDataDir,
|
||||
lifecycleUpdate: make(chan struct{}, 1),
|
||||
lifecycleReported: make(chan codersdk.WorkspaceAgentLifecycle, 1),
|
||||
lifecycleStates: []agentsdk.PostLifecycleRequest{{State: codersdk.WorkspaceAgentLifecycleCreated}},
|
||||
ignorePorts: options.IgnorePorts,
|
||||
portCacheDuration: options.PortCacheDuration,
|
||||
reportMetadataInterval: options.ReportMetadataInterval,
|
||||
serviceBannerRefreshInterval: options.ServiceBannerRefreshInterval,
|
||||
sshMaxTimeout: options.SSHMaxTimeout,
|
||||
subsystems: options.Subsystems,
|
||||
addresses: options.Addresses,
|
||||
syscaller: options.Syscaller,
|
||||
modifiedProcs: options.ModifiedProcesses,
|
||||
processManagementTick: options.ProcessManagementTick,
|
||||
logSender: agentsdk.NewLogSender(options.Logger),
|
||||
tailnetListenPort: options.TailnetListenPort,
|
||||
reconnectingPTYTimeout: options.ReconnectingPTYTimeout,
|
||||
logger: options.Logger,
|
||||
gracefulCtx: gracefulCtx,
|
||||
gracefulCancel: gracefulCancel,
|
||||
hardCtx: hardCtx,
|
||||
hardCancel: hardCancel,
|
||||
coordDisconnected: make(chan struct{}),
|
||||
environmentVariables: options.EnvironmentVariables,
|
||||
client: options.Client,
|
||||
exchangeToken: options.ExchangeToken,
|
||||
filesystem: options.Filesystem,
|
||||
logDir: options.LogDir,
|
||||
tempDir: options.TempDir,
|
||||
scriptDataDir: options.ScriptDataDir,
|
||||
lifecycleUpdate: make(chan struct{}, 1),
|
||||
lifecycleReported: make(chan codersdk.WorkspaceAgentLifecycle, 1),
|
||||
lifecycleStates: []agentsdk.PostLifecycleRequest{{State: codersdk.WorkspaceAgentLifecycleCreated}},
|
||||
ignorePorts: options.IgnorePorts,
|
||||
portCacheDuration: options.PortCacheDuration,
|
||||
reportMetadataInterval: options.ReportMetadataInterval,
|
||||
announcementBannersRefreshInterval: options.ServiceBannerRefreshInterval,
|
||||
sshMaxTimeout: options.SSHMaxTimeout,
|
||||
subsystems: options.Subsystems,
|
||||
addresses: options.Addresses,
|
||||
syscaller: options.Syscaller,
|
||||
modifiedProcs: options.ModifiedProcesses,
|
||||
processManagementTick: options.ProcessManagementTick,
|
||||
logSender: agentsdk.NewLogSender(options.Logger),
|
||||
|
||||
prometheusRegistry: prometheusRegistry,
|
||||
metrics: newAgentMetrics(prometheusRegistry),
|
||||
@@ -193,7 +193,7 @@ func New(options Options) Agent {
|
||||
// that gets closed on disconnection. This is used to wait for graceful disconnection from the
|
||||
// coordinator during shut down.
|
||||
close(a.coordDisconnected)
|
||||
a.serviceBanner.Store(new(codersdk.ServiceBannerConfig))
|
||||
a.announcementBanners.Store(new([]codersdk.BannerConfig))
|
||||
a.sessionToken.Store(new(string))
|
||||
a.init()
|
||||
return a
|
||||
@@ -231,19 +231,20 @@ type agent struct {
|
||||
|
||||
environmentVariables map[string]string
|
||||
|
||||
manifest atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection.
|
||||
reportMetadataInterval time.Duration
|
||||
scriptRunner *agentscripts.Runner
|
||||
serviceBanner atomic.Pointer[codersdk.ServiceBannerConfig] // serviceBanner is atomic because it is periodically updated.
|
||||
serviceBannerRefreshInterval time.Duration
|
||||
sessionToken atomic.Pointer[string]
|
||||
sshServer *agentssh.Server
|
||||
sshMaxTimeout time.Duration
|
||||
manifest atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection.
|
||||
reportMetadataInterval time.Duration
|
||||
scriptRunner *agentscripts.Runner
|
||||
announcementBanners atomic.Pointer[[]codersdk.BannerConfig] // announcementBanners is atomic because it is periodically updated.
|
||||
announcementBannersRefreshInterval time.Duration
|
||||
sessionToken atomic.Pointer[string]
|
||||
sshServer *agentssh.Server
|
||||
sshMaxTimeout time.Duration
|
||||
|
||||
lifecycleUpdate chan struct{}
|
||||
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
|
||||
lifecycleMu sync.RWMutex // Protects following.
|
||||
lifecycleStates []agentsdk.PostLifecycleRequest
|
||||
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.
|
||||
|
||||
network *tailnet.Conn
|
||||
addresses []netip.Prefix
|
||||
@@ -271,11 +272,11 @@ func (a *agent) TailnetConn() *tailnet.Conn {
|
||||
func (a *agent) init() {
|
||||
// pass the "hard" context because we explicitly close the SSH server as part of graceful shutdown.
|
||||
sshSrv, err := agentssh.NewServer(a.hardCtx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, &agentssh.Config{
|
||||
MaxTimeout: a.sshMaxTimeout,
|
||||
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
|
||||
ServiceBanner: func() *codersdk.ServiceBannerConfig { return a.serviceBanner.Load() },
|
||||
UpdateEnv: a.updateCommandEnv,
|
||||
WorkingDirectory: func() string { return a.manifest.Load().Directory },
|
||||
MaxTimeout: a.sshMaxTimeout,
|
||||
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
|
||||
AnnouncementBanners: func() *[]codersdk.BannerConfig { return a.announcementBanners.Load() },
|
||||
UpdateEnv: a.updateCommandEnv,
|
||||
WorkingDirectory: func() string { return a.manifest.Load().Directory },
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -625,7 +626,6 @@ 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[lastReportedIndex]
|
||||
if len(a.lifecycleStates) > lastReportedIndex+1 {
|
||||
report = a.lifecycleStates[lastReportedIndex+1]
|
||||
report := a.lifecycleStates[a.lifecycleLastReportedIndex]
|
||||
if len(a.lifecycleStates) > a.lifecycleLastReportedIndex+1 {
|
||||
report = a.lifecycleStates[a.lifecycleLastReportedIndex+1]
|
||||
}
|
||||
a.lifecycleMu.RUnlock()
|
||||
|
||||
if lastIndex == lastReportedIndex {
|
||||
if lastIndex == a.lifecycleLastReportedIndex {
|
||||
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?
|
||||
lastReportedIndex++
|
||||
a.lifecycleLastReportedIndex++
|
||||
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")
|
||||
lastReportedIndex++
|
||||
a.lifecycleLastReportedIndex++
|
||||
select {
|
||||
case a.lifecycleReported <- report.State:
|
||||
case <-a.lifecycleReported:
|
||||
a.lifecycleReported <- report.State
|
||||
}
|
||||
if lastReportedIndex < lastIndex {
|
||||
if a.lifecycleLastReportedIndex < lastIndex {
|
||||
// Keep reporting until we've sent all messages, we can't
|
||||
// rely on the channel triggering us before the backlog is
|
||||
// consumed.
|
||||
@@ -709,23 +709,26 @@ func (a *agent) setLifecycle(state codersdk.WorkspaceAgentLifecycle) {
|
||||
// (and must be done before the session actually starts).
|
||||
func (a *agent) fetchServiceBannerLoop(ctx context.Context, conn drpc.Conn) error {
|
||||
aAPI := proto.NewDRPCAgentClient(conn)
|
||||
ticker := time.NewTicker(a.serviceBannerRefreshInterval)
|
||||
ticker := time.NewTicker(a.announcementBannersRefreshInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{})
|
||||
bannersProto, err := aAPI.GetAnnouncementBanners(ctx, &proto.GetAnnouncementBannersRequest{})
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
a.logger.Error(ctx, "failed to update service banner", slog.Error(err))
|
||||
a.logger.Error(ctx, "failed to update notification banners", slog.Error(err))
|
||||
return err
|
||||
}
|
||||
serviceBanner := agentsdk.ServiceBannerFromProto(sbp)
|
||||
a.serviceBanner.Store(&serviceBanner)
|
||||
banners := make([]codersdk.BannerConfig, 0, len(bannersProto.AnnouncementBanners))
|
||||
for _, bannerProto := range bannersProto.AnnouncementBanners {
|
||||
banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto))
|
||||
}
|
||||
a.announcementBanners.Store(&banners)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -757,15 +760,18 @@ func (a *agent) run() (retErr error) {
|
||||
// redial the coder server and retry.
|
||||
connMan := newAPIConnRoutineManager(a.gracefulCtx, a.hardCtx, a.logger, conn)
|
||||
|
||||
connMan.start("init service banner", gracefulShutdownBehaviorStop,
|
||||
connMan.start("init notification banners", gracefulShutdownBehaviorStop,
|
||||
func(ctx context.Context, conn drpc.Conn) error {
|
||||
aAPI := proto.NewDRPCAgentClient(conn)
|
||||
sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{})
|
||||
bannersProto, err := aAPI.GetAnnouncementBanners(ctx, &proto.GetAnnouncementBannersRequest{})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch service banner: %w", err)
|
||||
}
|
||||
serviceBanner := agentsdk.ServiceBannerFromProto(sbp)
|
||||
a.serviceBanner.Store(&serviceBanner)
|
||||
banners := make([]codersdk.BannerConfig, 0, len(bannersProto.AnnouncementBanners))
|
||||
for _, bannerProto := range bannersProto.AnnouncementBanners {
|
||||
banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto))
|
||||
}
|
||||
a.announcementBanners.Store(&banners)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
@@ -807,23 +813,21 @@ func (a *agent) run() (retErr error) {
|
||||
// coordination <--------------------------+
|
||||
// derp map subscriber <----------------+
|
||||
// stats report loop <---------------+
|
||||
networkOK := make(chan struct{})
|
||||
manifestOK := make(chan struct{})
|
||||
networkOK := newCheckpoint(a.logger)
|
||||
manifestOK := newCheckpoint(a.logger)
|
||||
|
||||
connMan.start("handle manifest", gracefulShutdownBehaviorStop, a.handleManifest(manifestOK))
|
||||
|
||||
connMan.start("app health reporter", gracefulShutdownBehaviorStop,
|
||||
func(ctx context.Context, conn drpc.Conn) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-manifestOK:
|
||||
manifest := a.manifest.Load()
|
||||
NewWorkspaceAppHealthReporter(
|
||||
a.logger, manifest.Apps, agentsdk.AppHealthPoster(proto.NewDRPCAgentClient(conn)),
|
||||
)(ctx)
|
||||
return nil
|
||||
if err := manifestOK.wait(ctx); err != nil {
|
||||
return xerrors.Errorf("no manifest: %w", err)
|
||||
}
|
||||
manifest := a.manifest.Load()
|
||||
NewWorkspaceAppHealthReporter(
|
||||
a.logger, manifest.Apps, agentsdk.AppHealthPoster(proto.NewDRPCAgentClient(conn)),
|
||||
)(ctx)
|
||||
return nil
|
||||
})
|
||||
|
||||
connMan.start("create or update network", gracefulShutdownBehaviorStop,
|
||||
@@ -831,10 +835,8 @@ func (a *agent) run() (retErr error) {
|
||||
|
||||
connMan.start("coordination", gracefulShutdownBehaviorStop,
|
||||
func(ctx context.Context, conn drpc.Conn) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-networkOK:
|
||||
if err := networkOK.wait(ctx); err != nil {
|
||||
return xerrors.Errorf("no network: %w", err)
|
||||
}
|
||||
return a.runCoordinator(ctx, conn, a.network)
|
||||
},
|
||||
@@ -842,10 +844,8 @@ func (a *agent) run() (retErr error) {
|
||||
|
||||
connMan.start("derp map subscriber", gracefulShutdownBehaviorStop,
|
||||
func(ctx context.Context, conn drpc.Conn) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-networkOK:
|
||||
if err := networkOK.wait(ctx); err != nil {
|
||||
return xerrors.Errorf("no network: %w", err)
|
||||
}
|
||||
return a.runDERPMapSubscriber(ctx, conn, a.network)
|
||||
})
|
||||
@@ -853,10 +853,8 @@ func (a *agent) run() (retErr error) {
|
||||
connMan.start("fetch service banner loop", gracefulShutdownBehaviorStop, a.fetchServiceBannerLoop)
|
||||
|
||||
connMan.start("stats report loop", gracefulShutdownBehaviorStop, func(ctx context.Context, conn drpc.Conn) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-networkOK:
|
||||
if err := networkOK.wait(ctx); err != nil {
|
||||
return xerrors.Errorf("no network: %w", err)
|
||||
}
|
||||
return a.statsReporter.reportLoop(ctx, proto.NewDRPCAgentClient(conn))
|
||||
})
|
||||
@@ -865,8 +863,17 @@ func (a *agent) run() (retErr error) {
|
||||
}
|
||||
|
||||
// handleManifest returns a function that fetches and processes the manifest
|
||||
func (a *agent) handleManifest(manifestOK chan<- struct{}) func(ctx context.Context, conn drpc.Conn) error {
|
||||
func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, conn drpc.Conn) error {
|
||||
return func(ctx context.Context, conn drpc.Conn) error {
|
||||
var (
|
||||
sentResult = false
|
||||
err error
|
||||
)
|
||||
defer func() {
|
||||
if !sentResult {
|
||||
manifestOK.complete(err)
|
||||
}
|
||||
}()
|
||||
aAPI := proto.NewDRPCAgentClient(conn)
|
||||
mp, err := aAPI.GetManifest(ctx, &proto.GetManifestRequest{})
|
||||
if err != nil {
|
||||
@@ -903,14 +910,12 @@ func (a *agent) handleManifest(manifestOK chan<- struct{}) func(ctx context.Cont
|
||||
Subsystems: subsys,
|
||||
}})
|
||||
if err != nil {
|
||||
if xerrors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return xerrors.Errorf("update workspace agent startup: %w", err)
|
||||
}
|
||||
|
||||
oldManifest := a.manifest.Swap(&manifest)
|
||||
close(manifestOK)
|
||||
manifestOK.complete(nil)
|
||||
sentResult = true
|
||||
|
||||
// The startup script should only execute on the first run!
|
||||
if oldManifest == nil {
|
||||
@@ -971,14 +976,15 @@ func (a *agent) handleManifest(manifestOK chan<- struct{}) func(ctx context.Cont
|
||||
|
||||
// createOrUpdateNetwork waits for the manifest to be set using manifestOK, then creates or updates
|
||||
// the tailnet using the information in the manifest
|
||||
func (a *agent) createOrUpdateNetwork(manifestOK <-chan struct{}, networkOK chan<- struct{}) func(context.Context, drpc.Conn) error {
|
||||
return func(ctx context.Context, _ drpc.Conn) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-manifestOK:
|
||||
func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(context.Context, drpc.Conn) error {
|
||||
return func(ctx context.Context, _ drpc.Conn) (retErr error) {
|
||||
if err := manifestOK.wait(ctx); err != nil {
|
||||
return xerrors.Errorf("no manifest: %w", err)
|
||||
}
|
||||
var err error
|
||||
defer func() {
|
||||
networkOK.complete(retErr)
|
||||
}()
|
||||
manifest := a.manifest.Load()
|
||||
a.closeMutex.Lock()
|
||||
network := a.network
|
||||
@@ -1014,7 +1020,6 @@ func (a *agent) createOrUpdateNetwork(manifestOK <-chan struct{}, networkOK chan
|
||||
network.SetDERPForceWebSockets(manifest.DERPForceWebSockets)
|
||||
network.SetBlockEndpoints(manifest.DisableDirectConnections)
|
||||
}
|
||||
close(networkOK)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -614,12 +614,12 @@ func TestAgent_Session_TTY_MOTD_Update(t *testing.T) {
|
||||
// Set new banner func and wait for the agent to call it to update the
|
||||
// banner.
|
||||
ready := make(chan struct{}, 2)
|
||||
client.SetServiceBannerFunc(func() (codersdk.ServiceBannerConfig, error) {
|
||||
client.SetAnnouncementBannersFunc(func() ([]codersdk.BannerConfig, error) {
|
||||
select {
|
||||
case ready <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return test.banner, nil
|
||||
return []codersdk.BannerConfig{test.banner}, nil
|
||||
})
|
||||
<-ready
|
||||
<-ready // Wait for two updates to ensure the value has propagated.
|
||||
@@ -2193,15 +2193,15 @@ func setupAgentSSHClient(ctx context.Context, t *testing.T) *ssh.Client {
|
||||
func setupSSHSession(
|
||||
t *testing.T,
|
||||
manifest agentsdk.Manifest,
|
||||
serviceBanner codersdk.ServiceBannerConfig,
|
||||
banner codersdk.BannerConfig,
|
||||
prepareFS func(fs afero.Fs),
|
||||
opts ...func(*agenttest.Client, *agent.Options),
|
||||
) *ssh.Session {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
opts = append(opts, func(c *agenttest.Client, o *agent.Options) {
|
||||
c.SetServiceBannerFunc(func() (codersdk.ServiceBannerConfig, error) {
|
||||
return serviceBanner, nil
|
||||
c.SetAnnouncementBannersFunc(func() ([]codersdk.BannerConfig, error) {
|
||||
return []codersdk.BannerConfig{banner}, nil
|
||||
})
|
||||
})
|
||||
//nolint:dogsled
|
||||
|
||||
+14
-11
@@ -63,7 +63,7 @@ type Config struct {
|
||||
// file will be displayed to the user upon login.
|
||||
MOTDFile func() string
|
||||
// ServiceBanner returns the configuration for the Coder service banner.
|
||||
ServiceBanner func() *codersdk.ServiceBannerConfig
|
||||
AnnouncementBanners func() *[]codersdk.BannerConfig
|
||||
// UpdateEnv updates the environment variables for the command to be
|
||||
// executed. It can be used to add, modify or replace environment variables.
|
||||
UpdateEnv func(current []string) (updated []string, err error)
|
||||
@@ -123,8 +123,8 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
if config.MOTDFile == nil {
|
||||
config.MOTDFile = func() string { return "" }
|
||||
}
|
||||
if config.ServiceBanner == nil {
|
||||
config.ServiceBanner = func() *codersdk.ServiceBannerConfig { return &codersdk.ServiceBannerConfig{} }
|
||||
if config.AnnouncementBanners == nil {
|
||||
config.AnnouncementBanners = func() *[]codersdk.BannerConfig { return &[]codersdk.BannerConfig{} }
|
||||
}
|
||||
if config.WorkingDirectory == nil {
|
||||
config.WorkingDirectory = func() string {
|
||||
@@ -441,12 +441,15 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
|
||||
session.DisablePTYEmulation()
|
||||
|
||||
if isLoginShell(session.RawCommand()) {
|
||||
serviceBanner := s.config.ServiceBanner()
|
||||
if serviceBanner != nil {
|
||||
err := showServiceBanner(session, serviceBanner)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "agent failed to show service banner", slog.Error(err))
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "service_banner").Add(1)
|
||||
banners := s.config.AnnouncementBanners()
|
||||
if banners != nil {
|
||||
for _, banner := range *banners {
|
||||
err := showAnnouncementBanner(session, banner)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "agent failed to show announcement banner", slog.Error(err))
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "announcement_banner").Add(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -891,9 +894,9 @@ func isQuietLogin(fs afero.Fs, rawCommand string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// showServiceBanner will write the service banner if enabled and not blank
|
||||
// showAnnouncementBanner will write the service banner if enabled and not blank
|
||||
// along with a blank line for spacing.
|
||||
func showServiceBanner(session io.Writer, banner *codersdk.ServiceBannerConfig) error {
|
||||
func showAnnouncementBanner(session io.Writer, banner codersdk.BannerConfig) error {
|
||||
if banner.Enabled && banner.Message != "" {
|
||||
// The banner supports Markdown so we might want to parse it but Markdown is
|
||||
// still fairly readable in its raw form.
|
||||
|
||||
+21
-13
@@ -138,8 +138,8 @@ func (c *Client) GetStartupLogs() []agentsdk.Log {
|
||||
return c.logs
|
||||
}
|
||||
|
||||
func (c *Client) SetServiceBannerFunc(f func() (codersdk.ServiceBannerConfig, error)) {
|
||||
c.fakeAgentAPI.SetServiceBannerFunc(f)
|
||||
func (c *Client) SetAnnouncementBannersFunc(f func() ([]codersdk.BannerConfig, error)) {
|
||||
c.fakeAgentAPI.SetAnnouncementBannersFunc(f)
|
||||
}
|
||||
|
||||
func (c *Client) PushDERPMapUpdate(update *tailcfg.DERPMap) error {
|
||||
@@ -171,31 +171,39 @@ type FakeAgentAPI struct {
|
||||
lifecycleStates []codersdk.WorkspaceAgentLifecycle
|
||||
metadata map[string]agentsdk.Metadata
|
||||
|
||||
getServiceBannerFunc func() (codersdk.ServiceBannerConfig, error)
|
||||
getAnnouncementBannersFunc func() ([]codersdk.BannerConfig, error)
|
||||
}
|
||||
|
||||
func (f *FakeAgentAPI) GetManifest(context.Context, *agentproto.GetManifestRequest) (*agentproto.Manifest, error) {
|
||||
return f.manifest, nil
|
||||
}
|
||||
|
||||
func (f *FakeAgentAPI) SetServiceBannerFunc(fn func() (codersdk.ServiceBannerConfig, error)) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.getServiceBannerFunc = fn
|
||||
f.logger.Info(context.Background(), "updated ServiceBannerFunc")
|
||||
func (*FakeAgentAPI) GetServiceBanner(context.Context, *agentproto.GetServiceBannerRequest) (*agentproto.ServiceBanner, error) {
|
||||
return &agentproto.ServiceBanner{}, nil
|
||||
}
|
||||
|
||||
func (f *FakeAgentAPI) GetServiceBanner(context.Context, *agentproto.GetServiceBannerRequest) (*agentproto.ServiceBanner, error) {
|
||||
func (f *FakeAgentAPI) SetAnnouncementBannersFunc(fn func() ([]codersdk.BannerConfig, error)) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
if f.getServiceBannerFunc == nil {
|
||||
return &agentproto.ServiceBanner{}, nil
|
||||
f.getAnnouncementBannersFunc = fn
|
||||
f.logger.Info(context.Background(), "updated notification banners")
|
||||
}
|
||||
|
||||
func (f *FakeAgentAPI) GetAnnouncementBanners(context.Context, *agentproto.GetAnnouncementBannersRequest) (*agentproto.GetAnnouncementBannersResponse, error) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
if f.getAnnouncementBannersFunc == nil {
|
||||
return &agentproto.GetAnnouncementBannersResponse{AnnouncementBanners: []*agentproto.BannerConfig{}}, nil
|
||||
}
|
||||
sb, err := f.getServiceBannerFunc()
|
||||
banners, err := f.getAnnouncementBannersFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agentsdk.ProtoFromServiceBanner(sb), nil
|
||||
bannersProto := make([]*agentproto.BannerConfig, 0, len(banners))
|
||||
for _, banner := range banners {
|
||||
bannersProto = append(bannersProto, agentsdk.ProtoFromBannerConfig(banner))
|
||||
}
|
||||
return &agentproto.GetAnnouncementBannersResponse{AnnouncementBanners: bannersProto}, nil
|
||||
}
|
||||
|
||||
func (f *FakeAgentAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsRequest) (*agentproto.UpdateStatsResponse, error) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"cdr.dev/slog"
|
||||
)
|
||||
|
||||
// checkpoint allows a goroutine to communicate when it is OK to proceed beyond some async condition
|
||||
// to other dependent goroutines.
|
||||
type checkpoint struct {
|
||||
logger slog.Logger
|
||||
mu sync.Mutex
|
||||
called bool
|
||||
done chan struct{}
|
||||
err error
|
||||
}
|
||||
|
||||
// complete the checkpoint. Pass nil to indicate the checkpoint was ok. It is an error to call this
|
||||
// more than once.
|
||||
func (c *checkpoint) complete(err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.called {
|
||||
b := make([]byte, 2048)
|
||||
n := runtime.Stack(b, false)
|
||||
c.logger.Critical(context.Background(), "checkpoint complete called more than once", slog.F("stacktrace", b[:n]))
|
||||
return
|
||||
}
|
||||
c.called = true
|
||||
c.err = err
|
||||
close(c.done)
|
||||
}
|
||||
|
||||
func (c *checkpoint) wait(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-c.done:
|
||||
return c.err
|
||||
}
|
||||
}
|
||||
|
||||
func newCheckpoint(logger slog.Logger) *checkpoint {
|
||||
return &checkpoint{
|
||||
logger: logger,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestCheckpoint_CompleteWait(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := slogtest.Make(t, nil)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
uut := newCheckpoint(logger)
|
||||
err := xerrors.New("test")
|
||||
uut.complete(err)
|
||||
got := uut.wait(ctx)
|
||||
require.Equal(t, err, got)
|
||||
}
|
||||
|
||||
func TestCheckpoint_CompleteTwice(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
uut := newCheckpoint(logger)
|
||||
err := xerrors.New("test")
|
||||
uut.complete(err)
|
||||
uut.complete(nil) // drops CRITICAL log
|
||||
got := uut.wait(ctx)
|
||||
require.Equal(t, err, got)
|
||||
}
|
||||
|
||||
func TestCheckpoint_WaitComplete(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := slogtest.Make(t, nil)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
uut := newCheckpoint(logger)
|
||||
err := xerrors.New("test")
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- uut.wait(ctx)
|
||||
}()
|
||||
uut.complete(err)
|
||||
got := testutil.RequireRecvCtx(ctx, t, errCh)
|
||||
require.Equal(t, err, got)
|
||||
}
|
||||
+342
-129
@@ -1859,6 +1859,154 @@ func (x *BatchCreateLogsResponse) GetLogLimitExceeded() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type GetAnnouncementBannersRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
}
|
||||
|
||||
func (x *GetAnnouncementBannersRequest) Reset() {
|
||||
*x = GetAnnouncementBannersRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[22]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetAnnouncementBannersRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetAnnouncementBannersRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetAnnouncementBannersRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[22]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetAnnouncementBannersRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetAnnouncementBannersRequest) Descriptor() ([]byte, []int) {
|
||||
return file_agent_proto_agent_proto_rawDescGZIP(), []int{22}
|
||||
}
|
||||
|
||||
type GetAnnouncementBannersResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
AnnouncementBanners []*BannerConfig `protobuf:"bytes,1,rep,name=announcement_banners,json=announcementBanners,proto3" json:"announcement_banners,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetAnnouncementBannersResponse) Reset() {
|
||||
*x = GetAnnouncementBannersResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[23]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetAnnouncementBannersResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetAnnouncementBannersResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetAnnouncementBannersResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[23]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetAnnouncementBannersResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetAnnouncementBannersResponse) Descriptor() ([]byte, []int) {
|
||||
return file_agent_proto_agent_proto_rawDescGZIP(), []int{23}
|
||||
}
|
||||
|
||||
func (x *GetAnnouncementBannersResponse) GetAnnouncementBanners() []*BannerConfig {
|
||||
if x != nil {
|
||||
return x.AnnouncementBanners
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BannerConfig struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
BackgroundColor string `protobuf:"bytes,3,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"`
|
||||
}
|
||||
|
||||
func (x *BannerConfig) Reset() {
|
||||
*x = BannerConfig{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[24]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *BannerConfig) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BannerConfig) ProtoMessage() {}
|
||||
|
||||
func (x *BannerConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[24]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BannerConfig.ProtoReflect.Descriptor instead.
|
||||
func (*BannerConfig) Descriptor() ([]byte, []int) {
|
||||
return file_agent_proto_agent_proto_rawDescGZIP(), []int{24}
|
||||
}
|
||||
|
||||
func (x *BannerConfig) GetEnabled() bool {
|
||||
if x != nil {
|
||||
return x.Enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *BannerConfig) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BannerConfig) GetBackgroundColor() string {
|
||||
if x != nil {
|
||||
return x.BackgroundColor
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type WorkspaceApp_Healthcheck struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -1872,7 +2020,7 @@ type WorkspaceApp_Healthcheck struct {
|
||||
func (x *WorkspaceApp_Healthcheck) Reset() {
|
||||
*x = WorkspaceApp_Healthcheck{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[22]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[25]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1885,7 +2033,7 @@ func (x *WorkspaceApp_Healthcheck) String() string {
|
||||
func (*WorkspaceApp_Healthcheck) ProtoMessage() {}
|
||||
|
||||
func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[22]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[25]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1936,7 +2084,7 @@ type WorkspaceAgentMetadata_Result struct {
|
||||
func (x *WorkspaceAgentMetadata_Result) Reset() {
|
||||
*x = WorkspaceAgentMetadata_Result{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[23]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[26]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1949,7 +2097,7 @@ func (x *WorkspaceAgentMetadata_Result) String() string {
|
||||
func (*WorkspaceAgentMetadata_Result) ProtoMessage() {}
|
||||
|
||||
func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[23]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[26]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -2008,7 +2156,7 @@ type WorkspaceAgentMetadata_Description struct {
|
||||
func (x *WorkspaceAgentMetadata_Description) Reset() {
|
||||
*x = WorkspaceAgentMetadata_Description{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[24]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[27]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -2021,7 +2169,7 @@ func (x *WorkspaceAgentMetadata_Description) String() string {
|
||||
func (*WorkspaceAgentMetadata_Description) ProtoMessage() {}
|
||||
|
||||
func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[24]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[27]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -2086,7 +2234,7 @@ type Stats_Metric struct {
|
||||
func (x *Stats_Metric) Reset() {
|
||||
*x = Stats_Metric{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[27]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[30]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -2099,7 +2247,7 @@ func (x *Stats_Metric) String() string {
|
||||
func (*Stats_Metric) ProtoMessage() {}
|
||||
|
||||
func (x *Stats_Metric) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[27]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[30]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -2155,7 +2303,7 @@ type Stats_Metric_Label struct {
|
||||
func (x *Stats_Metric_Label) Reset() {
|
||||
*x = Stats_Metric_Label{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[28]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[31]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -2168,7 +2316,7 @@ func (x *Stats_Metric_Label) String() string {
|
||||
func (*Stats_Metric_Label) ProtoMessage() {}
|
||||
|
||||
func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[28]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[31]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -2210,7 +2358,7 @@ type BatchUpdateAppHealthRequest_HealthUpdate struct {
|
||||
func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() {
|
||||
*x = BatchUpdateAppHealthRequest_HealthUpdate{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[29]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[32]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -2223,7 +2371,7 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string {
|
||||
func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {}
|
||||
|
||||
func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[29]
|
||||
mi := &file_agent_proto_agent_proto_msgTypes[32]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -2594,64 +2742,87 @@ var file_agent_proto_agent_proto_rawDesc = []byte{
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69,
|
||||
0x6d, 0x69, 0x74, 0x5f, 0x65, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x08, 0x52, 0x10, 0x6c, 0x6f, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65,
|
||||
0x65, 0x64, 0x65, 0x64, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74,
|
||||
0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f,
|
||||
0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a,
|
||||
0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49,
|
||||
0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a,
|
||||
0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e,
|
||||
0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xf6, 0x05, 0x0a, 0x05, 0x41, 0x67,
|
||||
0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65,
|
||||
0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
|
||||
0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61,
|
||||
0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
|
||||
0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61,
|
||||
0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65,
|
||||
0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
||||
0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e,
|
||||
0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53,
|
||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b,
|
||||
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f,
|
||||
0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64,
|
||||
0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
|
||||
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69,
|
||||
0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
|
||||
0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c,
|
||||
0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
|
||||
0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61,
|
||||
0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c,
|
||||
0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e,
|
||||
0x65, 0x64, 0x65, 0x64, 0x22, 0x1f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75,
|
||||
0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f,
|
||||
0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x14, 0x61, 0x6e, 0x6e, 0x6f, 0x75,
|
||||
0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18,
|
||||
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
|
||||
0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e,
|
||||
0x66, 0x69, 0x67, 0x52, 0x13, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e,
|
||||
0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e,
|
||||
0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62,
|
||||
0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10,
|
||||
0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75,
|
||||
0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65,
|
||||
0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c,
|
||||
0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00,
|
||||
0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10,
|
||||
0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02,
|
||||
0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a,
|
||||
0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xef, 0x06, 0x0a,
|
||||
0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e,
|
||||
0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
|
||||
0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65,
|
||||
0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65,
|
||||
0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66,
|
||||
0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||
0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
|
||||
0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76,
|
||||
0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12,
|
||||
0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22,
|
||||
0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e,
|
||||
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
|
||||
0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74,
|
||||
0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64,
|
||||
0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61,
|
||||
0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
|
||||
0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a,
|
||||
0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48,
|
||||
0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61,
|
||||
0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64,
|
||||
0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e,
|
||||
0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
|
||||
0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76,
|
||||
0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70,
|
||||
0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e,
|
||||
0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12,
|
||||
0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
|
||||
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
|
||||
0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e,
|
||||
0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74,
|
||||
0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
|
||||
0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61,
|
||||
0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||
0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e,
|
||||
0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65,
|
||||
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62,
|
||||
0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67,
|
||||
0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e,
|
||||
0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f,
|
||||
0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65,
|
||||
0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68,
|
||||
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f,
|
||||
0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74,
|
||||
0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
|
||||
0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75,
|
||||
0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72,
|
||||
0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75,
|
||||
0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
|
||||
0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72,
|
||||
0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55,
|
||||
0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65,
|
||||
0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74,
|
||||
0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
|
||||
0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65,
|
||||
0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74,
|
||||
0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63,
|
||||
0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61,
|
||||
0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f,
|
||||
0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12,
|
||||
0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
|
||||
0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
||||
0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e,
|
||||
0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e,
|
||||
0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42,
|
||||
0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27,
|
||||
0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64,
|
||||
0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e,
|
||||
0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -2667,7 +2838,7 @@ func file_agent_proto_agent_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 7)
|
||||
var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
|
||||
var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 33)
|
||||
var file_agent_proto_agent_proto_goTypes = []interface{}{
|
||||
(AppHealth)(0), // 0: coder.agent.v2.AppHealth
|
||||
(WorkspaceApp_SharingLevel)(0), // 1: coder.agent.v2.WorkspaceApp.SharingLevel
|
||||
@@ -2698,73 +2869,79 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{
|
||||
(*Log)(nil), // 26: coder.agent.v2.Log
|
||||
(*BatchCreateLogsRequest)(nil), // 27: coder.agent.v2.BatchCreateLogsRequest
|
||||
(*BatchCreateLogsResponse)(nil), // 28: coder.agent.v2.BatchCreateLogsResponse
|
||||
(*WorkspaceApp_Healthcheck)(nil), // 29: coder.agent.v2.WorkspaceApp.Healthcheck
|
||||
(*WorkspaceAgentMetadata_Result)(nil), // 30: coder.agent.v2.WorkspaceAgentMetadata.Result
|
||||
(*WorkspaceAgentMetadata_Description)(nil), // 31: coder.agent.v2.WorkspaceAgentMetadata.Description
|
||||
nil, // 32: coder.agent.v2.Manifest.EnvironmentVariablesEntry
|
||||
nil, // 33: coder.agent.v2.Stats.ConnectionsByProtoEntry
|
||||
(*Stats_Metric)(nil), // 34: coder.agent.v2.Stats.Metric
|
||||
(*Stats_Metric_Label)(nil), // 35: coder.agent.v2.Stats.Metric.Label
|
||||
(*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 36: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
|
||||
(*durationpb.Duration)(nil), // 37: google.protobuf.Duration
|
||||
(*proto.DERPMap)(nil), // 38: coder.tailnet.v2.DERPMap
|
||||
(*timestamppb.Timestamp)(nil), // 39: google.protobuf.Timestamp
|
||||
(*GetAnnouncementBannersRequest)(nil), // 29: coder.agent.v2.GetAnnouncementBannersRequest
|
||||
(*GetAnnouncementBannersResponse)(nil), // 30: coder.agent.v2.GetAnnouncementBannersResponse
|
||||
(*BannerConfig)(nil), // 31: coder.agent.v2.BannerConfig
|
||||
(*WorkspaceApp_Healthcheck)(nil), // 32: coder.agent.v2.WorkspaceApp.Healthcheck
|
||||
(*WorkspaceAgentMetadata_Result)(nil), // 33: coder.agent.v2.WorkspaceAgentMetadata.Result
|
||||
(*WorkspaceAgentMetadata_Description)(nil), // 34: coder.agent.v2.WorkspaceAgentMetadata.Description
|
||||
nil, // 35: coder.agent.v2.Manifest.EnvironmentVariablesEntry
|
||||
nil, // 36: coder.agent.v2.Stats.ConnectionsByProtoEntry
|
||||
(*Stats_Metric)(nil), // 37: coder.agent.v2.Stats.Metric
|
||||
(*Stats_Metric_Label)(nil), // 38: coder.agent.v2.Stats.Metric.Label
|
||||
(*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 39: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
|
||||
(*durationpb.Duration)(nil), // 40: google.protobuf.Duration
|
||||
(*proto.DERPMap)(nil), // 41: coder.tailnet.v2.DERPMap
|
||||
(*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp
|
||||
}
|
||||
var file_agent_proto_agent_proto_depIdxs = []int32{
|
||||
1, // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel
|
||||
29, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck
|
||||
32, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck
|
||||
2, // 2: coder.agent.v2.WorkspaceApp.health:type_name -> coder.agent.v2.WorkspaceApp.Health
|
||||
37, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration
|
||||
30, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
|
||||
31, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
|
||||
32, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry
|
||||
38, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap
|
||||
40, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration
|
||||
33, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
|
||||
34, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
|
||||
35, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry
|
||||
41, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap
|
||||
8, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript
|
||||
7, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp
|
||||
31, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
|
||||
33, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry
|
||||
34, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric
|
||||
34, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
|
||||
36, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry
|
||||
37, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric
|
||||
14, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats
|
||||
37, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration
|
||||
40, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration
|
||||
4, // 15: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State
|
||||
39, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp
|
||||
42, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp
|
||||
17, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle
|
||||
36, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
|
||||
39, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
|
||||
5, // 19: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem
|
||||
21, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup
|
||||
30, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
|
||||
33, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
|
||||
23, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata
|
||||
39, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp
|
||||
42, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp
|
||||
6, // 24: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level
|
||||
26, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log
|
||||
37, // 26: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration
|
||||
39, // 27: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp
|
||||
37, // 28: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration
|
||||
37, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration
|
||||
3, // 30: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type
|
||||
35, // 31: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label
|
||||
0, // 32: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth
|
||||
11, // 33: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest
|
||||
13, // 34: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest
|
||||
15, // 35: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest
|
||||
18, // 36: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest
|
||||
19, // 37: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest
|
||||
22, // 38: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest
|
||||
24, // 39: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest
|
||||
27, // 40: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest
|
||||
10, // 41: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest
|
||||
12, // 42: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner
|
||||
16, // 43: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse
|
||||
17, // 44: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle
|
||||
20, // 45: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse
|
||||
21, // 46: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup
|
||||
25, // 47: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse
|
||||
28, // 48: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse
|
||||
41, // [41:49] is the sub-list for method output_type
|
||||
33, // [33:41] is the sub-list for method input_type
|
||||
33, // [33:33] is the sub-list for extension type_name
|
||||
33, // [33:33] is the sub-list for extension extendee
|
||||
0, // [0:33] is the sub-list for field type_name
|
||||
31, // 26: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig
|
||||
40, // 27: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration
|
||||
42, // 28: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp
|
||||
40, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration
|
||||
40, // 30: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration
|
||||
3, // 31: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type
|
||||
38, // 32: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label
|
||||
0, // 33: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth
|
||||
11, // 34: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest
|
||||
13, // 35: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest
|
||||
15, // 36: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest
|
||||
18, // 37: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest
|
||||
19, // 38: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest
|
||||
22, // 39: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest
|
||||
24, // 40: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest
|
||||
27, // 41: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest
|
||||
29, // 42: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest
|
||||
10, // 43: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest
|
||||
12, // 44: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner
|
||||
16, // 45: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse
|
||||
17, // 46: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle
|
||||
20, // 47: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse
|
||||
21, // 48: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup
|
||||
25, // 49: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse
|
||||
28, // 50: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse
|
||||
30, // 51: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse
|
||||
43, // [43:52] is the sub-list for method output_type
|
||||
34, // [34:43] is the sub-list for method input_type
|
||||
34, // [34:34] is the sub-list for extension type_name
|
||||
34, // [34:34] is the sub-list for extension extendee
|
||||
0, // [0:34] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_agent_proto_agent_proto_init() }
|
||||
@@ -3038,7 +3215,7 @@ func file_agent_proto_agent_proto_init() {
|
||||
}
|
||||
}
|
||||
file_agent_proto_agent_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*WorkspaceApp_Healthcheck); i {
|
||||
switch v := v.(*GetAnnouncementBannersRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -3050,7 +3227,7 @@ func file_agent_proto_agent_proto_init() {
|
||||
}
|
||||
}
|
||||
file_agent_proto_agent_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*WorkspaceAgentMetadata_Result); i {
|
||||
switch v := v.(*GetAnnouncementBannersResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -3062,7 +3239,31 @@ func file_agent_proto_agent_proto_init() {
|
||||
}
|
||||
}
|
||||
file_agent_proto_agent_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*WorkspaceAgentMetadata_Description); i {
|
||||
switch v := v.(*BannerConfig); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_agent_proto_agent_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*WorkspaceApp_Healthcheck); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_agent_proto_agent_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*WorkspaceAgentMetadata_Result); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -3074,6 +3275,18 @@ func file_agent_proto_agent_proto_init() {
|
||||
}
|
||||
}
|
||||
file_agent_proto_agent_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*WorkspaceAgentMetadata_Description); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_agent_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Stats_Metric); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -3085,7 +3298,7 @@ func file_agent_proto_agent_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_agent_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_agent_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Stats_Metric_Label); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -3097,7 +3310,7 @@ func file_agent_proto_agent_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_agent_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} {
|
||||
file_agent_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*BatchUpdateAppHealthRequest_HealthUpdate); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -3116,7 +3329,7 @@ func file_agent_proto_agent_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_agent_proto_agent_proto_rawDesc,
|
||||
NumEnums: 7,
|
||||
NumMessages: 30,
|
||||
NumMessages: 33,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -251,6 +251,18 @@ message BatchCreateLogsResponse {
|
||||
bool log_limit_exceeded = 1;
|
||||
}
|
||||
|
||||
message GetAnnouncementBannersRequest {}
|
||||
|
||||
message GetAnnouncementBannersResponse {
|
||||
repeated BannerConfig announcement_banners = 1;
|
||||
}
|
||||
|
||||
message BannerConfig {
|
||||
bool enabled = 1;
|
||||
string message = 2;
|
||||
string background_color = 3;
|
||||
}
|
||||
|
||||
service Agent {
|
||||
rpc GetManifest(GetManifestRequest) returns (Manifest);
|
||||
rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner);
|
||||
@@ -260,4 +272,5 @@ service Agent {
|
||||
rpc UpdateStartup(UpdateStartupRequest) returns (Startup);
|
||||
rpc BatchUpdateMetadata(BatchUpdateMetadataRequest) returns (BatchUpdateMetadataResponse);
|
||||
rpc BatchCreateLogs(BatchCreateLogsRequest) returns (BatchCreateLogsResponse);
|
||||
rpc GetAnnouncementBanners(GetAnnouncementBannersRequest) returns (GetAnnouncementBannersResponse);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ type DRPCAgentClient interface {
|
||||
UpdateStartup(ctx context.Context, in *UpdateStartupRequest) (*Startup, error)
|
||||
BatchUpdateMetadata(ctx context.Context, in *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error)
|
||||
BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error)
|
||||
GetAnnouncementBanners(ctx context.Context, in *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error)
|
||||
}
|
||||
|
||||
type drpcAgentClient struct {
|
||||
@@ -130,6 +131,15 @@ func (c *drpcAgentClient) BatchCreateLogs(ctx context.Context, in *BatchCreateLo
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *drpcAgentClient) GetAnnouncementBanners(ctx context.Context, in *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) {
|
||||
out := new(GetAnnouncementBannersResponse)
|
||||
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/GetAnnouncementBanners", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type DRPCAgentServer interface {
|
||||
GetManifest(context.Context, *GetManifestRequest) (*Manifest, error)
|
||||
GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error)
|
||||
@@ -139,6 +149,7 @@ type DRPCAgentServer interface {
|
||||
UpdateStartup(context.Context, *UpdateStartupRequest) (*Startup, error)
|
||||
BatchUpdateMetadata(context.Context, *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error)
|
||||
BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error)
|
||||
GetAnnouncementBanners(context.Context, *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error)
|
||||
}
|
||||
|
||||
type DRPCAgentUnimplementedServer struct{}
|
||||
@@ -175,9 +186,13 @@ func (s *DRPCAgentUnimplementedServer) BatchCreateLogs(context.Context, *BatchCr
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
func (s *DRPCAgentUnimplementedServer) GetAnnouncementBanners(context.Context, *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) {
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
type DRPCAgentDescription struct{}
|
||||
|
||||
func (DRPCAgentDescription) NumMethods() int { return 8 }
|
||||
func (DRPCAgentDescription) NumMethods() int { return 9 }
|
||||
|
||||
func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
|
||||
switch n {
|
||||
@@ -253,6 +268,15 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver,
|
||||
in1.(*BatchCreateLogsRequest),
|
||||
)
|
||||
}, DRPCAgentServer.BatchCreateLogs, true
|
||||
case 8:
|
||||
return "/coder.agent.v2.Agent/GetAnnouncementBanners", drpcEncoding_File_agent_proto_agent_proto{},
|
||||
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
|
||||
return srv.(DRPCAgentServer).
|
||||
GetAnnouncementBanners(
|
||||
ctx,
|
||||
in1.(*GetAnnouncementBannersRequest),
|
||||
)
|
||||
}, DRPCAgentServer.GetAnnouncementBanners, true
|
||||
default:
|
||||
return "", nil, nil, nil, false
|
||||
}
|
||||
@@ -389,3 +413,19 @@ func (x *drpcAgent_BatchCreateLogsStream) SendAndClose(m *BatchCreateLogsRespons
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
type DRPCAgent_GetAnnouncementBannersStream interface {
|
||||
drpc.Stream
|
||||
SendAndClose(*GetAnnouncementBannersResponse) error
|
||||
}
|
||||
|
||||
type drpcAgent_GetAnnouncementBannersStream struct {
|
||||
drpc.Stream
|
||||
}
|
||||
|
||||
func (x *drpcAgent_GetAnnouncementBannersStream) SendAndClose(m *GetAnnouncementBannersResponse) error {
|
||||
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ func ExternalAuth(ctx context.Context, writer io.Writer, opts ExternalAuthOption
|
||||
if auth.Authenticated {
|
||||
return nil
|
||||
}
|
||||
if auth.Optional {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(writer, "You must authenticate with %s to create a workspace with this template. Visit:\n\n\t%s\n\n", auth.DisplayName, auth.AuthenticateURL)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
|
||||
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter, defaultOverrides map[string]string) (string, error) {
|
||||
label := templateVersionParameter.Name
|
||||
if templateVersionParameter.DisplayName != "" {
|
||||
label = templateVersionParameter.DisplayName
|
||||
@@ -26,6 +26,11 @@ 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)" {
|
||||
@@ -58,7 +63,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
|
||||
var richParameterOption *codersdk.TemplateVersionParameterOption
|
||||
richParameterOption, err = RichSelect(inv, RichSelectOptions{
|
||||
Options: templateVersionParameter.Options,
|
||||
Default: templateVersionParameter.DefaultValue,
|
||||
Default: defaultValue,
|
||||
HideSearch: true,
|
||||
})
|
||||
if err == nil {
|
||||
@@ -69,7 +74,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
|
||||
} else {
|
||||
text := "Enter a value"
|
||||
if !templateVersionParameter.Required {
|
||||
text += fmt.Sprintf(" (default: %q)", templateVersionParameter.DefaultValue)
|
||||
text += fmt.Sprintf(" (default: %q)", defaultValue)
|
||||
}
|
||||
text += ":"
|
||||
|
||||
@@ -87,7 +92,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 = templateVersionParameter.DefaultValue
|
||||
value = defaultValue
|
||||
}
|
||||
|
||||
return value, nil
|
||||
|
||||
+3
-3
@@ -230,12 +230,12 @@ func (r *RootCmd) configSSH() *serpent.Command {
|
||||
Annotations: workspaceCommand,
|
||||
Use: "config-ssh",
|
||||
Short: "Add an SSH Host entry for your workspaces \"ssh coder.workspace\"",
|
||||
Long: formatExamples(
|
||||
example{
|
||||
Long: FormatExamples(
|
||||
Example{
|
||||
Description: "You can use -o (or --ssh-option) so set SSH options to be used for all your workspaces",
|
||||
Command: "coder config-ssh -o ForwardAgent=yes",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "You can use --dry-run (or -n) to see the changes that would be made",
|
||||
Command: "coder config-ssh --dry-run",
|
||||
},
|
||||
|
||||
+17
-8
@@ -35,8 +35,8 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
Annotations: workspaceCommand,
|
||||
Use: "create [name]",
|
||||
Short: "Create a workspace",
|
||||
Long: formatExamples(
|
||||
example{
|
||||
Long: FormatExamples(
|
||||
Example{
|
||||
Description: "Create a workspace for another user (if you have permission)",
|
||||
Command: "coder create <username>/<workspace_name>",
|
||||
},
|
||||
@@ -165,6 +165,11 @@ 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)
|
||||
@@ -178,8 +183,9 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
TemplateVersionID: templateVersionID,
|
||||
NewWorkspaceName: workspaceName,
|
||||
|
||||
RichParameterFile: parameterFlags.richParameterFile,
|
||||
RichParameters: cliBuildParameters,
|
||||
RichParameterFile: parameterFlags.richParameterFile,
|
||||
RichParameters: cliBuildParameters,
|
||||
RichParameterDefaults: cliBuildParameterDefaults,
|
||||
|
||||
SourceWorkspaceParameters: sourceWorkspaceParameters,
|
||||
})
|
||||
@@ -262,6 +268,7 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
cliui.SkipPromptOption(),
|
||||
)
|
||||
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
|
||||
cmd.Options = append(cmd.Options, parameterFlags.cliParameterDefaults()...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -276,9 +283,10 @@ type prepWorkspaceBuildArgs struct {
|
||||
PromptBuildOptions bool
|
||||
BuildOptions []codersdk.WorkspaceBuildParameter
|
||||
|
||||
PromptRichParameters bool
|
||||
RichParameters []codersdk.WorkspaceBuildParameter
|
||||
RichParameterFile string
|
||||
PromptRichParameters bool
|
||||
RichParameters []codersdk.WorkspaceBuildParameter
|
||||
RichParameterFile string
|
||||
RichParameterDefaults []codersdk.WorkspaceBuildParameter
|
||||
}
|
||||
|
||||
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
|
||||
@@ -311,7 +319,8 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
|
||||
WithBuildOptions(args.BuildOptions).
|
||||
WithPromptRichParameters(args.PromptRichParameters).
|
||||
WithRichParameters(args.RichParameters).
|
||||
WithRichParametersFile(parameterFile)
|
||||
WithRichParametersFile(parameterFile).
|
||||
WithRichParametersDefaults(args.RichParameterDefaults)
|
||||
buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -315,6 +315,68 @@ 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()
|
||||
|
||||
|
||||
+2
-2
@@ -28,8 +28,8 @@ func (r *RootCmd) dotfiles() *serpent.Command {
|
||||
Use: "dotfiles <git_repo_url>",
|
||||
Middleware: serpent.RequireNArgs(1),
|
||||
Short: "Personalize your workspace by applying a canonical dotfiles repository",
|
||||
Long: formatExamples(
|
||||
example{
|
||||
Long: FormatExamples(
|
||||
Example{
|
||||
Description: "Check out and install a dotfiles repository without prompts",
|
||||
Command: "coder dotfiles --yes git@github.com:example/dotfiles.git",
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -245,14 +244,8 @@ 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 {
|
||||
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)
|
||||
}
|
||||
// Best effort. If we get an error from syncing, just ignore it.
|
||||
_ = s.Sync()
|
||||
}
|
||||
|
||||
if c != nil {
|
||||
|
||||
+3
-3
@@ -35,8 +35,8 @@ func (r *RootCmd) externalAuthAccessToken() *serpent.Command {
|
||||
Short: "Print auth for an external provider",
|
||||
Long: "Print an access-token for an external auth provider. " +
|
||||
"The access-token will be validated and sent to stdout with exit code 0. " +
|
||||
"If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1\n" + formatExamples(
|
||||
example{
|
||||
"If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1\n" + FormatExamples(
|
||||
Example{
|
||||
Description: "Ensure that the user is authenticated with GitHub before cloning.",
|
||||
Command: `#!/usr/bin/env sh
|
||||
|
||||
@@ -49,7 +49,7 @@ else
|
||||
fi
|
||||
`,
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "Obtain an extra property of an access token for additional metadata.",
|
||||
Command: "coder external-auth access-token slack --extra \"authed_user.id\"",
|
||||
},
|
||||
|
||||
+3
-2
@@ -239,7 +239,7 @@ func (r *RootCmd) login() *serpent.Command {
|
||||
|
||||
if !inv.ParsedFlags().Changed("first-user-trial") && os.Getenv(firstUserTrialEnv) == "" {
|
||||
v, _ := cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Start a 30-day trial of Enterprise?",
|
||||
Text: "Start a trial of Enterprise?",
|
||||
IsConfirm: true,
|
||||
Default: "yes",
|
||||
})
|
||||
@@ -287,7 +287,8 @@ func (r *RootCmd) login() *serpent.Command {
|
||||
}
|
||||
|
||||
sessionToken, err = cliui.Prompt(inv, cliui.PromptOptions{
|
||||
Text: "Paste your token here:",
|
||||
Text: "Paste your token here:",
|
||||
Secret: true,
|
||||
Validate: func(token string) error {
|
||||
client.SetSessionToken(token)
|
||||
_, err := client.User(ctx, codersdk.Me)
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
|
||||
// need to wait for the agent to start.
|
||||
workspaceQuery := inv.Args[0]
|
||||
autostart := true
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, codersdk.Me, workspaceQuery)
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace and agent: %w", err)
|
||||
}
|
||||
|
||||
+4
-3
@@ -30,6 +30,7 @@ func (r *RootCmd) organizations() *serpent.Command {
|
||||
r.currentOrganization(),
|
||||
r.switchOrganization(),
|
||||
r.createOrganization(),
|
||||
r.organizationRoles(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -43,12 +44,12 @@ func (r *RootCmd) switchOrganization() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "set <organization name | ID>",
|
||||
Short: "set the organization used by the CLI. Pass an empty string to reset to the default organization.",
|
||||
Long: "set the organization used by the CLI. Pass an empty string to reset to the default organization.\n" + formatExamples(
|
||||
example{
|
||||
Long: "set the organization used by the CLI. Pass an empty string to reset to the default organization.\n" + FormatExamples(
|
||||
Example{
|
||||
Description: "Remove the current organization and defer to the default.",
|
||||
Command: "coder organizations set ''",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "Switch to a custom organization.",
|
||||
Command: "coder organizations set my-org",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
func (r *RootCmd) organizationRoles() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "roles",
|
||||
Short: "Manage organization roles.",
|
||||
Aliases: []string{"role"},
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
return inv.Command.HelpHandler(inv)
|
||||
},
|
||||
Hidden: true,
|
||||
Children: []*serpent.Command{
|
||||
r.showOrganizationRoles(),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *RootCmd) showOrganizationRoles() *serpent.Command {
|
||||
formatter := cliui.NewOutputFormatter(
|
||||
cliui.ChangeFormatterData(
|
||||
cliui.TableFormat([]assignableRolesTableRow{}, []string{"name", "display_name", "built_in", "site_permissions", "org_permissions", "user_permissions"}),
|
||||
func(data any) (any, error) {
|
||||
input, ok := data.([]codersdk.AssignableRoles)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("expected []codersdk.AssignableRoles got %T", data)
|
||||
}
|
||||
rows := make([]assignableRolesTableRow, 0, len(input))
|
||||
for _, role := range input {
|
||||
rows = append(rows, assignableRolesTableRow{
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
SitePermissions: fmt.Sprintf("%d permissions", len(role.SitePermissions)),
|
||||
OrganizationPermissions: fmt.Sprintf("%d organizations", len(role.OrganizationPermissions)),
|
||||
UserPermissions: fmt.Sprintf("%d permissions", len(role.UserPermissions)),
|
||||
Assignable: role.Assignable,
|
||||
BuiltIn: role.BuiltIn,
|
||||
})
|
||||
}
|
||||
return rows, nil
|
||||
},
|
||||
),
|
||||
cliui.JSONFormat(),
|
||||
)
|
||||
|
||||
client := new(codersdk.Client)
|
||||
cmd := &serpent.Command{
|
||||
Use: "show [role_names ...]",
|
||||
Short: "Show role(s)",
|
||||
Middleware: serpent.Chain(
|
||||
r.InitClient(client),
|
||||
),
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
ctx := inv.Context()
|
||||
org, err := CurrentOrganization(r, inv, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
roles, err := client.ListOrganizationRoles(ctx, org.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("listing roles: %w", err)
|
||||
}
|
||||
|
||||
if len(inv.Args) > 0 {
|
||||
// filter roles
|
||||
filtered := make([]codersdk.AssignableRoles, 0)
|
||||
for _, role := range roles {
|
||||
if slices.ContainsFunc(inv.Args, func(s string) bool {
|
||||
return strings.EqualFold(s, role.Name)
|
||||
}) {
|
||||
filtered = append(filtered, role)
|
||||
}
|
||||
}
|
||||
roles = filtered
|
||||
}
|
||||
|
||||
out, err := formatter.Format(inv.Context(), roles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(inv.Stdout, out)
|
||||
return err
|
||||
},
|
||||
}
|
||||
formatter.AttachOptions(&cmd.Options)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
type assignableRolesTableRow struct {
|
||||
Name string `table:"name,default_sort"`
|
||||
DisplayName string `table:"display_name"`
|
||||
SitePermissions string ` table:"site_permissions"`
|
||||
// map[<org_id>] -> Permissions
|
||||
OrganizationPermissions string `table:"org_permissions"`
|
||||
UserPermissions string `table:"user_permissions"`
|
||||
Assignable bool `table:"assignable"`
|
||||
BuiltIn bool `table:"built_in"`
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestShowOrganizationRoles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
|
||||
|
||||
const expectedRole = "test-role"
|
||||
dbgen.CustomRole(t, db, database.CustomRole{
|
||||
Name: expectedRole,
|
||||
DisplayName: "Expected",
|
||||
SitePermissions: nil,
|
||||
OrgPermissions: nil,
|
||||
UserPermissions: nil,
|
||||
OrganizationID: uuid.NullUUID{
|
||||
UUID: owner.OrganizationID,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
inv, root := clitest.New(t, "organization", "roles", "show")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, buf.String(), expectedRole)
|
||||
})
|
||||
}
|
||||
+15
-2
@@ -18,14 +18,16 @@ type workspaceParameterFlags struct {
|
||||
promptBuildOptions bool
|
||||
buildOptions []string
|
||||
|
||||
richParameterFile string
|
||||
richParameters []string
|
||||
richParameterFile string
|
||||
richParameters []string
|
||||
richParameterDefaults []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())
|
||||
}
|
||||
|
||||
@@ -62,6 +64,17 @@ func (wpf *workspaceParameterFlags) cliParameters() []serpent.Option {
|
||||
}
|
||||
}
|
||||
|
||||
func (wpf *workspaceParameterFlags) cliParameterDefaults() []serpent.Option {
|
||||
return serpent.OptionSet{
|
||||
serpent.Option{
|
||||
Flag: "parameter-default",
|
||||
Env: "CODER_RICH_PARAMETER_DEFAULT",
|
||||
Description: `Rich parameter default values in the format "name=value".`,
|
||||
Value: serpent.StringArrayOf(&wpf.richParameterDefaults),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wpf *workspaceParameterFlags) alwaysPrompt() serpent.Option {
|
||||
return serpent.Option{
|
||||
Flag: "always-prompt",
|
||||
|
||||
@@ -26,9 +26,10 @@ type ParameterResolver struct {
|
||||
lastBuildParameters []codersdk.WorkspaceBuildParameter
|
||||
sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
|
||||
|
||||
richParameters []codersdk.WorkspaceBuildParameter
|
||||
richParametersFile map[string]string
|
||||
buildOptions []codersdk.WorkspaceBuildParameter
|
||||
richParameters []codersdk.WorkspaceBuildParameter
|
||||
richParametersDefaults map[string]string
|
||||
richParametersFile map[string]string
|
||||
buildOptions []codersdk.WorkspaceBuildParameter
|
||||
|
||||
promptRichParameters bool
|
||||
promptBuildOptions bool
|
||||
@@ -59,6 +60,16 @@ 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
|
||||
@@ -227,7 +238,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)
|
||||
parameterValue, err := cliui.RichParameter(inv, tvp, pr.richParametersDefaults)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+6
-8
@@ -42,25 +42,23 @@ func (r *RootCmd) ping() *serpent.Command {
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(
|
||||
ctx, inv, client,
|
||||
false, // Do not autostart for a ping.
|
||||
codersdk.Me, workspaceName,
|
||||
workspaceName,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger := inv.Logger
|
||||
opts := &workspacesdk.DialAgentOptions{}
|
||||
|
||||
if r.verbose {
|
||||
logger = logger.AppendSinks(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
|
||||
opts.Logger = inv.Logger.AppendSinks(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
|
||||
}
|
||||
|
||||
if r.disableDirect {
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
|
||||
opts.BlockEndpoints = true
|
||||
}
|
||||
conn, err := workspacesdk.New(client).
|
||||
DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{
|
||||
Logger: logger,
|
||||
BlockEndpoints: r.disableDirect,
|
||||
})
|
||||
conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+12
-13
@@ -35,24 +35,24 @@ func (r *RootCmd) portForward() *serpent.Command {
|
||||
Use: "port-forward <workspace>",
|
||||
Short: `Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R".`,
|
||||
Aliases: []string{"tunnel"},
|
||||
Long: formatExamples(
|
||||
example{
|
||||
Long: FormatExamples(
|
||||
Example{
|
||||
Description: "Port forward a single TCP port from 1234 in the workspace to port 5678 on your local machine",
|
||||
Command: "coder port-forward <workspace> --tcp 5678:1234",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "Port forward a single UDP port from port 9000 to port 9000 on your local machine",
|
||||
Command: "coder port-forward <workspace> --udp 9000",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "Port forward multiple TCP ports and a UDP port",
|
||||
Command: "coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "Port forward multiple ports (TCP or UDP) in condensed syntax",
|
||||
Command: "coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "Port forward specifying the local address to bind to",
|
||||
Command: "coder port-forward <workspace> --tcp 1.2.3.4:8080:8080",
|
||||
},
|
||||
@@ -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, codersdk.Me, inv.Args[0])
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -95,19 +95,18 @@ func (r *RootCmd) portForward() *serpent.Command {
|
||||
return xerrors.Errorf("await agent: %w", err)
|
||||
}
|
||||
|
||||
opts := &workspacesdk.DialAgentOptions{}
|
||||
|
||||
logger := inv.Logger
|
||||
if r.verbose {
|
||||
logger = logger.AppendSinks(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
|
||||
opts.Logger = logger.AppendSinks(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
|
||||
}
|
||||
|
||||
if r.disableDirect {
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
|
||||
opts.BlockEndpoints = true
|
||||
}
|
||||
conn, err := workspacesdk.New(client).
|
||||
DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{
|
||||
Logger: logger,
|
||||
BlockEndpoints: r.disableDirect,
|
||||
})
|
||||
conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+8
-8
@@ -181,12 +181,12 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err
|
||||
`
|
||||
cmd := &serpent.Command{
|
||||
Use: "coder [global-flags] <subcommand>",
|
||||
Long: fmt.Sprintf(fmtLong, buildinfo.Version()) + formatExamples(
|
||||
example{
|
||||
Long: fmt.Sprintf(fmtLong, buildinfo.Version()) + FormatExamples(
|
||||
Example{
|
||||
Description: "Start a Coder server",
|
||||
Command: "coder server",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "Get started by creating a template from an example",
|
||||
Command: "coder templates init",
|
||||
},
|
||||
@@ -753,16 +753,16 @@ func isTTYWriter(inv *serpent.Invocation, writer io.Writer) bool {
|
||||
return isatty.IsTerminal(file.Fd())
|
||||
}
|
||||
|
||||
// example represents a standard example for command usage, to be used
|
||||
// with formatExamples.
|
||||
type example struct {
|
||||
// Example represents a standard example for command usage, to be used
|
||||
// with FormatExamples.
|
||||
type Example struct {
|
||||
Description string
|
||||
Command string
|
||||
}
|
||||
|
||||
// formatExamples formats the examples as width wrapped bulletpoint
|
||||
// FormatExamples formats the examples as width wrapped bulletpoint
|
||||
// descriptions with the command underneath.
|
||||
func formatExamples(examples ...example) string {
|
||||
func FormatExamples(examples ...Example) string {
|
||||
var sb strings.Builder
|
||||
|
||||
padStyle := cliui.DefaultStyles.Wrap.With(pretty.XPad(4, 0))
|
||||
|
||||
@@ -45,7 +45,7 @@ func Test_formatExamples(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
examples []example
|
||||
examples []Example
|
||||
wantMatches []string
|
||||
}{
|
||||
{
|
||||
@@ -55,7 +55,7 @@ func Test_formatExamples(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Output examples",
|
||||
examples: []example{
|
||||
examples: []Example{
|
||||
{
|
||||
Description: "Hello world.",
|
||||
Command: "echo hello",
|
||||
@@ -72,7 +72,7 @@ func Test_formatExamples(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "No description outputs commands",
|
||||
examples: []example{
|
||||
examples: []Example{
|
||||
{
|
||||
Command: "echo hello",
|
||||
},
|
||||
@@ -87,7 +87,7 @@ func Test_formatExamples(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := formatExamples(tt.examples...)
|
||||
got := FormatExamples(tt.examples...)
|
||||
if len(tt.wantMatches) == 0 {
|
||||
require.Empty(t, got)
|
||||
} else {
|
||||
|
||||
+6
-6
@@ -140,8 +140,8 @@ func (r *RootCmd) scheduleStart() *serpent.Command {
|
||||
client := new(codersdk.Client)
|
||||
cmd := &serpent.Command{
|
||||
Use: "start <workspace-name> { <start-time> [day-of-week] [location] | manual }",
|
||||
Long: scheduleStartDescriptionLong + "\n" + formatExamples(
|
||||
example{
|
||||
Long: scheduleStartDescriptionLong + "\n" + FormatExamples(
|
||||
Example{
|
||||
Description: "Set the workspace to start at 9:30am (in Dublin) from Monday to Friday",
|
||||
Command: "coder schedule start my-workspace 9:30AM Mon-Fri Europe/Dublin",
|
||||
},
|
||||
@@ -189,8 +189,8 @@ func (r *RootCmd) scheduleStop() *serpent.Command {
|
||||
client := new(codersdk.Client)
|
||||
return &serpent.Command{
|
||||
Use: "stop <workspace-name> { <duration> | manual }",
|
||||
Long: scheduleStopDescriptionLong + "\n" + formatExamples(
|
||||
example{
|
||||
Long: scheduleStopDescriptionLong + "\n" + FormatExamples(
|
||||
Example{
|
||||
Command: "coder schedule stop my-workspace 2h30m",
|
||||
},
|
||||
),
|
||||
@@ -234,8 +234,8 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
|
||||
overrideCmd := &serpent.Command{
|
||||
Use: "override-stop <workspace-name> <duration from now>",
|
||||
Short: "Override the stop time of a currently running workspace instance.",
|
||||
Long: scheduleOverrideDescriptionLong + "\n" + formatExamples(
|
||||
example{
|
||||
Long: scheduleOverrideDescriptionLong + "\n" + FormatExamples(
|
||||
Example{
|
||||
Command: "coder schedule override-stop my-workspace 90m",
|
||||
},
|
||||
),
|
||||
|
||||
+91
-87
@@ -796,31 +796,18 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):")
|
||||
|
||||
if vals.Telemetry.Enable {
|
||||
gitAuth := make([]telemetry.GitAuth, 0)
|
||||
// TODO:
|
||||
var gitAuthConfigs []codersdk.ExternalAuthConfig
|
||||
for _, cfg := range gitAuthConfigs {
|
||||
gitAuth = append(gitAuth, telemetry.GitAuth{
|
||||
Type: cfg.Type,
|
||||
})
|
||||
vals, err := vals.WithoutSecrets()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("remove secrets from deployment values: %w", err)
|
||||
}
|
||||
|
||||
options.Telemetry, err = telemetry.New(telemetry.Options{
|
||||
BuiltinPostgres: builtinPostgres,
|
||||
DeploymentID: deploymentID,
|
||||
Database: options.Database,
|
||||
Logger: logger.Named("telemetry"),
|
||||
URL: vals.Telemetry.URL.Value(),
|
||||
Wildcard: vals.WildcardAccessURL.String() != "",
|
||||
DERPServerRelayURL: vals.DERP.Server.RelayURL.String(),
|
||||
GitAuth: gitAuth,
|
||||
GitHubOAuth: vals.OAuth2.Github.ClientID != "",
|
||||
OIDCAuth: vals.OIDC.ClientID != "",
|
||||
OIDCIssuerURL: vals.OIDC.IssuerURL.String(),
|
||||
Prometheus: vals.Prometheus.Enable.Value(),
|
||||
STUN: len(vals.DERP.Server.STUNAddresses) != 0,
|
||||
Tunnel: tunnel != nil,
|
||||
Experiments: vals.Experiments.Value(),
|
||||
BuiltinPostgres: builtinPostgres,
|
||||
DeploymentID: deploymentID,
|
||||
Database: options.Database,
|
||||
Logger: logger.Named("telemetry"),
|
||||
URL: vals.Telemetry.URL.Value(),
|
||||
Tunnel: tunnel != nil,
|
||||
DeploymentConfig: vals,
|
||||
ParseLicenseJWT: func(lic *telemetry.License) error {
|
||||
// This will be nil when running in AGPL-only mode.
|
||||
if options.ParseLicenseClaims == nil {
|
||||
@@ -944,6 +931,13 @@ 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
|
||||
@@ -952,7 +946,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,
|
||||
ctx, coderAPI, provisionerdMetrics, logger, vals, daemonCacheDir, errCh, &provisionerdWaitGroup, name, provisionerTypes,
|
||||
)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create provisioner daemon: %w", err)
|
||||
@@ -965,7 +959,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, options.Database)
|
||||
purger := dbpurge.New(ctx, logger.Named("dbpurge"), options.Database)
|
||||
defer purger.Close()
|
||||
|
||||
// Updates workspace usage
|
||||
@@ -1340,6 +1334,7 @@ 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() {
|
||||
@@ -1359,79 +1354,88 @@ 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{}
|
||||
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()
|
||||
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()
|
||||
|
||||
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)
|
||||
} 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"),
|
||||
err := echo.Serve(ctx, &provisionersdk.ServeOptions{
|
||||
Listener: echoServer,
|
||||
WorkDirectory: workDir,
|
||||
},
|
||||
CachePath: tfDir,
|
||||
Tracer: tracer,
|
||||
})
|
||||
if err != nil && !xerrors.Is(err, context.Canceled) {
|
||||
select {
|
||||
case errCh <- err:
|
||||
default:
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("mkdir terraform dir: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
connector[string(database.ProvisionerTypeTerraform)] = sdkproto.NewDRPCProvisionerClient(terraformClient)
|
||||
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)
|
||||
default:
|
||||
return nil, xerrors.Errorf("unknown provisioner type %q", provisionerType)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return coderAPI.CreateInMemoryProvisionerDaemon(dialCtx, name, provisionerTypes)
|
||||
}, &provisionerd.Options{
|
||||
Logger: logger.Named(fmt.Sprintf("provisionerd-%s", name)),
|
||||
UpdateInterval: time.Second,
|
||||
|
||||
@@ -141,8 +141,8 @@ func Test_configureCipherSuites(t *testing.T) {
|
||||
name: "TLSUnsupported",
|
||||
minTLS: tls.VersionTLS10,
|
||||
maxTLS: tls.VersionTLS13,
|
||||
// TLS_RSA_WITH_AES_128_GCM_SHA256 only supports tls 1.2
|
||||
inputCiphers: []string{"TLS_RSA_WITH_AES_128_GCM_SHA256"},
|
||||
// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 only supports tls 1.2
|
||||
inputCiphers: []string{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"},
|
||||
wantErr: "no tls ciphers supported for tls versions",
|
||||
},
|
||||
{
|
||||
|
||||
+10
-5
@@ -1367,7 +1367,8 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons-echo",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--log-human", fiName,
|
||||
)
|
||||
clitest.Start(t, root)
|
||||
@@ -1385,7 +1386,8 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons-echo",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--log-human", fi,
|
||||
)
|
||||
clitest.Start(t, root)
|
||||
@@ -1403,7 +1405,8 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons-echo",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--log-json", fi,
|
||||
)
|
||||
clitest.Start(t, root)
|
||||
@@ -1424,7 +1427,8 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons-echo",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--log-stackdriver", fi,
|
||||
)
|
||||
// Attach pty so we get debug output from the command if this test
|
||||
@@ -1459,7 +1463,8 @@ func TestServer(t *testing.T) {
|
||||
"--in-memory",
|
||||
"--http-address", ":0",
|
||||
"--access-url", "http://example.com",
|
||||
"--provisioner-daemons-echo",
|
||||
"--provisioner-daemons=3",
|
||||
"--provisioner-types=echo",
|
||||
"--log-human", fi1,
|
||||
"--log-json", fi2,
|
||||
"--log-stackdriver", fi3,
|
||||
|
||||
+20
-19
@@ -39,7 +39,11 @@ func (r *RootCmd) speedtest() *serpent.Command {
|
||||
ctx, cancel := context.WithCancel(inv.Context())
|
||||
defer cancel()
|
||||
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, false, codersdk.Me, inv.Args[0])
|
||||
if direct && r.disableDirect {
|
||||
return xerrors.Errorf("--direct (-d) is incompatible with --%s", varDisableDirect)
|
||||
}
|
||||
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, false, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -52,18 +56,27 @@ func (r *RootCmd) speedtest() *serpent.Command {
|
||||
return xerrors.Errorf("await agent: %w", err)
|
||||
}
|
||||
|
||||
logger := inv.Logger.AppendSinks(sloghuman.Sink(inv.Stderr))
|
||||
opts := &workspacesdk.DialAgentOptions{}
|
||||
if r.verbose {
|
||||
logger = logger.Leveled(slog.LevelDebug)
|
||||
opts.Logger = inv.Logger.AppendSinks(sloghuman.Sink(inv.Stderr)).Leveled(slog.LevelDebug)
|
||||
}
|
||||
|
||||
if r.disableDirect {
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
|
||||
opts.BlockEndpoints = true
|
||||
}
|
||||
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, &workspacesdk.DialAgentOptions{
|
||||
Logger: logger,
|
||||
})
|
||||
DialAgent(ctx, workspaceAgent.ID, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -102,18 +115,6 @@ 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":
|
||||
|
||||
+55
-21
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -25,12 +26,8 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||
|
||||
"github.com/coder/retry"
|
||||
"github.com/coder/serpent"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/cli/cliutil"
|
||||
"github.com/coder/coder/v2/coderd/autobuild/notify"
|
||||
@@ -38,6 +35,9 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
"github.com/coder/coder/v2/pty"
|
||||
"github.com/coder/retry"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -56,6 +56,7 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
noWait bool
|
||||
logDirPath string
|
||||
remoteForwards []string
|
||||
env []string
|
||||
disableAutostart bool
|
||||
)
|
||||
client := new(codersdk.Client)
|
||||
@@ -79,6 +80,10 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Prevent unnecessary logs from the stdlib from messing up the TTY.
|
||||
// See: https://github.com/coder/coder/issues/13144
|
||||
log.SetOutput(io.Discard)
|
||||
|
||||
logger := inv.Logger
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
@@ -145,19 +150,26 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
stack := newCloserStack(ctx, logger)
|
||||
defer stack.close(nil)
|
||||
|
||||
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`)
|
||||
}
|
||||
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`)
|
||||
}
|
||||
}
|
||||
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, codersdk.Me, inv.Args[0])
|
||||
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])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -341,15 +353,22 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
}
|
||||
}
|
||||
|
||||
stdoutFile, validOut := inv.Stdout.(*os.File)
|
||||
stdinFile, validIn := inv.Stdin.(*os.File)
|
||||
if validOut && validIn && isatty.IsTerminal(stdoutFile.Fd()) {
|
||||
state, err := term.MakeRaw(int(stdinFile.Fd()))
|
||||
stdoutFile, validOut := inv.Stdout.(*os.File)
|
||||
if validIn && validOut && isatty.IsTerminal(stdinFile.Fd()) && isatty.IsTerminal(stdoutFile.Fd()) {
|
||||
inState, err := pty.MakeInputRaw(stdinFile.Fd())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = term.Restore(int(stdinFile.Fd()), state)
|
||||
_ = pty.RestoreTerminal(stdinFile.Fd(), inState)
|
||||
}()
|
||||
outState, err := pty.MakeOutputRaw(stdoutFile.Fd())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = pty.RestoreTerminal(stdoutFile.Fd(), outState)
|
||||
}()
|
||||
|
||||
windowChange := listenWindowSize(ctx)
|
||||
@@ -369,6 +388,12 @@ 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)
|
||||
@@ -477,6 +502,13 @@ 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
|
||||
@@ -551,10 +583,12 @@ 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, userID string, in string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
|
||||
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, input string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
|
||||
var (
|
||||
workspace codersdk.Workspace
|
||||
workspaceParts = strings.Split(in, ".")
|
||||
workspace codersdk.Workspace
|
||||
// The input will be `owner/name.agent`
|
||||
// The agent is optional.
|
||||
workspaceParts = strings.Split(input, ".")
|
||||
err error
|
||||
)
|
||||
|
||||
|
||||
@@ -968,6 +968,49 @@ 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")
|
||||
|
||||
+12
-6
@@ -99,7 +99,12 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
|
||||
|
||||
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
|
||||
if err != nil {
|
||||
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse build options: %w", err)
|
||||
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)
|
||||
}
|
||||
|
||||
buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
||||
@@ -108,11 +113,12 @@ 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,
|
||||
PromptBuildOptions: parameterFlags.promptBuildOptions,
|
||||
BuildOptions: buildOptions,
|
||||
PromptRichParameters: parameterFlags.promptRichParameters,
|
||||
RichParameters: cliRichParameters,
|
||||
RichParameterFile: parameterFlags.richParameterFile,
|
||||
RichParameterDefaults: cliRichParameterDefaults,
|
||||
})
|
||||
if err != nil {
|
||||
return codersdk.CreateWorkspaceBuildRequest{}, err
|
||||
|
||||
+58
-36
@@ -13,6 +13,7 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -100,7 +101,7 @@ func (r *RootCmd) supportBundle() *serpent.Command {
|
||||
|
||||
// Check if we're running inside a workspace
|
||||
if val, found := os.LookupEnv("CODER"); found && val == "true" {
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Running inside Coder workspace; this can affect results!")
|
||||
cliui.Warn(inv.Stderr, "Running inside Coder workspace; this can affect results!")
|
||||
cliLog.Debug(inv.Context(), "running inside coder workspace")
|
||||
}
|
||||
|
||||
@@ -114,32 +115,41 @@ 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),
|
||||
)
|
||||
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]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -165,8 +175,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: ws.ID,
|
||||
AgentID: agt.ID,
|
||||
WorkspaceID: wsID,
|
||||
AgentID: agtID,
|
||||
}
|
||||
|
||||
bun, err := support.Run(inv.Context(), &deps)
|
||||
@@ -174,6 +184,16 @@ 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 {
|
||||
@@ -181,6 +201,7 @@ 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
|
||||
},
|
||||
}
|
||||
@@ -222,20 +243,21 @@ 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 {
|
||||
@@ -255,17 +277,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),
|
||||
"workspace/template_file.zip": string(templateVersionBytes),
|
||||
"logs.txt": strings.Join(src.Logs, "\n"),
|
||||
"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),
|
||||
} {
|
||||
f, err := dest.Create(k)
|
||||
if err != nil {
|
||||
|
||||
+132
-45
@@ -23,6 +23,7 @@ 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"
|
||||
@@ -95,33 +96,50 @@ func TestSupportBundle(t *testing.T) {
|
||||
clitest.SetupConfig(t, client, root)
|
||||
err = inv.Run()
|
||||
require.NoError(t, err)
|
||||
assertBundleContents(t, path, secretValue)
|
||||
assertBundleContents(t, path, true, true, []string{secretValue})
|
||||
})
|
||||
|
||||
t.Run("NoWorkspace", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
var dc codersdk.DeploymentConfig
|
||||
secretValue := uuid.NewString()
|
||||
seedSecretDeploymentOptions(t, &dc, secretValue)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
DeploymentValues: dc.Values,
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
inv, root := clitest.New(t, "support", "bundle", "--yes")
|
||||
|
||||
d := t.TempDir()
|
||||
path := filepath.Join(d, "bundle.zip")
|
||||
inv, root := clitest.New(t, "support", "bundle", "--output-file", path, "--yes")
|
||||
//nolint: gocritic // requires owner privilege
|
||||
clitest.SetupConfig(t, client, root)
|
||||
err := inv.Run()
|
||||
require.ErrorContains(t, err, "must specify workspace name")
|
||||
require.NoError(t, err)
|
||||
assertBundleContents(t, path, false, false, []string{secretValue})
|
||||
})
|
||||
|
||||
t.Run("NoAgent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client, db := coderdtest.NewWithDatabase(t, nil)
|
||||
var dc codersdk.DeploymentConfig
|
||||
secretValue := uuid.NewString()
|
||||
seedSecretDeploymentOptions(t, &dc, secretValue)
|
||||
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
DeploymentValues: dc.Values,
|
||||
})
|
||||
admin := coderdtest.CreateFirstUser(t, client)
|
||||
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
|
||||
OrganizationID: admin.OrganizationID,
|
||||
OwnerID: admin.UserID,
|
||||
}).Do() // without agent!
|
||||
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--yes")
|
||||
d := t.TempDir()
|
||||
path := filepath.Join(d, "bundle.zip")
|
||||
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--output-file", path, "--yes")
|
||||
//nolint: gocritic // requires owner privilege
|
||||
clitest.SetupConfig(t, client, root)
|
||||
err := inv.Run()
|
||||
require.ErrorContains(t, err, "could not find agent")
|
||||
require.NoError(t, err)
|
||||
assertBundleContents(t, path, true, false, []string{secretValue})
|
||||
})
|
||||
|
||||
t.Run("NoPrivilege", func(t *testing.T) {
|
||||
@@ -140,7 +158,8 @@ func TestSupportBundle(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func assertBundleContents(t *testing.T, path string, badValues ...string) {
|
||||
// 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) {
|
||||
t.Helper()
|
||||
r, err := zip.OpenReader(path)
|
||||
require.NoError(t, err, "open zip file")
|
||||
@@ -164,6 +183,10 @@ func assertBundleContents(t *testing.T, path string, badValues ...string) {
|
||||
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")
|
||||
@@ -171,66 +194,130 @@ func assertBundleContents(t *testing.T, path string, badValues ...string) {
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "tailnet debug should not be empty")
|
||||
case "network/netcheck.json":
|
||||
var v workspacesdk.AgentConnectionInfo
|
||||
var v derphealth.Report
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "connection info should not be empty")
|
||||
require.NotEmpty(t, v, "netcheck 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 "agent/agent.json":
|
||||
var v codersdk.WorkspaceAgent
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "agent should not be empty")
|
||||
case "agent/listening_ports.json":
|
||||
var v codersdk.WorkspaceAgentListeningPortsResponse
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "agent listening ports should not be empty")
|
||||
case "agent/logs.txt":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "logs should not be empty")
|
||||
case "agent/agent_magicsock.html":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "agent magicsock should not be empty")
|
||||
case "agent/client_magicsock.html":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "client magicsock should not be empty")
|
||||
case "agent/manifest.json":
|
||||
var v agentsdk.Manifest
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "agent manifest should not be empty")
|
||||
case "agent/peer_diagnostics.json":
|
||||
var v *tailnet.PeerDiagnostics
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "peer diagnostics should not be empty")
|
||||
case "agent/ping_result.json":
|
||||
var v *ipnstate.PingResult
|
||||
decodeJSONFromZip(t, f, &v)
|
||||
require.NotEmpty(t, v, "ping result should not be empty")
|
||||
case "agent/prometheus.txt":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "agent prometheus metrics should not be empty")
|
||||
case "agent/startup_logs.txt":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.Contains(t, string(bs), "started up")
|
||||
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 "logs.txt":
|
||||
bs := readBytesFromZip(t, f)
|
||||
require.NotEmpty(t, bs, "logs should not be empty")
|
||||
|
||||
+3
-3
@@ -16,12 +16,12 @@ func (r *RootCmd) templates() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "templates",
|
||||
Short: "Manage templates",
|
||||
Long: "Templates are written in standard Terraform and describe the infrastructure for workspaces\n" + formatExamples(
|
||||
example{
|
||||
Long: "Templates are written in standard Terraform and describe the infrastructure for workspaces\n" + FormatExamples(
|
||||
Example{
|
||||
Description: "Make changes to your template, and plan the changes",
|
||||
Command: "coder templates plan my-template",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "Create or push an update to the template. Your developers can update their workspaces",
|
||||
Command: "coder templates push my-template",
|
||||
},
|
||||
|
||||
@@ -19,8 +19,8 @@ func (r *RootCmd) templateVersions() *serpent.Command {
|
||||
Use: "versions",
|
||||
Short: "Manage different versions of the specified template",
|
||||
Aliases: []string{"version"},
|
||||
Long: formatExamples(
|
||||
example{
|
||||
Long: FormatExamples(
|
||||
Example{
|
||||
Description: "List versions of a specific template",
|
||||
Command: "coder templates versions list my-template",
|
||||
},
|
||||
|
||||
+3
@@ -20,6 +20,9 @@ OPTIONS:
|
||||
--parameter string-array, $CODER_RICH_PARAMETER
|
||||
Rich parameter value in the format "name=value".
|
||||
|
||||
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
|
||||
Rich parameter default values in the format "name=value".
|
||||
|
||||
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
|
||||
Specify a file path with values for rich parameters defined in the
|
||||
template.
|
||||
|
||||
+3
@@ -19,6 +19,9 @@ OPTIONS:
|
||||
--parameter string-array, $CODER_RICH_PARAMETER
|
||||
Rich parameter value in the format "name=value".
|
||||
|
||||
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
|
||||
Rich parameter default values in the format "name=value".
|
||||
|
||||
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
|
||||
Specify a file path with values for rich parameters defined in the
|
||||
template.
|
||||
|
||||
+4
@@ -60,6 +60,10 @@ OPTIONS:
|
||||
--support-links struct[[]codersdk.LinkConfig], $CODER_SUPPORT_LINKS
|
||||
Support links to display in the top right drop down menu.
|
||||
|
||||
--terms-of-service-url string, $CODER_TERMS_OF_SERVICE_URL
|
||||
A URL to an external Terms of Service that must be accepted by users
|
||||
when logging in.
|
||||
|
||||
--update-check bool, $CODER_UPDATE_CHECK (default: false)
|
||||
Periodically check for new releases of Coder and inform the owner. The
|
||||
check is performed once per day.
|
||||
|
||||
Vendored
+3
@@ -9,6 +9,9 @@ OPTIONS:
|
||||
--disable-autostart bool, $CODER_SSH_DISABLE_AUTOSTART (default: false)
|
||||
Disable starting the workspace automatically when connecting via SSH.
|
||||
|
||||
-e, --env string-array, $CODER_SSH_ENV
|
||||
Set environment variable(s) for session (key1=value1,key2=value2,...).
|
||||
|
||||
-A, --forward-agent bool, $CODER_SSH_FORWARD_AGENT
|
||||
Specifies whether to forward the SSH agent specified in
|
||||
$SSH_AUTH_SOCK.
|
||||
|
||||
+3
@@ -19,6 +19,9 @@ OPTIONS:
|
||||
--parameter string-array, $CODER_RICH_PARAMETER
|
||||
Rich parameter value in the format "name=value".
|
||||
|
||||
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
|
||||
Rich parameter default values in the format "name=value".
|
||||
|
||||
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
|
||||
Specify a file path with values for rich parameters defined in the
|
||||
template.
|
||||
|
||||
+3
@@ -21,6 +21,9 @@ 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.
|
||||
|
||||
+9
-4
@@ -379,10 +379,11 @@ provisioning:
|
||||
# state for a long time, consider increasing this.
|
||||
# (default: 3, type: int)
|
||||
daemons: 3
|
||||
# Whether to use echo provisioner daemons instead of Terraform. This is for E2E
|
||||
# tests.
|
||||
# (default: false, type: bool)
|
||||
daemonsEcho: false
|
||||
# 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
|
||||
# Deprecated and ignored.
|
||||
# (default: 1s, type: duration)
|
||||
daemonPollInterval: 1s
|
||||
@@ -414,6 +415,10 @@ 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)
|
||||
|
||||
+4
-4
@@ -17,16 +17,16 @@ func (r *RootCmd) tokens() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "tokens",
|
||||
Short: "Manage personal access tokens",
|
||||
Long: "Tokens are used to authenticate automated clients to Coder.\n" + formatExamples(
|
||||
example{
|
||||
Long: "Tokens are used to authenticate automated clients to Coder.\n" + FormatExamples(
|
||||
Example{
|
||||
Description: "Create a token for automation",
|
||||
Command: "coder tokens create",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "List your tokens",
|
||||
Command: "coder tokens ls",
|
||||
},
|
||||
example{
|
||||
Example{
|
||||
Description: "Remove a token by ID",
|
||||
Command: "coder tokens rm WuoWs4ZsMX",
|
||||
},
|
||||
|
||||
+2
-2
@@ -57,8 +57,8 @@ func (r *RootCmd) userSingle() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "show <username|user_id|'me'>",
|
||||
Short: "Show a single user. Use 'me' to indicate the currently authenticated user.",
|
||||
Long: formatExamples(
|
||||
example{
|
||||
Long: FormatExamples(
|
||||
Example{
|
||||
Command: "coder users show me",
|
||||
},
|
||||
),
|
||||
|
||||
+2
-2
@@ -40,8 +40,8 @@ func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *serpen
|
||||
Use: fmt.Sprintf("%s <username|user_id>", verb),
|
||||
Short: short,
|
||||
Aliases: aliases,
|
||||
Long: formatExamples(
|
||||
example{
|
||||
Long: FormatExamples(
|
||||
Example{
|
||||
Command: fmt.Sprintf("coder users %s example_user", verb),
|
||||
},
|
||||
),
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ func (r *RootCmd) vscodeSSH() *serpent.Command {
|
||||
// will call this command after the workspace is started.
|
||||
autostart := false
|
||||
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, owner, name)
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, fmt.Sprintf("%s/%s", owner, name))
|
||||
if err != nil {
|
||||
return xerrors.Errorf("find workspace and agent: %w", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/appearance"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
)
|
||||
|
||||
type AnnouncementBannerAPI struct {
|
||||
appearanceFetcher *atomic.Pointer[appearance.Fetcher]
|
||||
}
|
||||
|
||||
// Deprecated: GetServiceBanner has been deprecated in favor of GetAnnouncementBanners.
|
||||
func (a *AnnouncementBannerAPI) GetServiceBanner(ctx context.Context, _ *proto.GetServiceBannerRequest) (*proto.ServiceBanner, error) {
|
||||
cfg, err := (*a.appearanceFetcher.Load()).Fetch(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("fetch appearance: %w", err)
|
||||
}
|
||||
return agentsdk.ProtoFromServiceBanner(cfg.ServiceBanner), nil
|
||||
}
|
||||
|
||||
func (a *AnnouncementBannerAPI) GetAnnouncementBanners(ctx context.Context, _ *proto.GetAnnouncementBannersRequest) (*proto.GetAnnouncementBannersResponse, error) {
|
||||
cfg, err := (*a.appearanceFetcher.Load()).Fetch(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("fetch appearance: %w", err)
|
||||
}
|
||||
banners := make([]*proto.BannerConfig, 0, len(cfg.AnnouncementBanners))
|
||||
for _, banner := range cfg.AnnouncementBanners {
|
||||
banners = append(banners, agentsdk.ProtoFromBannerConfig(banner))
|
||||
}
|
||||
return &proto.GetAnnouncementBannersResponse{
|
||||
AnnouncementBanners: banners,
|
||||
}, nil
|
||||
}
|
||||
+13
-22
@@ -11,36 +11,30 @@ import (
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/appearance"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
)
|
||||
|
||||
func TestGetServiceBanner(t *testing.T) {
|
||||
func TestGetAnnouncementBanners(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := codersdk.ServiceBannerConfig{
|
||||
cfg := []codersdk.BannerConfig{{
|
||||
Enabled: true,
|
||||
Message: "hello world",
|
||||
BackgroundColor: "#000000",
|
||||
}
|
||||
Message: "The beep-bop will be boop-beeped on Saturday at 12AM PST.",
|
||||
BackgroundColor: "#00FF00",
|
||||
}}
|
||||
|
||||
var ff appearance.Fetcher = fakeFetcher{cfg: codersdk.AppearanceConfig{ServiceBanner: cfg}}
|
||||
var ff appearance.Fetcher = fakeFetcher{cfg: codersdk.AppearanceConfig{AnnouncementBanners: cfg}}
|
||||
ptr := atomic.Pointer[appearance.Fetcher]{}
|
||||
ptr.Store(&ff)
|
||||
|
||||
api := &ServiceBannerAPI{
|
||||
appearanceFetcher: &ptr,
|
||||
}
|
||||
|
||||
resp, err := api.GetServiceBanner(context.Background(), &agentproto.GetServiceBannerRequest{})
|
||||
api := &AnnouncementBannerAPI{appearanceFetcher: &ptr}
|
||||
resp, err := api.GetAnnouncementBanners(context.Background(), &agentproto.GetAnnouncementBannersRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, &agentproto.ServiceBanner{
|
||||
Enabled: cfg.Enabled,
|
||||
Message: cfg.Message,
|
||||
BackgroundColor: cfg.BackgroundColor,
|
||||
}, resp)
|
||||
require.Len(t, resp.AnnouncementBanners, 1)
|
||||
require.Equal(t, cfg[0], agentsdk.BannerConfigFromProto(resp.AnnouncementBanners[0]))
|
||||
})
|
||||
|
||||
t.Run("FetchError", func(t *testing.T) {
|
||||
@@ -51,11 +45,8 @@ func TestGetServiceBanner(t *testing.T) {
|
||||
ptr := atomic.Pointer[appearance.Fetcher]{}
|
||||
ptr.Store(&ff)
|
||||
|
||||
api := &ServiceBannerAPI{
|
||||
appearanceFetcher: &ptr,
|
||||
}
|
||||
|
||||
resp, err := api.GetServiceBanner(context.Background(), &agentproto.GetServiceBannerRequest{})
|
||||
api := &AnnouncementBannerAPI{appearanceFetcher: &ptr}
|
||||
resp, err := api.GetAnnouncementBanners(context.Background(), &agentproto.GetAnnouncementBannersRequest{})
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
require.Nil(t, resp)
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/prometheusmetrics"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/coderd/workspacestats"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
tailnetproto "github.com/coder/coder/v2/tailnet/proto"
|
||||
@@ -35,7 +36,7 @@ import (
|
||||
type API struct {
|
||||
opts Options
|
||||
*ManifestAPI
|
||||
*ServiceBannerAPI
|
||||
*AnnouncementBannerAPI
|
||||
*StatsAPI
|
||||
*LifecycleAPI
|
||||
*AppsAPI
|
||||
@@ -59,7 +60,7 @@ type Options struct {
|
||||
DerpMapFn func() *tailcfg.DERPMap
|
||||
TailnetCoordinator *atomic.Pointer[tailnet.Coordinator]
|
||||
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
StatsBatcher StatsBatcher
|
||||
StatsReporter *workspacestats.Reporter
|
||||
AppearanceFetcher *atomic.Pointer[appearance.Fetcher]
|
||||
PublishWorkspaceUpdateFn func(ctx context.Context, workspaceID uuid.UUID)
|
||||
PublishWorkspaceAgentLogsUpdateFn func(ctx context.Context, workspaceAgentID uuid.UUID, msg agentsdk.LogsNotifyMessage)
|
||||
@@ -107,19 +108,16 @@ func New(opts Options) *API {
|
||||
},
|
||||
}
|
||||
|
||||
api.ServiceBannerAPI = &ServiceBannerAPI{
|
||||
api.AnnouncementBannerAPI = &AnnouncementBannerAPI{
|
||||
appearanceFetcher: opts.AppearanceFetcher,
|
||||
}
|
||||
|
||||
api.StatsAPI = &StatsAPI{
|
||||
AgentFn: api.agent,
|
||||
Database: opts.Database,
|
||||
Pubsub: opts.Pubsub,
|
||||
Log: opts.Log,
|
||||
StatsBatcher: opts.StatsBatcher,
|
||||
TemplateScheduleStore: opts.TemplateScheduleStore,
|
||||
StatsReporter: opts.StatsReporter,
|
||||
AgentStatsRefreshInterval: opts.AgentStatsRefreshInterval,
|
||||
UpdateAgentMetricsFn: opts.UpdateAgentMetricsFn,
|
||||
}
|
||||
|
||||
api.LifecycleAPI = &LifecycleAPI{
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/appearance"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
)
|
||||
|
||||
type ServiceBannerAPI struct {
|
||||
appearanceFetcher *atomic.Pointer[appearance.Fetcher]
|
||||
}
|
||||
|
||||
func (a *ServiceBannerAPI) GetServiceBanner(ctx context.Context, _ *proto.GetServiceBannerRequest) (*proto.ServiceBanner, error) {
|
||||
cfg, err := (*a.appearanceFetcher.Load()).Fetch(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("fetch appearance: %w", err)
|
||||
}
|
||||
return agentsdk.ProtoFromServiceBanner(cfg.ServiceBanner), nil
|
||||
}
|
||||
+11
-82
@@ -2,10 +2,8 @@ package agentapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/xerrors"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
@@ -13,13 +11,9 @@ 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"
|
||||
"github.com/coder/coder/v2/coderd/prometheusmetrics"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/coderd/workspacestats"
|
||||
)
|
||||
|
||||
type StatsBatcher interface {
|
||||
@@ -29,12 +23,9 @@ type StatsBatcher interface {
|
||||
type StatsAPI struct {
|
||||
AgentFn func(context.Context) (database.WorkspaceAgent, error)
|
||||
Database database.Store
|
||||
Pubsub pubsub.Pubsub
|
||||
Log slog.Logger
|
||||
StatsBatcher StatsBatcher
|
||||
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
StatsReporter *workspacestats.Reporter
|
||||
AgentStatsRefreshInterval time.Duration
|
||||
UpdateAgentMetricsFn func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric)
|
||||
|
||||
TimeNowFn func() time.Time // defaults to dbtime.Now()
|
||||
}
|
||||
@@ -70,79 +61,17 @@ func (a *StatsAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsR
|
||||
slog.F("payload", req),
|
||||
)
|
||||
|
||||
now := a.now()
|
||||
if req.Stats.ConnectionCount > 0 {
|
||||
var nextAutostart time.Time
|
||||
if workspace.AutostartSchedule.String != "" {
|
||||
templateSchedule, err := (*(a.TemplateScheduleStore.Load())).Get(ctx, a.Database, workspace.TemplateID)
|
||||
// If the template schedule fails to load, just default to bumping
|
||||
// without the next transition and log it.
|
||||
if err != nil {
|
||||
a.Log.Error(ctx, "failed to load template schedule bumping activity, defaulting to bumping by 60min",
|
||||
slog.F("workspace_id", workspace.ID),
|
||||
slog.F("template_id", workspace.TemplateID),
|
||||
slog.Error(err),
|
||||
)
|
||||
} else {
|
||||
next, allowed := autobuild.NextAutostartSchedule(now, workspace.AutostartSchedule.String, templateSchedule)
|
||||
if allowed {
|
||||
nextAutostart = next
|
||||
}
|
||||
}
|
||||
}
|
||||
ActivityBumpWorkspace(ctx, a.Log.Named("activity_bump"), a.Database, workspace.ID, nextAutostart)
|
||||
}
|
||||
|
||||
var errGroup errgroup.Group
|
||||
errGroup.Go(func() error {
|
||||
err := a.StatsBatcher.Add(now, workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, req.Stats)
|
||||
if err != nil {
|
||||
a.Log.Error(ctx, "add agent stats to batcher", slog.Error(err))
|
||||
return xerrors.Errorf("insert workspace agent stats batch: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
err := a.Database.UpdateWorkspaceLastUsedAt(ctx, database.UpdateWorkspaceLastUsedAtParams{
|
||||
ID: workspace.ID,
|
||||
LastUsedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update workspace LastUsedAt: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if a.UpdateAgentMetricsFn != nil {
|
||||
errGroup.Go(func() error {
|
||||
user, err := a.Database.GetUserByID(ctx, workspace.OwnerID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get user: %w", err)
|
||||
}
|
||||
|
||||
a.UpdateAgentMetricsFn(ctx, prometheusmetrics.AgentMetricLabels{
|
||||
Username: user.Username,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: workspaceAgent.Name,
|
||||
TemplateName: getWorkspaceAgentByIDRow.TemplateName,
|
||||
}, req.Stats.Metrics)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
err = errGroup.Wait()
|
||||
err = a.StatsReporter.ReportAgentStats(
|
||||
ctx,
|
||||
a.now(),
|
||||
workspace,
|
||||
workspaceAgent,
|
||||
getWorkspaceAgentByIDRow.TemplateName,
|
||||
req.Stats,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("update stats in database: %w", err)
|
||||
return nil, xerrors.Errorf("report agent stats: %w", err)
|
||||
}
|
||||
|
||||
// Tell the frontend about the new agent report, now that everything is updated
|
||||
a.publishWorkspaceAgentStats(ctx, workspace.ID)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (a *StatsAPI) publishWorkspaceAgentStats(ctx context.Context, workspaceID uuid.UUID) {
|
||||
err := a.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspaceID), []byte{})
|
||||
if err != nil {
|
||||
a.Log.Warn(ctx, "failed to publish workspace agent stats",
|
||||
slog.F("workspace_id", workspaceID), slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||
"github.com/coder/coder/v2/coderd/prometheusmetrics"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/workspacestats"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
@@ -129,21 +130,24 @@ func TestUpdateStates(t *testing.T) {
|
||||
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
|
||||
return agent, nil
|
||||
},
|
||||
Database: dbM,
|
||||
Pubsub: ps,
|
||||
StatsBatcher: batcher,
|
||||
TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore),
|
||||
Database: dbM,
|
||||
StatsReporter: workspacestats.NewReporter(workspacestats.ReporterOptions{
|
||||
Database: dbM,
|
||||
Pubsub: ps,
|
||||
StatsBatcher: batcher,
|
||||
TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore),
|
||||
UpdateAgentMetricsFn: func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) {
|
||||
updateAgentMetricsFnCalled = true
|
||||
assert.Equal(t, prometheusmetrics.AgentMetricLabels{
|
||||
Username: user.Username,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: agent.Name,
|
||||
TemplateName: template.Name,
|
||||
}, labels)
|
||||
assert.Equal(t, req.Stats.Metrics, metrics)
|
||||
},
|
||||
}),
|
||||
AgentStatsRefreshInterval: 10 * time.Second,
|
||||
UpdateAgentMetricsFn: func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) {
|
||||
updateAgentMetricsFnCalled = true
|
||||
assert.Equal(t, prometheusmetrics.AgentMetricLabels{
|
||||
Username: user.Username,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: agent.Name,
|
||||
TemplateName: template.Name,
|
||||
}, labels)
|
||||
assert.Equal(t, req.Stats.Metrics, metrics)
|
||||
},
|
||||
TimeNowFn: func() time.Time {
|
||||
return now
|
||||
},
|
||||
@@ -232,13 +236,16 @@ func TestUpdateStates(t *testing.T) {
|
||||
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
|
||||
return agent, nil
|
||||
},
|
||||
Database: dbM,
|
||||
Pubsub: ps,
|
||||
StatsBatcher: batcher,
|
||||
TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore),
|
||||
Database: dbM,
|
||||
StatsReporter: workspacestats.NewReporter(workspacestats.ReporterOptions{
|
||||
Database: dbM,
|
||||
Pubsub: ps,
|
||||
StatsBatcher: batcher,
|
||||
TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore),
|
||||
// Ignored when nil.
|
||||
UpdateAgentMetricsFn: nil,
|
||||
}),
|
||||
AgentStatsRefreshInterval: 10 * time.Second,
|
||||
// Ignored when nil.
|
||||
UpdateAgentMetricsFn: nil,
|
||||
TimeNowFn: func() time.Time {
|
||||
return now
|
||||
},
|
||||
@@ -274,12 +281,15 @@ func TestUpdateStates(t *testing.T) {
|
||||
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
|
||||
return agent, nil
|
||||
},
|
||||
Database: dbM,
|
||||
Pubsub: ps,
|
||||
StatsBatcher: nil, // should not be called
|
||||
TemplateScheduleStore: nil, // should not be called
|
||||
Database: dbM,
|
||||
StatsReporter: workspacestats.NewReporter(workspacestats.ReporterOptions{
|
||||
Database: dbM,
|
||||
Pubsub: ps,
|
||||
StatsBatcher: nil, // should not be called
|
||||
TemplateScheduleStore: nil, // should not be called
|
||||
UpdateAgentMetricsFn: nil, // should not be called
|
||||
}),
|
||||
AgentStatsRefreshInterval: 10 * time.Second,
|
||||
UpdateAgentMetricsFn: nil, // should not be called
|
||||
TimeNowFn: func() time.Time {
|
||||
panic("should not be called")
|
||||
},
|
||||
@@ -343,21 +353,24 @@ func TestUpdateStates(t *testing.T) {
|
||||
AgentFn: func(context.Context) (database.WorkspaceAgent, error) {
|
||||
return agent, nil
|
||||
},
|
||||
Database: dbM,
|
||||
Pubsub: ps,
|
||||
StatsBatcher: batcher,
|
||||
TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore),
|
||||
Database: dbM,
|
||||
StatsReporter: workspacestats.NewReporter(workspacestats.ReporterOptions{
|
||||
Database: dbM,
|
||||
Pubsub: ps,
|
||||
StatsBatcher: batcher,
|
||||
TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore),
|
||||
UpdateAgentMetricsFn: func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) {
|
||||
updateAgentMetricsFnCalled = true
|
||||
assert.Equal(t, prometheusmetrics.AgentMetricLabels{
|
||||
Username: user.Username,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: agent.Name,
|
||||
TemplateName: template.Name,
|
||||
}, labels)
|
||||
assert.Equal(t, req.Stats.Metrics, metrics)
|
||||
},
|
||||
}),
|
||||
AgentStatsRefreshInterval: 15 * time.Second,
|
||||
UpdateAgentMetricsFn: func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) {
|
||||
updateAgentMetricsFnCalled = true
|
||||
assert.Equal(t, prometheusmetrics.AgentMetricLabels{
|
||||
Username: user.Username,
|
||||
WorkspaceName: workspace.Name,
|
||||
AgentName: agent.Name,
|
||||
TemplateName: template.Name,
|
||||
}, labels)
|
||||
assert.Equal(t, req.Stats.Metrics, metrics)
|
||||
},
|
||||
TimeNowFn: func() time.Time {
|
||||
return now
|
||||
},
|
||||
|
||||
Generated
+398
-88
@@ -1987,6 +1987,82 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Organizations"
|
||||
],
|
||||
"summary": "Delete organization",
|
||||
"operationId": "delete-organization",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization ID or name",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Organizations"
|
||||
],
|
||||
"summary": "Update organization",
|
||||
"operationId": "update-organization",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization ID or name",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Patch organization request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateOrganizationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Organization"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/groups": {
|
||||
@@ -2149,6 +2225,42 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Members"
|
||||
],
|
||||
"summary": "Upsert a custom organization role",
|
||||
"operationId": "upsert-a-custom-organization-role",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}/roles": {
|
||||
@@ -5904,6 +6016,7 @@ const docTemplate = `{
|
||||
],
|
||||
"summary": "Submit workspace agent stats",
|
||||
"operationId": "submit-workspace-agent-stats",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Stats request",
|
||||
@@ -8265,6 +8378,12 @@ const docTemplate = `{
|
||||
"codersdk.AppearanceConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"announcement_banners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
},
|
||||
"application_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8272,7 +8391,12 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"service_banner": {
|
||||
"$ref": "#/definitions/codersdk.ServiceBannerConfig"
|
||||
"description": "Deprecated: ServiceBanner has been replaced by AnnouncementBanners.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"support_links": {
|
||||
"type": "array",
|
||||
@@ -8297,11 +8421,38 @@ const docTemplate = `{
|
||||
"assignable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"built_in": {
|
||||
"description": "BuiltIn roles are immutable",
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"user_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8445,6 +8596,9 @@ const docTemplate = `{
|
||||
},
|
||||
"password": {
|
||||
"$ref": "#/definitions/codersdk.AuthMethod"
|
||||
},
|
||||
"terms_of_service_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8453,12 +8607,16 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"create",
|
||||
"read",
|
||||
"update",
|
||||
"delete"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.RBACAction"
|
||||
}
|
||||
]
|
||||
},
|
||||
"object": {
|
||||
@@ -8526,6 +8684,20 @@ const docTemplate = `{
|
||||
"AutomaticUpdatesNever"
|
||||
]
|
||||
},
|
||||
"codersdk.BannerConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"background_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.BuildInfoResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8537,10 +8709,18 @@ 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"
|
||||
},
|
||||
"telemetry": {
|
||||
"description": "Telemetry is a boolean that indicates whether telemetry is enabled.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"upgrade_message": {
|
||||
"description": "UpgradeMessage is the message displayed to users when an outdated client\nis detected.",
|
||||
"type": "string"
|
||||
@@ -9295,9 +9475,6 @@ const docTemplate = `{
|
||||
"disable_path_apps": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_session_expiry_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"docs_url": {
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
},
|
||||
@@ -9335,12 +9512,6 @@ const docTemplate = `{
|
||||
"logging": {
|
||||
"$ref": "#/definitions/codersdk.LoggingConfig"
|
||||
},
|
||||
"max_session_expiry": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_token_lifetime": {
|
||||
"type": "integer"
|
||||
},
|
||||
"metrics_cache_refresh_interval": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -9392,6 +9563,9 @@ const docTemplate = `{
|
||||
"secure_auth_cookie": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"session_lifetime": {
|
||||
"$ref": "#/definitions/codersdk.SessionLifetime"
|
||||
},
|
||||
"ssh_keygen_algorithm": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -9413,6 +9587,9 @@ const docTemplate = `{
|
||||
"telemetry": {
|
||||
"$ref": "#/definitions/codersdk.TelemetryConfig"
|
||||
},
|
||||
"terms_of_service_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"tls": {
|
||||
"$ref": "#/definitions/codersdk.TLSConfig"
|
||||
},
|
||||
@@ -9512,17 +9689,21 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"example",
|
||||
"shared-ports",
|
||||
"auto-fill-parameters"
|
||||
"auto-fill-parameters",
|
||||
"multi-organization",
|
||||
"custom-roles"
|
||||
],
|
||||
"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."
|
||||
"ExperimentCustomRoles": "Allows creating runtime custom roles",
|
||||
"ExperimentExample": "This isn't used for anything.",
|
||||
"ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed."
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"ExperimentExample",
|
||||
"ExperimentSharedPorts",
|
||||
"ExperimentAutoFillParameters"
|
||||
"ExperimentAutoFillParameters",
|
||||
"ExperimentMultiOrganization",
|
||||
"ExperimentCustomRoles"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
@@ -9605,12 +9786,6 @@ const docTemplate = `{
|
||||
"description": "DisplayName is shown in the UI to identify the auth config.",
|
||||
"type": "string"
|
||||
},
|
||||
"extra_token_keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"description": "ID is a unique identifier for the auth config.\nIt defaults to ` + "`" + `type` + "`" + ` when not provided.",
|
||||
"type": "string"
|
||||
@@ -10337,7 +10512,7 @@ const docTemplate = `{
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"updated_at": {
|
||||
@@ -10417,6 +10592,21 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.Permission": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/codersdk.RBACAction"
|
||||
},
|
||||
"negate": {
|
||||
"description": "Negate makes this a negative permission",
|
||||
"type": "boolean"
|
||||
},
|
||||
"resource_type": {
|
||||
"$ref": "#/definitions/codersdk.RBACResource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PostOAuth2ProviderAppRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -10481,11 +10671,15 @@ const docTemplate = `{
|
||||
"daemon_psk": {
|
||||
"type": "string"
|
||||
},
|
||||
"daemons": {
|
||||
"type": "integer"
|
||||
"daemon_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"daemons_echo": {
|
||||
"type": "boolean"
|
||||
"daemons": {
|
||||
"description": "Daemons is the number of built-in terraform provisioners.",
|
||||
"type": "integer"
|
||||
},
|
||||
"force_cancel_interval": {
|
||||
"type": "integer"
|
||||
@@ -10741,59 +10935,94 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.RBACAction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"application_connect",
|
||||
"assign",
|
||||
"create",
|
||||
"delete",
|
||||
"read",
|
||||
"read_personal",
|
||||
"ssh",
|
||||
"update",
|
||||
"update_personal",
|
||||
"use",
|
||||
"view_insights",
|
||||
"start",
|
||||
"stop"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ActionApplicationConnect",
|
||||
"ActionAssign",
|
||||
"ActionCreate",
|
||||
"ActionDelete",
|
||||
"ActionRead",
|
||||
"ActionReadPersonal",
|
||||
"ActionSSH",
|
||||
"ActionUpdate",
|
||||
"ActionUpdatePersonal",
|
||||
"ActionUse",
|
||||
"ActionViewInsights",
|
||||
"ActionWorkspaceStart",
|
||||
"ActionWorkspaceStop"
|
||||
]
|
||||
},
|
||||
"codersdk.RBACResource": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"workspace",
|
||||
"workspace_proxy",
|
||||
"workspace_execution",
|
||||
"application_connect",
|
||||
"audit_log",
|
||||
"template",
|
||||
"group",
|
||||
"file",
|
||||
"provisioner_daemon",
|
||||
"organization",
|
||||
"assign_role",
|
||||
"assign_org_role",
|
||||
"*",
|
||||
"api_key",
|
||||
"user",
|
||||
"user_data",
|
||||
"user_workspace_build_parameters",
|
||||
"organization_member",
|
||||
"license",
|
||||
"assign_org_role",
|
||||
"assign_role",
|
||||
"audit_log",
|
||||
"debug_info",
|
||||
"deployment_config",
|
||||
"deployment_stats",
|
||||
"file",
|
||||
"group",
|
||||
"license",
|
||||
"oauth2_app",
|
||||
"oauth2_app_code_token",
|
||||
"oauth2_app_secret",
|
||||
"organization",
|
||||
"organization_member",
|
||||
"provisioner_daemon",
|
||||
"replicas",
|
||||
"debug_info",
|
||||
"system",
|
||||
"template_insights"
|
||||
"tailnet_coordinator",
|
||||
"template",
|
||||
"user",
|
||||
"workspace",
|
||||
"workspace_dormant",
|
||||
"workspace_proxy"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ResourceWorkspace",
|
||||
"ResourceWorkspaceProxy",
|
||||
"ResourceWorkspaceExecution",
|
||||
"ResourceWorkspaceApplicationConnect",
|
||||
"ResourceWildcard",
|
||||
"ResourceApiKey",
|
||||
"ResourceAssignOrgRole",
|
||||
"ResourceAssignRole",
|
||||
"ResourceAuditLog",
|
||||
"ResourceTemplate",
|
||||
"ResourceGroup",
|
||||
"ResourceFile",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceOrganization",
|
||||
"ResourceRoleAssignment",
|
||||
"ResourceOrgRoleAssignment",
|
||||
"ResourceAPIKey",
|
||||
"ResourceUser",
|
||||
"ResourceUserData",
|
||||
"ResourceUserWorkspaceBuildParameters",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceLicense",
|
||||
"ResourceDeploymentValues",
|
||||
"ResourceDeploymentStats",
|
||||
"ResourceReplicas",
|
||||
"ResourceDebugInfo",
|
||||
"ResourceDeploymentConfig",
|
||||
"ResourceDeploymentStats",
|
||||
"ResourceFile",
|
||||
"ResourceGroup",
|
||||
"ResourceLicense",
|
||||
"ResourceOauth2App",
|
||||
"ResourceOauth2AppCodeToken",
|
||||
"ResourceOauth2AppSecret",
|
||||
"ResourceOrganization",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceReplicas",
|
||||
"ResourceSystem",
|
||||
"ResourceTemplateInsights"
|
||||
"ResourceTailnetCoordinator",
|
||||
"ResourceTemplate",
|
||||
"ResourceUser",
|
||||
"ResourceWorkspace",
|
||||
"ResourceWorkspaceDormant",
|
||||
"ResourceWorkspaceProxy"
|
||||
]
|
||||
},
|
||||
"codersdk.RateLimitConfig": {
|
||||
@@ -11020,6 +11249,29 @@ const docTemplate = `{
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"user_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -11053,20 +11305,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ServiceBannerConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"background_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.SessionCountDeploymentStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11084,6 +11322,33 @@ 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.SlimRole": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.SupportConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11296,6 +11561,10 @@ const docTemplate = `{
|
||||
"format": "uuid"
|
||||
}
|
||||
},
|
||||
"times_used": {
|
||||
"type": "integer",
|
||||
"example": 2
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -11597,7 +11866,7 @@ const docTemplate = `{
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -11877,6 +12146,12 @@ const docTemplate = `{
|
||||
"codersdk.UpdateAppearanceConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"announcement_banners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
},
|
||||
"application_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -11884,7 +12159,12 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"service_banner": {
|
||||
"$ref": "#/definitions/codersdk.ServiceBannerConfig"
|
||||
"description": "Deprecated: ServiceBanner has been replaced by AnnouncementBanners.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -11905,6 +12185,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateOrganizationRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateRoles": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12123,7 +12414,7 @@ const docTemplate = `{
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -13823,7 +14114,16 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"severity": {
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
"enum": [
|
||||
"ok",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
@@ -13906,7 +14206,7 @@ const docTemplate = `{
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/health.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13921,10 +14221,20 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"healthy": {
|
||||
"description": "Healthy is deprecated and left for backward compatibility purposes, use ` + "`" + `Severity` + "`" + ` instead.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"severity": {
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
"enum": [
|
||||
"ok",
|
||||
"warning",
|
||||
"error"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
@@ -14189,7 +14499,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"host": {
|
||||
"description": "host or host:port",
|
||||
"description": "host or host:port (see Hostname and Port methods)",
|
||||
"type": "string"
|
||||
},
|
||||
"omitHost": {
|
||||
|
||||
Generated
+378
-88
@@ -1732,6 +1732,72 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Organizations"],
|
||||
"summary": "Delete organization",
|
||||
"operationId": "delete-organization",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization ID or name",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Organizations"],
|
||||
"summary": "Update organization",
|
||||
"operationId": "update-organization",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization ID or name",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Patch organization request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateOrganizationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.Organization"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/groups": {
|
||||
@@ -1876,6 +1942,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Members"],
|
||||
"summary": "Upsert a custom organization role",
|
||||
"operationId": "upsert-a-custom-organization-role",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Organization ID",
|
||||
"name": "organization",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/organizations/{organization}/members/{user}/roles": {
|
||||
@@ -5201,6 +5299,7 @@
|
||||
"tags": ["Agents"],
|
||||
"summary": "Submit workspace agent stats",
|
||||
"operationId": "submit-workspace-agent-stats",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Stats request",
|
||||
@@ -7334,6 +7433,12 @@
|
||||
"codersdk.AppearanceConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"announcement_banners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
},
|
||||
"application_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -7341,7 +7446,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"service_banner": {
|
||||
"$ref": "#/definitions/codersdk.ServiceBannerConfig"
|
||||
"description": "Deprecated: ServiceBanner has been replaced by AnnouncementBanners.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"support_links": {
|
||||
"type": "array",
|
||||
@@ -7366,11 +7476,38 @@
|
||||
"assignable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"built_in": {
|
||||
"description": "BuiltIn roles are immutable",
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"user_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7514,6 +7651,9 @@
|
||||
},
|
||||
"password": {
|
||||
"$ref": "#/definitions/codersdk.AuthMethod"
|
||||
},
|
||||
"terms_of_service_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7522,8 +7662,12 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["create", "read", "update", "delete"]
|
||||
"enum": ["create", "read", "update", "delete"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.RBACAction"
|
||||
}
|
||||
]
|
||||
},
|
||||
"object": {
|
||||
"description": "Object can represent a \"set\" of objects, such as: all workspaces in an organization, all workspaces owned by me, and all workspaces across the entire product.\nWhen defining an object, use the most specific language when possible to\nproduce the smallest set. Meaning to set as many fields on 'Object' as\nyou can. Example, if you want to check if you can update all workspaces\nowned by 'me', try to also add an 'OrganizationID' to the settings.\nOmitting the 'OrganizationID' could produce the incorrect value, as\nworkspaces have both `user` and `organization` owners.",
|
||||
@@ -7584,6 +7728,20 @@
|
||||
"enum": ["always", "never"],
|
||||
"x-enum-varnames": ["AutomaticUpdatesAlways", "AutomaticUpdatesNever"]
|
||||
},
|
||||
"codersdk.BannerConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"background_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.BuildInfoResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7595,10 +7753,18 @@
|
||||
"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"
|
||||
},
|
||||
"telemetry": {
|
||||
"description": "Telemetry is a boolean that indicates whether telemetry is enabled.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"upgrade_message": {
|
||||
"description": "UpgradeMessage is the message displayed to users when an outdated client\nis detected.",
|
||||
"type": "string"
|
||||
@@ -8300,9 +8466,6 @@
|
||||
"disable_path_apps": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_session_expiry_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"docs_url": {
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
},
|
||||
@@ -8340,12 +8503,6 @@
|
||||
"logging": {
|
||||
"$ref": "#/definitions/codersdk.LoggingConfig"
|
||||
},
|
||||
"max_session_expiry": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_token_lifetime": {
|
||||
"type": "integer"
|
||||
},
|
||||
"metrics_cache_refresh_interval": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -8397,6 +8554,9 @@
|
||||
"secure_auth_cookie": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"session_lifetime": {
|
||||
"$ref": "#/definitions/codersdk.SessionLifetime"
|
||||
},
|
||||
"ssh_keygen_algorithm": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8418,6 +8578,9 @@
|
||||
"telemetry": {
|
||||
"$ref": "#/definitions/codersdk.TelemetryConfig"
|
||||
},
|
||||
"terms_of_service_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"tls": {
|
||||
"$ref": "#/definitions/codersdk.TLSConfig"
|
||||
},
|
||||
@@ -8511,15 +8674,23 @@
|
||||
},
|
||||
"codersdk.Experiment": {
|
||||
"type": "string",
|
||||
"enum": ["example", "shared-ports", "auto-fill-parameters"],
|
||||
"enum": [
|
||||
"example",
|
||||
"auto-fill-parameters",
|
||||
"multi-organization",
|
||||
"custom-roles"
|
||||
],
|
||||
"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."
|
||||
"ExperimentCustomRoles": "Allows creating runtime custom roles",
|
||||
"ExperimentExample": "This isn't used for anything.",
|
||||
"ExperimentMultiOrganization": "Requires organization context for interactions, default org is assumed."
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"ExperimentExample",
|
||||
"ExperimentSharedPorts",
|
||||
"ExperimentAutoFillParameters"
|
||||
"ExperimentAutoFillParameters",
|
||||
"ExperimentMultiOrganization",
|
||||
"ExperimentCustomRoles"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
@@ -8602,12 +8773,6 @@
|
||||
"description": "DisplayName is shown in the UI to identify the auth config.",
|
||||
"type": "string"
|
||||
},
|
||||
"extra_token_keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"description": "ID is a unique identifier for the auth config.\nIt defaults to `type` when not provided.",
|
||||
"type": "string"
|
||||
@@ -9281,7 +9446,7 @@
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"updated_at": {
|
||||
@@ -9356,6 +9521,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.Permission": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/codersdk.RBACAction"
|
||||
},
|
||||
"negate": {
|
||||
"description": "Negate makes this a negative permission",
|
||||
"type": "boolean"
|
||||
},
|
||||
"resource_type": {
|
||||
"$ref": "#/definitions/codersdk.RBACResource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PostOAuth2ProviderAppRequest": {
|
||||
"type": "object",
|
||||
"required": ["callback_url", "name"],
|
||||
@@ -9417,11 +9597,15 @@
|
||||
"daemon_psk": {
|
||||
"type": "string"
|
||||
},
|
||||
"daemons": {
|
||||
"type": "integer"
|
||||
"daemon_types": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"daemons_echo": {
|
||||
"type": "boolean"
|
||||
"daemons": {
|
||||
"description": "Daemons is the number of built-in terraform provisioners.",
|
||||
"type": "integer"
|
||||
},
|
||||
"force_cancel_interval": {
|
||||
"type": "integer"
|
||||
@@ -9651,59 +9835,94 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.RBACAction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"application_connect",
|
||||
"assign",
|
||||
"create",
|
||||
"delete",
|
||||
"read",
|
||||
"read_personal",
|
||||
"ssh",
|
||||
"update",
|
||||
"update_personal",
|
||||
"use",
|
||||
"view_insights",
|
||||
"start",
|
||||
"stop"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ActionApplicationConnect",
|
||||
"ActionAssign",
|
||||
"ActionCreate",
|
||||
"ActionDelete",
|
||||
"ActionRead",
|
||||
"ActionReadPersonal",
|
||||
"ActionSSH",
|
||||
"ActionUpdate",
|
||||
"ActionUpdatePersonal",
|
||||
"ActionUse",
|
||||
"ActionViewInsights",
|
||||
"ActionWorkspaceStart",
|
||||
"ActionWorkspaceStop"
|
||||
]
|
||||
},
|
||||
"codersdk.RBACResource": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"workspace",
|
||||
"workspace_proxy",
|
||||
"workspace_execution",
|
||||
"application_connect",
|
||||
"audit_log",
|
||||
"template",
|
||||
"group",
|
||||
"file",
|
||||
"provisioner_daemon",
|
||||
"organization",
|
||||
"assign_role",
|
||||
"assign_org_role",
|
||||
"*",
|
||||
"api_key",
|
||||
"user",
|
||||
"user_data",
|
||||
"user_workspace_build_parameters",
|
||||
"organization_member",
|
||||
"license",
|
||||
"assign_org_role",
|
||||
"assign_role",
|
||||
"audit_log",
|
||||
"debug_info",
|
||||
"deployment_config",
|
||||
"deployment_stats",
|
||||
"file",
|
||||
"group",
|
||||
"license",
|
||||
"oauth2_app",
|
||||
"oauth2_app_code_token",
|
||||
"oauth2_app_secret",
|
||||
"organization",
|
||||
"organization_member",
|
||||
"provisioner_daemon",
|
||||
"replicas",
|
||||
"debug_info",
|
||||
"system",
|
||||
"template_insights"
|
||||
"tailnet_coordinator",
|
||||
"template",
|
||||
"user",
|
||||
"workspace",
|
||||
"workspace_dormant",
|
||||
"workspace_proxy"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ResourceWorkspace",
|
||||
"ResourceWorkspaceProxy",
|
||||
"ResourceWorkspaceExecution",
|
||||
"ResourceWorkspaceApplicationConnect",
|
||||
"ResourceWildcard",
|
||||
"ResourceApiKey",
|
||||
"ResourceAssignOrgRole",
|
||||
"ResourceAssignRole",
|
||||
"ResourceAuditLog",
|
||||
"ResourceTemplate",
|
||||
"ResourceGroup",
|
||||
"ResourceFile",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceOrganization",
|
||||
"ResourceRoleAssignment",
|
||||
"ResourceOrgRoleAssignment",
|
||||
"ResourceAPIKey",
|
||||
"ResourceUser",
|
||||
"ResourceUserData",
|
||||
"ResourceUserWorkspaceBuildParameters",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceLicense",
|
||||
"ResourceDeploymentValues",
|
||||
"ResourceDeploymentStats",
|
||||
"ResourceReplicas",
|
||||
"ResourceDebugInfo",
|
||||
"ResourceDeploymentConfig",
|
||||
"ResourceDeploymentStats",
|
||||
"ResourceFile",
|
||||
"ResourceGroup",
|
||||
"ResourceLicense",
|
||||
"ResourceOauth2App",
|
||||
"ResourceOauth2AppCodeToken",
|
||||
"ResourceOauth2AppSecret",
|
||||
"ResourceOrganization",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceReplicas",
|
||||
"ResourceSystem",
|
||||
"ResourceTemplateInsights"
|
||||
"ResourceTailnetCoordinator",
|
||||
"ResourceTemplate",
|
||||
"ResourceUser",
|
||||
"ResourceWorkspace",
|
||||
"ResourceWorkspaceDormant",
|
||||
"ResourceWorkspaceProxy"
|
||||
]
|
||||
},
|
||||
"codersdk.RateLimitConfig": {
|
||||
@@ -9922,6 +10141,29 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_permissions": {
|
||||
"description": "OrganizationPermissions are specific for the organization in the field 'OrganizationID' above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"site_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
},
|
||||
"user_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9955,20 +10197,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ServiceBannerConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"background_color": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.SessionCountDeploymentStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9986,6 +10214,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.SlimRole": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.SupportConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10196,6 +10451,10 @@
|
||||
"format": "uuid"
|
||||
}
|
||||
},
|
||||
"times_used": {
|
||||
"type": "integer",
|
||||
"example": 2
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -10479,7 +10738,7 @@
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -10736,6 +10995,12 @@
|
||||
"codersdk.UpdateAppearanceConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"announcement_banners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
},
|
||||
"application_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10743,7 +11008,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"service_banner": {
|
||||
"$ref": "#/definitions/codersdk.ServiceBannerConfig"
|
||||
"description": "Deprecated: ServiceBanner has been replaced by AnnouncementBanners.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.BannerConfig"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -10764,6 +11034,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateOrganizationRequest": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateRoles": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10962,7 +11241,7 @@
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Role"
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -12563,7 +12842,12 @@
|
||||
}
|
||||
},
|
||||
"severity": {
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
"enum": ["ok", "warning", "error"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
@@ -12642,7 +12926,7 @@
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/health.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12657,10 +12941,16 @@
|
||||
"type": "string"
|
||||
},
|
||||
"healthy": {
|
||||
"description": "Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"severity": {
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
"enum": ["ok", "warning", "error"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/health.Severity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
@@ -12925,7 +13215,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"host": {
|
||||
"description": "host or host:port",
|
||||
"description": "host or host:port (see Hostname and Port methods)",
|
||||
"type": "string"
|
||||
},
|
||||
"omitHost": {
|
||||
|
||||
+7
-7
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/telemetry"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
@@ -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.SessionDuration.Value(),
|
||||
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.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.SessionDuration.Value(),
|
||||
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
|
||||
LoginType: database.LoginTypePassword,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
// All api generated keys will last 1 week. Browser login tokens have
|
||||
@@ -255,7 +255,7 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
keys, err = AuthorizeFilter(api.HTTPAuth, r, rbac.ActionRead, keys)
|
||||
keys, err = AuthorizeFilter(api.HTTPAuth, r, policy.ActionRead, keys)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching keys.",
|
||||
@@ -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.MaxTokenLifetime.Value(),
|
||||
MaxTokenLifetime: values.Sessions.MaximumTokenDuration.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.MaxTokenLifetime.Value() {
|
||||
if lifetime > api.DeploymentValues.Sessions.MaximumTokenDuration.Value() {
|
||||
return xerrors.Errorf(
|
||||
"lifetime must be less than %v",
|
||||
api.DeploymentValues.MaxTokenLifetime,
|
||||
api.DeploymentValues.Sessions.MaximumTokenDuration,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestTokenUserSetMaxLifetime(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
dc := coderdtest.DeploymentValues(t)
|
||||
dc.MaxTokenLifetime = serpent.Duration(time.Hour * 24 * 7)
|
||||
dc.Sessions.MaximumTokenDuration = 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.SessionDuration = serpent.Duration(time.Second)
|
||||
dc.Sessions.DefaultDuration = 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.SessionDuration.Value().Seconds(), apiKey.LifetimeSeconds)
|
||||
require.WithinDuration(t, apiKey.CreatedAt.Add(dc.SessionDuration.Value()), apiKey.ExpiresAt, 2*time.Second)
|
||||
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)
|
||||
|
||||
// Update the session token to be expired so we can test that it is
|
||||
// rejected for extra points.
|
||||
|
||||
@@ -32,7 +32,8 @@ type AGPLFetcher struct{}
|
||||
|
||||
func (AGPLFetcher) Fetch(context.Context) (codersdk.AppearanceConfig, error) {
|
||||
return codersdk.AppearanceConfig{
|
||||
SupportLinks: DefaultSupportLinks,
|
||||
AnnouncementBanners: []codersdk.BannerConfig{},
|
||||
SupportLinks: DefaultSupportLinks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -196,12 +196,12 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
|
||||
CreatedAt: dblog.UserCreatedAt.Time,
|
||||
Status: codersdk.UserStatus(dblog.UserStatus.UserStatus),
|
||||
},
|
||||
Roles: []codersdk.Role{},
|
||||
Roles: []codersdk.SlimRole{},
|
||||
}
|
||||
|
||||
for _, roleName := range dblog.UserRoles {
|
||||
rbacRole, _ := rbac.RoleByName(roleName)
|
||||
user.Roles = append(user.Roles, db2sdk.Role(rbacRole))
|
||||
user.Roles = append(user.Roles, db2sdk.SlimRole(rbacRole))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:"workpace_id"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id"`
|
||||
}
|
||||
|
||||
func NewNop() Auditor {
|
||||
|
||||
@@ -233,6 +233,26 @@ func requireOrgID[T Auditable](ctx context.Context, id uuid.UUID, log slog.Logge
|
||||
return id
|
||||
}
|
||||
|
||||
// InitRequestWithCancel returns a commit function with a boolean arg.
|
||||
// If the arg is false, future calls to commit() will not create an audit log
|
||||
// entry.
|
||||
func InitRequestWithCancel[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request[T], func(commit bool)) {
|
||||
req, commitF := InitRequest[T](w, p)
|
||||
cancelled := false
|
||||
return req, func(commit bool) {
|
||||
// Once 'commit=false' is called, block
|
||||
// any future commit attempts.
|
||||
if !commit {
|
||||
cancelled = true
|
||||
return
|
||||
}
|
||||
// If it was ever cancelled, block any commits
|
||||
if !cancelled {
|
||||
commitF()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitRequest initializes an audit log for a request. It returns a function
|
||||
// that should be deferred, causing the audit log to be committed when the
|
||||
// handler returns.
|
||||
|
||||
+8
-13
@@ -11,13 +11,14 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// AuthorizeFilter takes a list of objects and returns the filtered list of
|
||||
// objects that the user is authorized to perform the given action on.
|
||||
// This is faster than calling Authorize() on each object.
|
||||
func AuthorizeFilter[O rbac.Objecter](h *HTTPAuthorizer, r *http.Request, action rbac.Action, objects []O) ([]O, error) {
|
||||
func AuthorizeFilter[O rbac.Objecter](h *HTTPAuthorizer, r *http.Request, action policy.Action, objects []O) ([]O, error) {
|
||||
roles := httpmw.UserAuthorization(r)
|
||||
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles, action, objects)
|
||||
if err != nil {
|
||||
@@ -50,7 +51,7 @@ type HTTPAuthorizer struct {
|
||||
// httpapi.Forbidden(rw)
|
||||
// return
|
||||
// }
|
||||
func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
|
||||
func (api *API) Authorize(r *http.Request, action policy.Action, object rbac.Objecter) bool {
|
||||
return api.HTTPAuth.Authorize(r, action, object)
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objec
|
||||
// httpapi.Forbidden(rw)
|
||||
// return
|
||||
// }
|
||||
func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
|
||||
func (h *HTTPAuthorizer) Authorize(r *http.Request, action policy.Action, object rbac.Objecter) bool {
|
||||
roles := httpmw.UserAuthorization(r)
|
||||
err := h.Authorizer.Authorize(r.Context(), roles, action, object.RBACObject())
|
||||
if err != nil {
|
||||
@@ -95,7 +96,7 @@ func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object r
|
||||
// from postgres are already authorized, and the caller does not need to
|
||||
// call 'Authorize()' on the returned objects.
|
||||
// Note the authorization is only for the given action and object type.
|
||||
func (h *HTTPAuthorizer) AuthorizeSQLFilter(r *http.Request, action rbac.Action, objectType string) (rbac.PreparedAuthorized, error) {
|
||||
func (h *HTTPAuthorizer) AuthorizeSQLFilter(r *http.Request, action policy.Action, objectType string) (rbac.PreparedAuthorized, error) {
|
||||
roles := httpmw.UserAuthorization(r)
|
||||
prepared, err := h.Authorizer.Prepare(r.Context(), roles, action, objectType)
|
||||
if err != nil {
|
||||
@@ -168,7 +169,7 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
|
||||
obj := rbac.Object{
|
||||
Owner: v.Object.OwnerID,
|
||||
OrgID: v.Object.OrganizationID,
|
||||
Type: v.Object.ResourceType.String(),
|
||||
Type: string(v.Object.ResourceType),
|
||||
}
|
||||
if obj.Owner == "me" {
|
||||
obj.Owner = auth.ID
|
||||
@@ -188,13 +189,7 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
|
||||
var dbObj rbac.Objecter
|
||||
var dbErr error
|
||||
// Only support referencing some resources by ID.
|
||||
switch v.Object.ResourceType.String() {
|
||||
case rbac.ResourceWorkspaceExecution.Type:
|
||||
wrkSpace, err := api.Database.GetWorkspaceByID(ctx, id)
|
||||
if err == nil {
|
||||
dbObj = wrkSpace.ExecutionRBAC()
|
||||
}
|
||||
dbErr = err
|
||||
switch string(v.Object.ResourceType) {
|
||||
case rbac.ResourceWorkspace.Type:
|
||||
dbObj, dbErr = api.Database.GetWorkspaceByID(ctx, id)
|
||||
case rbac.ResourceTemplate.Type:
|
||||
@@ -219,7 +214,7 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
|
||||
obj = dbObj.RBACObject()
|
||||
}
|
||||
|
||||
err := api.Authorizer.Authorize(ctx, auth, rbac.Action(v.Action), obj)
|
||||
err := api.Authorizer.Authorize(ctx, auth, policy.Action(v.Action), obj)
|
||||
response[k] = err == nil
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -368,7 +367,7 @@ func isEligibleForAutostart(user database.User, ws database.Workspace, build dat
|
||||
return false
|
||||
}
|
||||
|
||||
nextTransition, allowed := NextAutostartSchedule(build.CreatedAt, ws.AutostartSchedule.String, templateSchedule)
|
||||
nextTransition, allowed := schedule.NextAutostart(build.CreatedAt, ws.AutostartSchedule.String, templateSchedule)
|
||||
if !allowed {
|
||||
return false
|
||||
}
|
||||
@@ -377,29 +376,6 @@ 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 {
|
||||
|
||||
@@ -185,6 +185,142 @@ QYLbNYkedkNuhRmEBesPqj4aDz68ZDI6fJ92sj2q18QvJUJ5Qz728AvtFOat+Ajg
|
||||
K0PFqPYEAviUKr162NB1XZJxf6uyIjUlnG4UEdHfUqdhl0R84mMtrYINksTzQ2sH
|
||||
YM8fEhqICtTlcRLr/FErUaPUe9648nziSnA0qKH7rUZqP/Ifmbo+WNZSZG1BbgOh
|
||||
lk+521W+Ncih3HRbvRBE0LWYT8vWKnfjgZKxwHwJ
|
||||
-----END CERTIFICATE-----`,
|
||||
// Microsoft Azure RSA TLS Issuing CA 03
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIFrDCCBJSgAwIBAgIQBRllJkSaXj0aOHSPXc/rzDANBgkqhkiG9w0BAQwFADBh
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
|
||||
MjAeFw0yMzA2MDgwMDAwMDBaFw0yNjA4MjUyMzU5NTlaMF0xCzAJBgNVBAYTAlVT
|
||||
MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLjAsBgNVBAMTJU1pY3Jv
|
||||
c29mdCBBenVyZSBSU0EgVExTIElzc3VpbmcgQ0EgMDMwggIiMA0GCSqGSIb3DQEB
|
||||
AQUAA4ICDwAwggIKAoICAQCUaitvevlZirydcTjMIt2fr5ei7LvQx7bdIVobgEZ1
|
||||
Qlqf3BH6etKdmZChydkN0XXAb8Ysew8aCixKtrVeDCe5xRRCnKaFcEvqg2cSfbpX
|
||||
FevXDvfbTK2ed7YASOJ/pv31stqHd9m0xWZLCmsXZ8x6yIxgEGVHjIAOCyTAgcQy
|
||||
8ItIjmxn3Vu2FFVBemtP38Nzur/8id85uY7QPspI8Er8qVBBBHp6PhxTIKxAZpZb
|
||||
XtBf2VxIKbvUGEvCxWCrKNfv+j0oEqDpXOqGFpVBK28Q48u/0F+YBUY8FKP4rfgF
|
||||
I4lG9mnzMmCL76k+HjyBtU5zikDGqgm4mlPXgSRqEh0CvQS7zyrBRWiJCfK0g67f
|
||||
69CVGa7fji8pz99J59s8bYW7jgyro93LCGb4N3QfJLurB//ehDp33XdIhizJtopj
|
||||
UoFUGLnomVnMRTUNtMSAy7J4r1yjJDLufgnrPZ0yjYo6nyMiFswCaMmFfclUKtGz
|
||||
zbPDpIBuf0hmvJAt0LyWlYUst5geusPxbkM5XOhLn7px+/y+R0wMT3zNZYQxlsLD
|
||||
bXGYsRdE9jxcIts+IQwWZGnmHhhC1kvKC/nAYcqBZctMQB5q/qsPH652dc73zOx6
|
||||
Bp2gTZqokGCv5PGxiXcrwouOUIlYgizBDYGBDU02S4BRDM3oW9motVUonBnF8JHV
|
||||
RwIDAQABo4IBYjCCAV4wEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU/glx
|
||||
QFUFEETYpIF1uJ4a6UoGiMgwHwYDVR0jBBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYX
|
||||
jzkwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjB2BggrBgEFBQcBAQRqMGgwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
|
||||
ZXJ0LmNvbTBABggrBgEFBQcwAoY0aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
|
||||
L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNydDBCBgNVHR8EOzA5MDegNaAzhjFodHRw
|
||||
Oi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxSb290RzIuY3JsMB0G
|
||||
A1UdIAQWMBQwCAYGZ4EMAQIBMAgGBmeBDAECAjANBgkqhkiG9w0BAQwFAAOCAQEA
|
||||
AQkxu6RRPlD3yrYhxg9jIlVZKjAnC9H+D0SSq4j1I8dNImZ4QjexTEv+224CSvy4
|
||||
zfp9gmeRfC8rnrr4FN4UFppYIgqR4H7jIUVMG9ECUcQj2Ef11RXqKOg5LK3fkoFz
|
||||
/Nb9CYvg4Ws9zv8xmE1Mr2N6WDgLuTBIwul2/7oakjj8MA5EeijIjHgB1/0r5mPm
|
||||
eFYVx8xCuX/j7+q4tH4PiHzzBcfqb3k0iR4DlhiZfDmy4FuNWXGM8ZoMM43EnRN/
|
||||
meqAcMkABZhY4gqeWZbOgxber297PnGOCcIplOwpPfLu1A1K9frVwDzAG096a8L0
|
||||
+ItQCmz7TjRH4ptX5Zh9pw==
|
||||
-----END CERTIFICATE-----`,
|
||||
// Microsoft Azure RSA TLS Issuing CA 04
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIFrDCCBJSgAwIBAgIQCfluwpVVXyR0nq8eXc7UnTANBgkqhkiG9w0BAQwFADBh
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
|
||||
MjAeFw0yMzA2MDgwMDAwMDBaFw0yNjA4MjUyMzU5NTlaMF0xCzAJBgNVBAYTAlVT
|
||||
MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLjAsBgNVBAMTJU1pY3Jv
|
||||
c29mdCBBenVyZSBSU0EgVExTIElzc3VpbmcgQ0EgMDQwggIiMA0GCSqGSIb3DQEB
|
||||
AQUAA4ICDwAwggIKAoICAQDBeUy13eRZ/QC5bN7/IOGxodny7Xm2BFc88d3cca3y
|
||||
HyyVx1Y60+afY6DAo/2Ls1uzAfbDfMzAVWJazPH4tckaItDv//htEbbNJnAGvZPB
|
||||
4VqNviwDEmlAWT/MTAmzXfTgWXuUNgRlzZbjoFaPm+t6iJ6HdvDpWQAJbsBUZCga
|
||||
t257tM28JnAHUTWdiDBn+2z6EGh2DA6BCx04zHDKVSegLY8+5P80Lqze0d6i3T2J
|
||||
J7rfxCmxUXfCGOv9iQIUZfhv4vCb8hsm/JdNUMiomJhSPa0bi3rda/swuJHCH//d
|
||||
wz2AGzZRRGdj7Kna4t6ToxK17lAF3Q6Qp368C9cE6JLMj+3UbY3umWCPRA5/Dms4
|
||||
/wl3GvDEw7HpyKsvRNPpjDZyiFzZGC2HZmGMsrZMT3hxmyQwmz1O3eGYdO5EIq1S
|
||||
W/vT1yShZTSusqmICQo5gWWRZTwCENekSbVX9qRr77o0pjKtuBMZTGQTixwpT/rg
|
||||
Ul7Mr4M2nqK55Kovy/kUN1znfPdW/Fj9iCuvPKwKFdyt2RVgxJDvgIF/bNoRkRxh
|
||||
wVB6qRgs4EiTrNbRoZAHEFF5wRBf9gWn9HeoI66VtdMZvJRH+0/FDWB4/zwxS16n
|
||||
nADJaVPXh6JHJFYs9p0wZmvct3GNdWrOLRAG2yzbfFZS8fJcX1PYxXXo4By16yGW
|
||||
hQIDAQABo4IBYjCCAV4wEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUO3DR
|
||||
U+l2JZ1gqMpmD8abrm9UFmowHwYDVR0jBBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYX
|
||||
jzkwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjB2BggrBgEFBQcBAQRqMGgwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
|
||||
ZXJ0LmNvbTBABggrBgEFBQcwAoY0aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
|
||||
L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNydDBCBgNVHR8EOzA5MDegNaAzhjFodHRw
|
||||
Oi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxSb290RzIuY3JsMB0G
|
||||
A1UdIAQWMBQwCAYGZ4EMAQIBMAgGBmeBDAECAjANBgkqhkiG9w0BAQwFAAOCAQEA
|
||||
o9sJvBNLQSJ1e7VaG3cSZHBz6zjS70A1gVO1pqsmX34BWDPz1TAlOyJiLlA+eUF4
|
||||
B2OWHd3F//dJJ/3TaCFunjBhZudv3busl7flz42K/BG/eOdlg0kiUf07PCYY5/FK
|
||||
YTIch51j1moFlBqbglwkdNIVae2tOu0OdX2JiA+bprYcGxa7eayLetvPiA77ynTc
|
||||
UNMKOqYB41FZHOXe5IXDI5t2RsDM9dMEZv4+cOb9G9qXcgDar1AzPHEt/39335zC
|
||||
HofQ0QuItCDCDzahWZci9Nn9hb/SvAtPWHZLkLBG6I0iwGxvMwcTTc9Jnb4Flysr
|
||||
mQlwKsS2MphOoI23Qq3cSA==
|
||||
-----END CERTIFICATE-----`,
|
||||
// Microsoft Azure RSA TLS Issuing CA 07
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIFrDCCBJSgAwIBAgIQCkOpUJsBNS+JlXnscgi6UDANBgkqhkiG9w0BAQwFADBh
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
|
||||
MjAeFw0yMzA2MDgwMDAwMDBaFw0yNjA4MjUyMzU5NTlaMF0xCzAJBgNVBAYTAlVT
|
||||
MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLjAsBgNVBAMTJU1pY3Jv
|
||||
c29mdCBBenVyZSBSU0EgVExTIElzc3VpbmcgQ0EgMDcwggIiMA0GCSqGSIb3DQEB
|
||||
AQUAA4ICDwAwggIKAoICAQC1ZF7KYus5OO3GWqJoR4xznLDNCjocogqeCIVdi4eE
|
||||
BmF3zIYeuXXNoJAUF+mn86NBt3yMM0559JZDkiSDi9MpA2By4yqQlTHzfbOrvs7I
|
||||
4LWsOYTEClVFQgzXqa2ps2g855HPQW1hZXVh/yfmbtrCNVa//G7FPDqSdrAQ+M8w
|
||||
0364kyZApds/RPcqGORjZNokrNzYcGub27vqE6BGP6XeQO5YDFobi9BvvTOO+ZA9
|
||||
HGIU7FbdLhRm6YP+FO8NRpvterfqZrRt3bTn8GT5LsOTzIQgJMt4/RWLF4EKNc97
|
||||
CXOSCZFn7mFNx4SzTvy23B46z9dQPfWBfTFaxU5pIa0uVWv+jFjG7l1odu0WZqBd
|
||||
j0xnvXggu564CXmLz8F3draOH6XS7Ys9sTVM3Ow20MJyHtuA3hBDv+tgRhrGvNRD
|
||||
MbSzTO6axNWvL46HWVEChHYlxVBCTfSQmpbcAdZOQtUfs9E4sCFrqKcRPdg7ryhY
|
||||
fGbj3q0SLh55559ITttdyYE+wE4RhODgILQ3MaYZoyiL1E/4jqCOoRaFhF5R++vb
|
||||
YpemcpWx7unptfOpPRRnnN4U3pqZDj4yXexcyS52Rd8BthFY/cBg8XIR42BPeVRl
|
||||
OckZ+ttduvKVbvmGf+rFCSUoy1tyRwQNXzqeZTLrX+REqgFDOMVe0I49Frc2/Avw
|
||||
3wIDAQABo4IBYjCCAV4wEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUzhUW
|
||||
O+oCo6Zr2tkr/eWMUr56UKgwHwYDVR0jBBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYX
|
||||
jzkwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjB2BggrBgEFBQcBAQRqMGgwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
|
||||
ZXJ0LmNvbTBABggrBgEFBQcwAoY0aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
|
||||
L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNydDBCBgNVHR8EOzA5MDegNaAzhjFodHRw
|
||||
Oi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxSb290RzIuY3JsMB0G
|
||||
A1UdIAQWMBQwCAYGZ4EMAQIBMAgGBmeBDAECAjANBgkqhkiG9w0BAQwFAAOCAQEA
|
||||
bbV8m4/LCSvb0nBF9jb7MVLH/9JjHGbn0QjB4R4bMlGHbDXDWtW9pFqMPrRh2Q76
|
||||
Bqm+yrrgX83jPZAcvOd7F7+lzDxZnYoFEWhxW9WnuM8Te5x6HBPCPRbIuzf9pSUT
|
||||
/ozvbKFCDxxgC2xKmgp6NwxRuGcy5KQQh4xkq/hJrnnF3RLakrkUBYFPUneip+wS
|
||||
BzAfK3jHXnkNCPNvKeLIXfLMsffEzP/j8hFkjWL3oh5yaj1HmlW8RE4Tl/GdUVzQ
|
||||
D1x42VSusQuRGtuSxLhzBNBeJtyD//2u7wY2uLYpgK0o3X0iIJmwpt7Ovp6Bs4tI
|
||||
E/peia+Qcdk9Qsr+1VgCGA==
|
||||
-----END CERTIFICATE-----`,
|
||||
// Microsoft Azure RSA TLS Issuing CA 08
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIFrDCCBJSgAwIBAgIQDvt+VH7fD/EGmu5XaW17oDANBgkqhkiG9w0BAQwFADBh
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
|
||||
MjAeFw0yMzA2MDgwMDAwMDBaFw0yNjA4MjUyMzU5NTlaMF0xCzAJBgNVBAYTAlVT
|
||||
MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLjAsBgNVBAMTJU1pY3Jv
|
||||
c29mdCBBenVyZSBSU0EgVExTIElzc3VpbmcgQ0EgMDgwggIiMA0GCSqGSIb3DQEB
|
||||
AQUAA4ICDwAwggIKAoICAQCy7oIFzcDVZVbomWZtSwrAX8LiKXsbCcwuFL7FHkD5
|
||||
m67olmOdTueOKhNER5ykFs/meKG1fwzd35/+Q1+KTxcV89IIXmErtSsj8EWu7rdE
|
||||
AVYnYMFbstqwkIVNEoz4OIM82hn+N5p57zkHGPogzF6TOPRUOK8yYyCPeqnHvoVp
|
||||
E5b0kZL4QT8bdyhSRQbUsUiSaOuF5y3eZ9Vc92baDkhY7CFZE2ThLLv5PQ0WxzLo
|
||||
t3t18d2vQP5x29I0n6NFsj37J2d/EH/Z6a/lhAVzKjfYloGcQ1IPyDEIGh9gYJnM
|
||||
LFZiUbm/GBmlpKVr8M03OWKCR0thRbfnU6UoskrwGrECAnnojFEUw+j8i6gFLBNW
|
||||
XtBOtYvgl8SHCCVKUUUl4YOfR5zF4OkKirJuUbOmB2AOmLjYJIcabDvxMcmryhQi
|
||||
nog+/+jgHJnY62opgStkdaImMPzyLB7ZaWVnxpRdtFKO1ZvGkZeRNvbPAUKR2kNe
|
||||
knuh3NtFvz2dY3xP7AfhyLE/t8vW72nAzlRKz++L70CgCvj/yeObPwaAPDd2sZ0o
|
||||
j2u/N+k6egGq04e+GBW+QYCSoJ5eAY36il0fu7dYSHYDo7RB5aPTLqnybp8wMeAa
|
||||
tcagc8U9OM42ghELTaWFARuyoCmgqR7y8fAU9Njhcqrm6+0Xzv/vzMfhL4Ulpf1G
|
||||
7wIDAQABo4IBYjCCAV4wEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU9n4v
|
||||
vYCjSrJwW+vfmh/Y7cphgAcwHwYDVR0jBBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYX
|
||||
jzkwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
|
||||
AjB2BggrBgEFBQcBAQRqMGgwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
|
||||
ZXJ0LmNvbTBABggrBgEFBQcwAoY0aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
|
||||
L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNydDBCBgNVHR8EOzA5MDegNaAzhjFodHRw
|
||||
Oi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxSb290RzIuY3JsMB0G
|
||||
A1UdIAQWMBQwCAYGZ4EMAQIBMAgGBmeBDAECAjANBgkqhkiG9w0BAQwFAAOCAQEA
|
||||
loABcB94CeH6DWKwa4550BTzLxlTHVNseQJ5SetnPpBuPNLPgOLe9Y7ZMn4ZK6mh
|
||||
feK7RiMzan4UF9CD5rF3TcCevo3IxrdV+YfBwvlbGYv+6JmX3mAMlaUb23Y2pONo
|
||||
ixFJEOcAMKKR55mSC5W4nQ6jDfp7Qy/504MQpdjJflk90RHsIZGXVPw/JdbBp0w6
|
||||
pDb4o5CqydmZqZMrEvbGk1p8kegFkBekp/5WVfd86BdH2xs+GKO3hyiA8iBrBCGJ
|
||||
fqrijbRnZm7q5+ydXF3jhJDJWfxW5EBYZBJrUz/a+8K/78BjwI8z2VYJpG4t6r4o
|
||||
tOGB5sEyDPDwqx00Rouu8g==
|
||||
-----END CERTIFICATE-----`,
|
||||
// Microsoft Azure TLS Issuing CA 01
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +15,11 @@ 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)
|
||||
@@ -35,6 +41,11 @@ func TestValidate(t *testing.T) {
|
||||
payload: "MIILiQYJKoZIhvcNAQcCoIILejCCC3YCAQExDzANBgkqhkiG9w0BAQsFADCCAUAGCSqGSIb3DQEHAaCCATEEggEteyJsaWNlbnNlVHlwZSI6IiIsIm5vbmNlIjoiMjAyMzAzMDgtMjMwOTMzIiwicGxhbiI6eyJuYW1lIjoiIiwicHJvZHVjdCI6IiIsInB1Ymxpc2hlciI6IiJ9LCJza3UiOiIxOC4wNC1MVFMiLCJzdWJzY3JpcHRpb25JZCI6IjBhZmJmZmZhLTVkZjktNGEzYi05ODdlLWZlNzU3NzYyNDI3MiIsInRpbWVTdGFtcCI6eyJjcmVhdGVkT24iOiIwMy8wOC8yMyAxNzowOTozMyAtMDAwMCIsImV4cGlyZXNPbiI6IjAzLzA4LzIzIDIzOjA5OjMzIC0wMDAwIn0sInZtSWQiOiI5OTA4NzhkNC0wNjhhLTRhYzQtOWVlOS0xMjMxZDIyMThlZjIifaCCCHswggh3MIIGX6ADAgECAhMzAIXQK9n2YdJHP1paAAAAhdArMA0GCSqGSIb3DQEBDAUAMFkxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKjAoBgNVBAMTIU1pY3Jvc29mdCBBenVyZSBUTFMgSXNzdWluZyBDQSAwNTAeFw0yMzAyMDMxOTAxMThaFw0yNDAxMjkxOTAxMThaMGgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJXQTEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMRowGAYDVQQDExFtZXRhZGF0YS5henVyZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMrbkY7Z8ffglHPokuGfRDOBjFt6n68OuReoq2CbnhyEdosDsfJBsoCr5vV3mVcpil1+y0HeabKr+PdJ6GWCXiymxxgMtNMIuz/kt4OVOJSkV3wJyMNYRjGUAB53jw2cJnhIgLy6QmxOm2cnDb+IBFGn7WAw/XqT8taDd6RPDHR6P+XqpWuMN/MheCOdJRagmr8BUNt95eOhRAGZeUWHKcCssBa9xZNmTzgd26NuBRpeGVrjuPCaQXiGWXvJ7zujWOiMopgw7UWXMiJp6J+Nn75Dx+MbPjlLYYBhFEEBaXj0iKuj/3/lm3nkkMLcYPxEJE0lPuX1yQQLUx3l1bBYyykCAwEAAaOCBCcwggQjMIIBfQYKKwYBBAHWeQIEAgSCAW0EggFpAWcAdgDuzdBk1dsazsVct520zROiModGfLzs3sNRSFlGcR+1mwAAAYYYsLzVAAAEAwBHMEUCIQD+BaiDS1uFyVGdeMc5vBUpJOmBhxgRyTkH3kQG+KD6RwIgWIMxqyGtmM9rH5CrWoruToiz7NNfDmp11LLHZNaKpq4AdgBz2Z6JG0yWeKAgfUed5rLGHNBRXnEZKoxrgBB6wXdytQAAAYYYsL0bAAAEAwBHMEUCIQDNxRWECEZmEk9zRmRPNv3QP0lDsUzaKhYvFPmah/wkKwIgXyCv+fvWga+XB2bcKQqom10nvTDBExIZeoOWBSfKVLgAdQB2/4g/Crb7lVHCYcz1h7o0tKTNuyncaEIKn+ZnTFo6dAAAAYYYsL0bAAAEAwBGMEQCICCTSeyEisZwmi49g941B6exndOFwF4JqtoXbWmFcxRcAiBCDaVJJN0e0ZVSPkx9NVMGWvBjQbIYtSG4LEkCdDsMejAnBgkrBgEEAYI3FQoEGjAYMAoGCCsGAQUFBwMCMAoGCCsGAQUFBwMBMDwGCSsGAQQBgjcVBwQvMC0GJSsGAQQBgjcVCIe91xuB5+tGgoGdLo7QDIfw2h1dgoTlaYLzpz4CAWQCASUwga4GCCsGAQUFBwEBBIGhMIGeMG0GCCsGAQUFBzAChmFodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMEF6dXJlJTIwVExTJTIwSXNzdWluZyUyMENBJTIwMDUlMjAtJTIweHNpZ24uY3J0MC0GCCsGAQUFBzABhiFodHRwOi8vb25lb2NzcC5taWNyb3NvZnQuY29tL29jc3AwHQYDVR0OBBYEFBcZK26vkjWcbAk7XwJHTP/lxgeXMA4GA1UdDwEB/wQEAwIEsDA9BgNVHREENjA0gh91c2dvdnZpcmdpbmlhLm1ldGFkYXRhLmF6dXJlLnVzghFtZXRhZGF0YS5henVyZS51czAMBgNVHRMBAf8EAjAAMGQGA1UdHwRdMFswWaBXoFWGU2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMEF6dXJlJTIwVExTJTIwSXNzdWluZyUyMENBJTIwMDUuY3JsMGYGA1UdIARfMF0wUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTAIBgZngQwBAgIwHwYDVR0jBBgwFoAUx7KcfxzjuFrv6WgaqF2UwSZSamgwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA0GCSqGSIb3DQEBDAUAA4ICAQCUExuLe7D71C5kek65sqKXUodQJXVVpFG0Y4l9ZacBFql8BgHvu2Qvt8zfWsyCHy4A2KcMeHLwi2DdspyTjxSnwkuPcQ4ndhgAqrLkfoTc435NnnsiyzCUNDeGIQ+g+QSRPV86u6LmvFr0ZaOqxp6eJDPYewHhKyGLQuUyBjUNkhS+tGzuvsHaeCUYclmbZFN75IQSvBmL0XOsOD7wXPZB1a68D26wyCIbIC8MuFwxreTrvdRKt/5zIfBnku6S6xRgkzH64gfBLbU5e2VCdaKzElWEKRLJgl3R6raNRqFot+XNfa26H5sMZpZkuHrvkPZcvd5zOfL7fnVZoMLo4A3kFpet7tr1ls0ifqodzlOBMNrUdf+o3kJ1seCjzx2WdFP+2liO80d0oHKiv8djuttlPfQkV8WATmyLoZVoPcNovayrVUjTWFMXqIShhhTbIJ3ZRSZrz6rZLok0Xin3+4d28iMsi7tjxnBW/A/eiPrqs7f2v2rLXuf5/XHuzHIYQpiZpnvA90mE1HBB9fv4sETsw9TuL2nXai/c06HGGM06i4o+lRuyvymrlt/QPR7SCPXl5fZFVAavLtu1UtafrK/qcKQTHnVJeZ20+JdDIJDP2qcxQvdw7XA88aa/Y/olM+yHIjpaPpsRFa2o8UB0ct+x1cTAhLhj3vNwhZHoFlVcFzGCAZswggGXAgEBMHAwWTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEqMCgGA1UEAxMhTWljcm9zb2Z0IEF6dXJlIFRMUyBJc3N1aW5nIENBIDA1AhMzAIXQK9n2YdJHP1paAAAAhdArMA0GCSqGSIb3DQEBCwUAMA0GCSqGSIb3DQEBAQUABIIBAFuEf//loqaib860Ys5yZkrRj1QiSDSzkU+Vxx9fYXzWzNT4KgMhkEhRRvoE6TR/tIUzbKFQxIVRrlW2lbGSj8JEeLoEVlp2Pc4gNRJeX2N9qVDPvy9lmYuBm1XjypLPwvYjvfPjsLRKkNdQ5MWzrC3F2q2OOQP4sviy/DCcoDitEmqmqiCuog/DiS5xETivde3pTZGiFwKlgzptj4/KYN/iZTzU25fFSCD5Mq2IxHRj39gFkqpFekdSRihSH0W3oyPfic/E3H0rVtSkiFm2SL6nPjILjhaJcV7az+X7Qu4AXYZ/TrabX+OW5dJ69SoJ01DfnqGD0sll0+P3QSUHEvA=",
|
||||
vmID: "990878d4-068a-4ac4-9ee9-1231d2218ef2",
|
||||
date: mustTime(time.RFC3339, "2023-04-01T00:00:00Z"),
|
||||
}, {
|
||||
name: "rsa",
|
||||
payload: "MIILnwYJKoZIhvcNAQcCoIILkDCCC4wCAQExDzANBgkqhkiG9w0BAQsFADCCAUUGCSqGSIb3DQEHAaCCATYEggEyeyJsaWNlbnNlVHlwZSI6IiIsIm5vbmNlIjoiMjAyNDA0MjItMjMzMjQ1IiwicGxhbiI6eyJuYW1lIjoiIiwicHJvZHVjdCI6IiIsInB1Ymxpc2hlciI6IiJ9LCJza3UiOiIyMF8wNC1sdHMtZ2VuMiIsInN1YnNjcmlwdGlvbklkIjoiMDVlOGIyODUtNGNlMS00NmEzLWI0YzktZjUxYmE2N2Q2YWNjIiwidGltZVN0YW1wIjp7ImNyZWF0ZWRPbiI6IjA0LzIyLzI0IDE3OjMyOjQ1IC0wMDAwIiwiZXhwaXJlc09uIjoiMDQvMjIvMjQgMjM6MzI6NDUgLTAwMDAifSwidm1JZCI6Ijk2MGE0YjRhLWRhYjItNDRlZi05YjczLTc3NTMwNDNiNGYxNiJ9oIIIiDCCCIQwggZsoAMCAQICEzMAJtj/yBIW1kk+vsIAAAAm2P8wDQYJKoZIhvcNAQEMBQAwXTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEuMCwGA1UEAxMlTWljcm9zb2Z0IEF6dXJlIFJTQSBUTFMgSXNzdWluZyBDQSAwODAeFw0yNDA0MTgwODM1MzdaFw0yNTA0MTMwODM1MzdaMGkxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJXQTEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMRswGQYDVQQDExJtZXRhZGF0YS5henVyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0T031XgxaebNQjKFQZ4BudeN+wOEHQoFq/x+cKSXM8HJrC2pF8y/ngSsuCLGt72M+30KxdbPHl56kd52uwDw1ZBrQO6Xw+GorRbtM4YQi+gLr8t9x+GUfuOX7E+5juidXax7la5ZhpVVLb3f+8NyxbphvEdFadXcgyQga1pl4v1U8elkbX3PPtEQXzwYotU+RU/ZTwXMYqfvJuaKwc4T2s083kaL3DwAfVxL0f6ey/MXuNQb4+ho15y9/f9gwMyzMDLlYChmY6cGSS4tsyrG5SrybE3jl8LZ1ZLVJ2fAIxbmJzBn1q+Eu4G6TZlnMDEsjznf7gqnP+n/o7N6l0sY1AgMBAAGjggQvMIIEKzCCAX4GCisGAQQB1nkCBAIEggFuBIIBagFoAHYAzxFW7tUufK/zh1vZaS6b6RpxZ0qwF+ysAdJbd87MOwgAAAGO8GIJ/QAABAMARzBFAiEAvJQ2mDRow9TMvLddWpYqNXLiehSFsj2+xUqh8yP/B8YCIBJjVoELj3kdVr3ceAuZFte9FH6sBsgeMsIgfndho6hRAHUAfVkeEuF4KnscYWd8Xv340IdcFKBOlZ65Ay/ZDowuebgAAAGO8GIK2AAABAMARjBEAiAxXD1R9yLASrpMh4ie0wn3AjCoSPniZ8virEVz8tKnkwIgWxGU9DjjQk7gPWYVBsiXP9t1WPJ6mNJ1UkmAw8iDdFoAdwBVgdTCFpA2AUrqC5tXPFPwwOQ4eHAlCBcvo6odBxPTDAAAAY7wYgrtAAAEAwBIMEYCIQCaSjdXbUhrDyPNsRqewp5UdVYABGQAIgNwfKsq/JpbmAIhAPy5qQ6H2enXwuKsorEZTwIkKIoMgLsWs4anx9lXTJMeMCcGCSsGAQQBgjcVCgQaMBgwCgYIKwYBBQUHAwIwCgYIKwYBBQUHAwEwPAYJKwYBBAGCNxUHBC8wLQYlKwYBBAGCNxUIh73XG4Hn60aCgZ0ujtAMh/DaHV2ChOVpgvOnPgIBZAIBJjCBtAYIKwYBBQUHAQEEgacwgaQwcwYIKwYBBQUHMAKGZ2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQXp1cmUlMjBSU0ElMjBUTFMlMjBJc3N1aW5nJTIwQ0ElMjAwOCUyMC0lMjB4c2lnbi5jcnQwLQYIKwYBBQUHMAGGIWh0dHA6Ly9vbmVvY3NwLm1pY3Jvc29mdC5jb20vb2NzcDAdBgNVHQ4EFgQUnqRq3WHOZDoNmLD/arJg9RscxLowDgYDVR0PAQH/BAQDAgWgMDgGA1UdEQQxMC+CGWVhc3R1cy5tZXRhZGF0YS5henVyZS5jb22CEm1ldGFkYXRhLmF6dXJlLmNvbTAMBgNVHRMBAf8EAjAAMGoGA1UdHwRjMGEwX6BdoFuGWWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMEF6dXJlJTIwUlNBJTIwVExTJTIwSXNzdWluZyUyMENBJTIwMDguY3JsMGYGA1UdIARfMF0wUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTAIBgZngQwBAgIwHwYDVR0jBBgwFoAU9n4vvYCjSrJwW+vfmh/Y7cphgAcwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA0GCSqGSIb3DQEBDAUAA4ICAQB4FwyqZFVdmB9Hu+YUJOJrGUYRlXbnCmdXlLi5w2QRCf9RKIykGdv28dH1ezhXJUCj3jCVZMav4GaSl0dPUcTetfnc/UrwsmbGRIMubbGjCz75FcNz/kXy7E/jPeyJrxsuO/ijyZNUSy0EQF3NuhTJw/SfAQtXv48NmVFDM2QMMhMRLDfOV4CPcialAFACFQTt6LMdG2hlB972Bffl+BVPkUKDLj89xQRd/cyWYweYfPCsNLYLDml98rY3v4yVKAvv+l7IOuKOzhlOe9U1oPJK7AP7GZzojKrisPQt4HlP4zEmeUzJtL6RqGdHac7/lUMVPOniE/L+5gBDBsN3nOGJ/QE+bBsmfdn4ewuLj6/LCd/JhCZFDeyTvtuX43JWIr9e0UOtENCG3Ub4SuUftf58+NuedCaNMZW2jqrFvQl+sCX+v1kkxxmRphU7B8TZP0SHaBDqeIqHPNWD7eyn/7+VTY54wrwF1v5S6b5zpL1tjZ55c9wpVBT6m77mNuR/2l7/VSh/qL2LgKVVo06q+Qz2c0pIjOI+7FobLRNtb7C8SqkdwuT1b0vnZslA8ZUEtwUm5RHcGu66sg/hb4lGNZbAklxGeAR3uQju0OQN/Lj4kXiii737dci0lIpIKA92hUKybLrYCyZDhp5I6is0gTdm4+rxVEY1K39R3cF3U5thuzGCAZ8wggGbAgEBMHQwXTELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEuMCwGA1UEAxMlTWljcm9zb2Z0IEF6dXJlIFJTQSBUTFMgSXNzdWluZyBDQSAwOAITMwAm2P/IEhbWST6+wgAAACbY/zANBgkqhkiG9w0BAQsFADANBgkqhkiG9w0BAQEFAASCAQDRukRXI01EvAoF0J+C1aYCmjwAtMlnQr5fBKod8T75FhM+mTJ2GApCyc5H8hn7IDl8ki8DdKfLjipnuEvjknZcVkfrzE72R9Pu+C2ffKfrSsJmsBHPMEKBPtlzhexCYiPamMGdVg8HqX6mhQkjjavk1SY+ewZvyEeuq+RSQIBVL1lw0UOWv+txDKlu9v69skb1DQ2HSet0sejEb48vqGeN4TMSoQFNeBOzHDkEeoqXxtZqsUhMtQzbwrpAFcUREB8DaCOXcv1DOminJB3Q19bpuMQ/2+Fc3HJtTTWRV3+3b7VnQl/sUDzTjcWXvwjrLGKk3MSTcQ+1rJRlBzkOJ+aK",
|
||||
vmID: "960a4b4a-dab2-44ef-9b73-7753043b4f16",
|
||||
date: mustTime(time.RFC3339, "2024-04-22T17:32:44Z"),
|
||||
}} {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -53,7 +64,11 @@ func TestValidate(t *testing.T) {
|
||||
|
||||
func TestExpiresSoon(t *testing.T) {
|
||||
t.Parallel()
|
||||
const threshold = 2
|
||||
// TODO (@kylecarbs): It's unknown why Microsoft does not have new certificates live...
|
||||
// The certificate is automatically fetched if it's not found in our database,
|
||||
// so in a worst-case scenario expired certificates will only impact 100% airgapped users.
|
||||
t.Skip()
|
||||
const threshold = 1
|
||||
|
||||
for _, c := range azureidentity.Certificates {
|
||||
block, rest := pem.Decode([]byte(c))
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# See: https://learn.microsoft.com/en-us/azure/security/fundamentals/azure-ca-details?tabs=certificate-authority-chains
|
||||
declare -a CERTIFICATES=(
|
||||
"Microsoft RSA TLS CA 01=https://crt.sh/?d=3124375355"
|
||||
"Microsoft RSA TLS CA 02=https://crt.sh/?d=3124375356"
|
||||
"Microsoft Azure RSA TLS Issuing CA 03=https://www.microsoft.com/pkiops/certs/Microsoft%20Azure%20RSA%20TLS%20Issuing%20CA%2003%20-%20xsign.crt"
|
||||
"Microsoft Azure RSA TLS Issuing CA 04=https://www.microsoft.com/pkiops/certs/Microsoft%20Azure%20RSA%20TLS%20Issuing%20CA%2004%20-%20xsign.crt"
|
||||
"Microsoft Azure RSA TLS Issuing CA 07=https://www.microsoft.com/pkiops/certs/Microsoft%20Azure%20RSA%20TLS%20Issuing%20CA%2007%20-%20xsign.crt"
|
||||
"Microsoft Azure RSA TLS Issuing CA 08=https://www.microsoft.com/pkiops/certs/Microsoft%20Azure%20RSA%20TLS%20Issuing%20CA%2008%20-%20xsign.crt"
|
||||
"Microsoft Azure TLS Issuing CA 01=https://www.microsoft.com/pki/certs/Microsoft%20Azure%20TLS%20Issuing%20CA%2001.cer"
|
||||
"Microsoft Azure TLS Issuing CA 02=https://www.microsoft.com/pki/certs/Microsoft%20Azure%20TLS%20Issuing%20CA%2002.cer"
|
||||
"Microsoft Azure TLS Issuing CA 05=https://www.microsoft.com/pki/certs/Microsoft%20Azure%20TLS%20Issuing%20CA%2005.cer"
|
||||
"Microsoft Azure TLS Issuing CA 06=https://www.microsoft.com/pki/certs/Microsoft%20Azure%20TLS%20Issuing%20CA%2006.cer"
|
||||
)
|
||||
|
||||
CONTENT="var Certificates = []string{"
|
||||
|
||||
for CERT in "${CERTIFICATES[@]}"; do
|
||||
IFS="=" read -r NAME URL <<<"$CERT"
|
||||
echo "Downloading certificate: $NAME"
|
||||
PEM=$(curl -sSL "$URL" | openssl x509 -outform PEM)
|
||||
echo "$PEM"
|
||||
|
||||
CONTENT+="\n// $NAME\n\`$PEM\`,"
|
||||
done
|
||||
|
||||
CONTENT+="\n}"
|
||||
|
||||
sed -i '/var Certificates = /,$d' azureidentity.go
|
||||
# shellcheck disable=SC2059
|
||||
printf "$CONTENT" >>azureidentity.go
|
||||
gofmt -w azureidentity.go
|
||||
@@ -240,6 +240,7 @@ func (b *Batcher) flush(ctx context.Context, forced bool, reason string) {
|
||||
b.buf.ConnectionsByProto = payload
|
||||
}
|
||||
|
||||
// nolint:gocritic // (#13146) Will be moved soon as part of refactor.
|
||||
err = b.store.InsertWorkspaceAgentStats(ctx, *b.buf)
|
||||
elapsed := time.Since(start)
|
||||
if err != nil {
|
||||
|
||||
+56
-22
@@ -60,12 +60,15 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/prometheusmetrics"
|
||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/telemetry"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/coderd/updatecheck"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps"
|
||||
"github.com/coder/coder/v2/coderd/workspacestats"
|
||||
"github.com/coder/coder/v2/coderd/workspaceusage"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/drpc"
|
||||
@@ -422,6 +425,7 @@ func New(options *Options) *API {
|
||||
TemplateScheduleStore: options.TemplateScheduleStore,
|
||||
UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore,
|
||||
AccessControlStore: options.AccessControlStore,
|
||||
CustomRoleHandler: atomic.Pointer[CustomRoleHandler]{},
|
||||
Experiments: experiments,
|
||||
healthCheckGroup: &singleflight.Group[string, *healthsdk.HealthcheckReport]{},
|
||||
Acquirer: provisionerdserver.NewAcquirer(
|
||||
@@ -434,8 +438,20 @@ func New(options *Options) *API {
|
||||
workspaceUsageTracker: options.WorkspaceUsageTracker,
|
||||
}
|
||||
|
||||
var customRoleHandler CustomRoleHandler = &agplCustomRoleHandler{}
|
||||
api.CustomRoleHandler.Store(&customRoleHandler)
|
||||
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,
|
||||
Telemetry: api.Telemetry.Enabled(),
|
||||
}
|
||||
api.SiteHandler = site.New(&site.Options{
|
||||
BinFS: binFS,
|
||||
BinHashes: binHashes,
|
||||
@@ -444,6 +460,7 @@ func New(options *Options) *API {
|
||||
OAuth2Configs: oauthConfigs,
|
||||
DocsURL: options.DeploymentValues.DocsURL.String(),
|
||||
AppearanceFetcher: &api.AppearanceFetcher,
|
||||
BuildInfo: buildInfo,
|
||||
})
|
||||
api.SiteHandler.Experiments.Store(&experiments)
|
||||
|
||||
@@ -535,13 +552,22 @@ func New(options *Options) *API {
|
||||
api.Logger.Fatal(api.ctx, "failed to initialize tailnet client service", slog.Error(err))
|
||||
}
|
||||
|
||||
api.statsReporter = workspacestats.NewReporter(workspacestats.ReporterOptions{
|
||||
Database: options.Database,
|
||||
Logger: options.Logger.Named("workspacestats"),
|
||||
Pubsub: options.Pubsub,
|
||||
TemplateScheduleStore: options.TemplateScheduleStore,
|
||||
StatsBatcher: options.StatsBatcher,
|
||||
UpdateAgentMetricsFn: options.UpdateAgentMetrics,
|
||||
AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize,
|
||||
})
|
||||
workspaceAppsLogger := options.Logger.Named("workspaceapps")
|
||||
if options.WorkspaceAppsStatsCollectorOptions.Logger == nil {
|
||||
named := workspaceAppsLogger.Named("stats_collector")
|
||||
options.WorkspaceAppsStatsCollectorOptions.Logger = &named
|
||||
}
|
||||
if options.WorkspaceAppsStatsCollectorOptions.Reporter == nil {
|
||||
options.WorkspaceAppsStatsCollectorOptions.Reporter = workspaceapps.NewStatsDBReporter(options.Database, workspaceapps.DefaultStatsDBReporterBatchSize)
|
||||
options.WorkspaceAppsStatsCollectorOptions.Reporter = api.statsReporter
|
||||
}
|
||||
|
||||
api.workspaceAppServer = &workspaceapps.Server{
|
||||
@@ -566,7 +592,7 @@ func New(options *Options) *API {
|
||||
DB: options.Database,
|
||||
OAuth2Configs: oauthConfigs,
|
||||
RedirectToLogin: false,
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.DisableSessionExpiryRefresh.Value(),
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
|
||||
Optional: false,
|
||||
SessionTokenFunc: nil, // Default behavior
|
||||
PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc,
|
||||
@@ -576,7 +602,7 @@ func New(options *Options) *API {
|
||||
DB: options.Database,
|
||||
OAuth2Configs: oauthConfigs,
|
||||
RedirectToLogin: true,
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.DisableSessionExpiryRefresh.Value(),
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
|
||||
Optional: false,
|
||||
SessionTokenFunc: nil, // Default behavior
|
||||
PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc,
|
||||
@@ -586,7 +612,7 @@ func New(options *Options) *API {
|
||||
DB: options.Database,
|
||||
OAuth2Configs: oauthConfigs,
|
||||
RedirectToLogin: false,
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.DisableSessionExpiryRefresh.Value(),
|
||||
DisableSessionExpiryRefresh: options.DeploymentValues.Sessions.DisableExpiryRefresh.Value(),
|
||||
Optional: true,
|
||||
SessionTokenFunc: nil, // Default behavior
|
||||
PostAuthAdditionalHeadersFunc: options.PostAuthAdditionalHeadersFunc,
|
||||
@@ -611,8 +637,6 @@ func New(options *Options) *API {
|
||||
cors := httpmw.Cors(options.DeploymentValues.Dangerous.AllowAllCors.Value())
|
||||
prometheusMW := httpmw.Prometheus(options.PrometheusRegistry)
|
||||
|
||||
api.statsBatcher = options.StatsBatcher
|
||||
|
||||
r.Use(
|
||||
httpmw.Recover(api.Logger),
|
||||
tracing.StatusWriterMiddleware,
|
||||
@@ -620,6 +644,7 @@ func New(options *Options) *API {
|
||||
httpmw.AttachRequestID,
|
||||
httpmw.ExtractRealIP(api.RealIPConfig),
|
||||
httpmw.Logger(api.Logger),
|
||||
rolestore.CustomRoleMW,
|
||||
prometheusMW,
|
||||
// Build-Version is helpful for debugging.
|
||||
func(next http.Handler) http.Handler {
|
||||
@@ -735,7 +760,7 @@ func New(options *Options) *API {
|
||||
// All CSP errors will be logged
|
||||
r.Post("/csp/reports", api.logReportCSPViolations)
|
||||
|
||||
r.Get("/buildinfo", buildInfo(api.AccessURL, api.DeploymentValues.CLIUpgradeMessage.String()))
|
||||
r.Get("/buildinfo", buildInfoHandler(buildInfo))
|
||||
// /regions is overridden in the enterprise version
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
@@ -799,6 +824,8 @@ func New(options *Options) *API {
|
||||
httpmw.ExtractOrganizationParam(options.Database),
|
||||
)
|
||||
r.Get("/", api.organization)
|
||||
r.Patch("/", api.patchOrganization)
|
||||
r.Delete("/", api.deleteOrganization)
|
||||
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
|
||||
r.Route("/templates", func(r chi.Router) {
|
||||
r.Post("/", api.postTemplateByOrganization)
|
||||
@@ -813,7 +840,12 @@ func New(options *Options) *API {
|
||||
})
|
||||
})
|
||||
r.Route("/members", func(r chi.Router) {
|
||||
r.Get("/roles", api.assignableOrgRoles)
|
||||
r.Route("/roles", func(r chi.Router) {
|
||||
r.Get("/", api.assignableOrgRoles)
|
||||
r.With(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentCustomRoles)).
|
||||
Patch("/", api.patchOrgRoles)
|
||||
})
|
||||
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractOrganizationMemberParam(options.Database),
|
||||
@@ -904,7 +936,7 @@ func New(options *Options) *API {
|
||||
r.Post("/logout", api.postLogout)
|
||||
// These routes query information about site wide roles.
|
||||
r.Route("/roles", func(r chi.Router) {
|
||||
r.Get("/", api.assignableSiteRoles)
|
||||
r.Get("/", api.AssignableSiteRoles)
|
||||
})
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractUserParam(options.Database))
|
||||
@@ -1045,9 +1077,6 @@ 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)
|
||||
@@ -1099,7 +1128,7 @@ func New(options *Options) *API {
|
||||
// Ensure only owners can access debug endpoints.
|
||||
func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDebugInfo) {
|
||||
if !api.Authorize(r, policy.ActionRead, rbac.ResourceDebugInfo) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
@@ -1237,6 +1266,8 @@ type API struct {
|
||||
// passed to dbauthz.
|
||||
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||
PortSharer atomic.Pointer[portsharing.PortSharer]
|
||||
// CustomRoleHandler is the AGPL/Enterprise implementation for custom roles.
|
||||
CustomRoleHandler atomic.Pointer[CustomRoleHandler]
|
||||
|
||||
HTTPAuth *HTTPAuthorizer
|
||||
|
||||
@@ -1265,7 +1296,7 @@ type API struct {
|
||||
healthCheckGroup *singleflight.Group[string, *healthsdk.HealthcheckReport]
|
||||
healthCheckCache atomic.Pointer[healthsdk.HealthcheckReport]
|
||||
|
||||
statsBatcher *batchstats.Batcher
|
||||
statsReporter *workspacestats.Reporter
|
||||
|
||||
Acquirer *provisionerdserver.Acquirer
|
||||
// dbRolluper rolls up template usage stats from raw agent and app
|
||||
@@ -1341,7 +1372,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) (client proto.DRPCProvisionerDaemonClient, err error) {
|
||||
func (api *API) CreateInMemoryProvisionerDaemon(dialCtx context.Context, name string, provisionerTypes []codersdk.ProvisionerType) (client proto.DRPCProvisionerDaemonClient, err error) {
|
||||
tracer := api.TracerProvider.Tracer(tracing.TracerName)
|
||||
clientSession, serverSession := drpc.MemTransportPipe()
|
||||
defer func() {
|
||||
@@ -1358,18 +1389,21 @@ 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: []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(),
|
||||
Provisioners: dbTypes,
|
||||
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)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/rbac/regosql"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
@@ -84,7 +85,7 @@ func (a RBACAsserter) AllCalls() []AuthCall {
|
||||
// AssertChecked will assert a given rbac check was performed. It does not care
|
||||
// about order of checks, or any other checks. This is useful when you do not
|
||||
// care about asserting every check that was performed.
|
||||
func (a RBACAsserter) AssertChecked(t *testing.T, action rbac.Action, objects ...interface{}) {
|
||||
func (a RBACAsserter) AssertChecked(t *testing.T, action policy.Action, objects ...interface{}) {
|
||||
converted := a.convertObjects(t, objects...)
|
||||
pairs := make([]ActionObjectPair, 0, len(converted))
|
||||
for _, obj := range converted {
|
||||
@@ -95,7 +96,7 @@ func (a RBACAsserter) AssertChecked(t *testing.T, action rbac.Action, objects ..
|
||||
|
||||
// AssertInOrder must be called in the correct order of authz checks. If the objects
|
||||
// or actions are not in the correct order, the test will fail.
|
||||
func (a RBACAsserter) AssertInOrder(t *testing.T, action rbac.Action, objects ...interface{}) {
|
||||
func (a RBACAsserter) AssertInOrder(t *testing.T, action policy.Action, objects ...interface{}) {
|
||||
converted := a.convertObjects(t, objects...)
|
||||
pairs := make([]ActionObjectPair, 0, len(converted))
|
||||
for _, obj := range converted {
|
||||
@@ -155,13 +156,13 @@ type RecordingAuthorizer struct {
|
||||
}
|
||||
|
||||
type ActionObjectPair struct {
|
||||
Action rbac.Action
|
||||
Action policy.Action
|
||||
Object rbac.Object
|
||||
}
|
||||
|
||||
// Pair is on the RecordingAuthorizer to be easy to find and keep the pkg
|
||||
// interface smaller.
|
||||
func (*RecordingAuthorizer) Pair(action rbac.Action, object rbac.Objecter) ActionObjectPair {
|
||||
func (*RecordingAuthorizer) Pair(action policy.Action, object rbac.Objecter) ActionObjectPair {
|
||||
return ActionObjectPair{
|
||||
Action: action,
|
||||
Object: object.RBACObject(),
|
||||
@@ -248,7 +249,7 @@ func (r *RecordingAuthorizer) AssertActor(t *testing.T, actor rbac.Subject, did
|
||||
}
|
||||
|
||||
// recordAuthorize is the internal method that records the Authorize() call.
|
||||
func (r *RecordingAuthorizer) recordAuthorize(subject rbac.Subject, action rbac.Action, object rbac.Object) {
|
||||
func (r *RecordingAuthorizer) recordAuthorize(subject rbac.Subject, action policy.Action, object rbac.Object) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
@@ -283,7 +284,7 @@ func caller(skip int) string {
|
||||
return str
|
||||
}
|
||||
|
||||
func (r *RecordingAuthorizer) Authorize(ctx context.Context, subject rbac.Subject, action rbac.Action, object rbac.Object) error {
|
||||
func (r *RecordingAuthorizer) Authorize(ctx context.Context, subject rbac.Subject, action policy.Action, object rbac.Object) error {
|
||||
r.recordAuthorize(subject, action, object)
|
||||
if r.Wrapped == nil {
|
||||
panic("Developer error: RecordingAuthorizer.Wrapped is nil")
|
||||
@@ -291,7 +292,7 @@ func (r *RecordingAuthorizer) Authorize(ctx context.Context, subject rbac.Subjec
|
||||
return r.Wrapped.Authorize(ctx, subject, action, object)
|
||||
}
|
||||
|
||||
func (r *RecordingAuthorizer) Prepare(ctx context.Context, subject rbac.Subject, action rbac.Action, objectType string) (rbac.PreparedAuthorized, error) {
|
||||
func (r *RecordingAuthorizer) Prepare(ctx context.Context, subject rbac.Subject, action policy.Action, objectType string) (rbac.PreparedAuthorized, error) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
if r.Wrapped == nil {
|
||||
@@ -325,7 +326,7 @@ type PreparedRecorder struct {
|
||||
rec *RecordingAuthorizer
|
||||
prepped rbac.PreparedAuthorized
|
||||
subject rbac.Subject
|
||||
action rbac.Action
|
||||
action policy.Action
|
||||
|
||||
rw sync.Mutex
|
||||
usingSQL bool
|
||||
@@ -357,11 +358,11 @@ type FakeAuthorizer struct {
|
||||
|
||||
var _ rbac.Authorizer = (*FakeAuthorizer)(nil)
|
||||
|
||||
func (d *FakeAuthorizer) Authorize(_ context.Context, _ rbac.Subject, _ rbac.Action, _ rbac.Object) error {
|
||||
func (d *FakeAuthorizer) Authorize(_ context.Context, _ rbac.Subject, _ policy.Action, _ rbac.Object) error {
|
||||
return d.AlwaysReturn
|
||||
}
|
||||
|
||||
func (d *FakeAuthorizer) Prepare(_ context.Context, subject rbac.Subject, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
|
||||
func (d *FakeAuthorizer) Prepare(_ context.Context, subject rbac.Subject, action policy.Action, _ string) (rbac.PreparedAuthorized, error) {
|
||||
return &fakePreparedAuthorizer{
|
||||
Original: d,
|
||||
Subject: subject,
|
||||
@@ -377,7 +378,7 @@ type fakePreparedAuthorizer struct {
|
||||
sync.RWMutex
|
||||
Original *FakeAuthorizer
|
||||
Subject rbac.Subject
|
||||
Action rbac.Action
|
||||
Action policy.Action
|
||||
}
|
||||
|
||||
func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Object) error {
|
||||
@@ -392,7 +393,7 @@ func (*fakePreparedAuthorizer) CompileToSQL(_ context.Context, _ regosql.Convert
|
||||
|
||||
// Random rbac helper funcs
|
||||
|
||||
func RandomRBACAction() rbac.Action {
|
||||
func RandomRBACAction() policy.Action {
|
||||
all := rbac.AllActions()
|
||||
return all[must(cryptorand.Intn(len(all)))]
|
||||
}
|
||||
@@ -403,10 +404,10 @@ func RandomRBACObject() rbac.Object {
|
||||
Owner: uuid.NewString(),
|
||||
OrgID: uuid.NewString(),
|
||||
Type: randomRBACType(),
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
ACLUserList: map[string][]policy.Action{
|
||||
namesgenerator.GetRandomName(1): {RandomRBACAction()},
|
||||
},
|
||||
ACLGroupList: map[string][]rbac.Action{
|
||||
ACLGroupList: map[string][]policy.Action{
|
||||
namesgenerator.GetRandomName(1): {RandomRBACAction()},
|
||||
},
|
||||
}
|
||||
@@ -415,23 +416,16 @@ func RandomRBACObject() rbac.Object {
|
||||
func randomRBACType() string {
|
||||
all := []string{
|
||||
rbac.ResourceWorkspace.Type,
|
||||
rbac.ResourceWorkspaceExecution.Type,
|
||||
rbac.ResourceWorkspaceApplicationConnect.Type,
|
||||
rbac.ResourceAuditLog.Type,
|
||||
rbac.ResourceTemplate.Type,
|
||||
rbac.ResourceGroup.Type,
|
||||
rbac.ResourceFile.Type,
|
||||
rbac.ResourceProvisionerDaemon.Type,
|
||||
rbac.ResourceOrganization.Type,
|
||||
rbac.ResourceRoleAssignment.Type,
|
||||
rbac.ResourceOrgRoleAssignment.Type,
|
||||
rbac.ResourceAPIKey.Type,
|
||||
rbac.ResourceUser.Type,
|
||||
rbac.ResourceUserData.Type,
|
||||
rbac.ResourceOrganizationMember.Type,
|
||||
rbac.ResourceWildcard.Type,
|
||||
rbac.ResourceLicense.Type,
|
||||
rbac.ResourceDeploymentValues.Type,
|
||||
rbac.ResourceReplicas.Type,
|
||||
rbac.ResourceDebugInfo.Type,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
)
|
||||
|
||||
func TestAuthzRecorder(t *testing.T) {
|
||||
@@ -101,7 +102,7 @@ func TestAuthzRecorder(t *testing.T) {
|
||||
}
|
||||
|
||||
// fuzzAuthzPrep has same action and object types for all calls.
|
||||
func fuzzAuthzPrep(t *testing.T, prep rbac.PreparedAuthorized, n int, action rbac.Action, objectType string) []coderdtest.ActionObjectPair {
|
||||
func fuzzAuthzPrep(t *testing.T, prep rbac.PreparedAuthorized, n int, action policy.Action, objectType string) []coderdtest.ActionObjectPair {
|
||||
t.Helper()
|
||||
pairs := make([]coderdtest.ActionObjectPair, 0, n)
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
}
|
||||
|
||||
if options.Authorizer == nil {
|
||||
defAuth := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
|
||||
defAuth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
||||
if _, ok := t.(*testing.T); ok {
|
||||
options.Authorizer = &RecordingAuthorizer{
|
||||
Wrapped: defAuth,
|
||||
@@ -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")
|
||||
return coderAPI.CreateInMemoryProvisionerDaemon(dialCtx, "test", []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho})
|
||||
}, &provisionerd.Options{
|
||||
Logger: coderAPI.Logger.Named("provisionerd").Leveled(slog.LevelDebug),
|
||||
UpdateInterval: 250 * time.Millisecond,
|
||||
|
||||
@@ -604,7 +604,7 @@ func (f *FakeIDP) CreateAuthCode(t testing.TB, state string) string {
|
||||
// something.
|
||||
// Essentially this is used to fake the Coderd side of the exchange.
|
||||
// The flow starts at the user hitting the OIDC login page.
|
||||
func (f *FakeIDP) OIDCCallback(t testing.TB, state string, idTokenClaims jwt.MapClaims) (*http.Response, error) {
|
||||
func (f *FakeIDP) OIDCCallback(t testing.TB, state string, idTokenClaims jwt.MapClaims) *http.Response {
|
||||
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, nil
|
||||
return resp
|
||||
}
|
||||
|
||||
// ProviderJSON is the .well-known/configuration JSON
|
||||
@@ -1255,7 +1255,9 @@ type ExternalAuthConfigOptions struct {
|
||||
// ValidatePayload is the payload that is used when the user calls the
|
||||
// equivalent of "userinfo" for oauth2. This is not standardized, so is
|
||||
// different for each provider type.
|
||||
ValidatePayload func(email string) interface{}
|
||||
//
|
||||
// The int,error payload can control the response if set.
|
||||
ValidatePayload func(email string) (interface{}, int, error)
|
||||
|
||||
// routes is more advanced usage. This allows the caller to
|
||||
// completely customize the response. It captures all routes under the /external-auth-validate/*
|
||||
@@ -1292,7 +1294,20 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu
|
||||
case "/user", "/", "":
|
||||
var payload interface{} = "OK"
|
||||
if custom.ValidatePayload != nil {
|
||||
payload = custom.ValidatePayload(email)
|
||||
var err error
|
||||
var code int
|
||||
payload, code, err = custom.ValidatePayload(email)
|
||||
if code == 0 && err == nil {
|
||||
code = http.StatusOK
|
||||
}
|
||||
if code == 0 && err != nil {
|
||||
code = http.StatusUnauthorized
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(rw, fmt.Sprintf("failed validation via custom method: %s", err.Error()), code)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(code)
|
||||
}
|
||||
_ = json.NewEncoder(rw).Encode(payload)
|
||||
default:
|
||||
|
||||
@@ -54,12 +54,12 @@ func TestFakeIDPBasicFlow(t *testing.T) {
|
||||
token = oauthToken
|
||||
})
|
||||
|
||||
resp, err := fake.OIDCCallback(t, expectedState, jwt.MapClaims{})
|
||||
require.NoError(t, err)
|
||||
//nolint:bodyclose
|
||||
resp := fake.OIDCCallback(t, expectedState, jwt.MapClaims{})
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// Test the user info
|
||||
_, err = cfg.Provider.UserInfo(ctx, oauth2.StaticTokenSource(token))
|
||||
_, err := cfg.Provider.UserInfo(ctx, oauth2.StaticTokenSource(token))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now test it can refresh
|
||||
|
||||
@@ -103,7 +103,7 @@ func (q *sqlQuerier) InTx(function func(Store) error, txOpts *sql.TxOptions) err
|
||||
// Transaction succeeded.
|
||||
return nil
|
||||
}
|
||||
if err != nil && !IsSerializedError(err) {
|
||||
if !IsSerializedError(err) {
|
||||
// We should only retry if the error is a serialization error.
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/parameter"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
@@ -28,9 +29,25 @@ import (
|
||||
// database types to slices of codersdk types.
|
||||
// Only works if the function takes a single argument.
|
||||
func List[F any, T any](list []F, convert func(F) T) []T {
|
||||
into := make([]T, 0, len(list))
|
||||
for _, item := range list {
|
||||
into = append(into, convert(item))
|
||||
return ListLazy(convert)(list)
|
||||
}
|
||||
|
||||
// ListLazy returns the converter function for a list, but does not eval
|
||||
// the input. Helpful for combining the Map and the List functions.
|
||||
func ListLazy[F any, T any](convert func(F) T) func(list []F) []T {
|
||||
return func(list []F) []T {
|
||||
into := make([]T, 0, len(list))
|
||||
for _, item := range list {
|
||||
into = append(into, convert(item))
|
||||
}
|
||||
return into
|
||||
}
|
||||
}
|
||||
|
||||
func Map[K comparable, F any, T any](params map[K]F, convert func(F) T) map[K]T {
|
||||
into := make(map[K]T)
|
||||
for k, item := range params {
|
||||
into[k] = convert(item)
|
||||
}
|
||||
return into
|
||||
}
|
||||
@@ -150,12 +167,20 @@ func User(user database.User, organizationIDs []uuid.UUID) codersdk.User {
|
||||
convertedUser := codersdk.User{
|
||||
ReducedUser: ReducedUser(user),
|
||||
OrganizationIDs: organizationIDs,
|
||||
Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
|
||||
Roles: make([]codersdk.SlimRole, 0, len(user.RBACRoles)),
|
||||
}
|
||||
|
||||
for _, roleName := range user.RBACRoles {
|
||||
rbacRole, _ := rbac.RoleByName(roleName)
|
||||
convertedUser.Roles = append(convertedUser.Roles, Role(rbacRole))
|
||||
rbacRole, err := rbac.RoleByName(roleName)
|
||||
if err == nil {
|
||||
convertedUser.Roles = append(convertedUser.Roles, SlimRole(rbacRole))
|
||||
} else {
|
||||
// TODO: Fix this for custom roles to display the actual display_name
|
||||
// Requires plumbing either a cached role value, or the db.
|
||||
convertedUser.Roles = append(convertedUser.Roles, codersdk.SlimRole{
|
||||
Name: roleName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return convertedUser
|
||||
@@ -180,8 +205,8 @@ func Group(group database.Group, members []database.User) codersdk.Group {
|
||||
}
|
||||
}
|
||||
|
||||
func Role(role rbac.Role) codersdk.Role {
|
||||
return codersdk.Role{
|
||||
func SlimRole(role rbac.Role) codersdk.SlimRole {
|
||||
return codersdk.SlimRole{
|
||||
DisplayName: role.DisplayName,
|
||||
Name: role.Name,
|
||||
}
|
||||
@@ -500,3 +525,55 @@ func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.Provisioner
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func Role(role rbac.Role) codersdk.Role {
|
||||
roleName, orgIDStr, err := rbac.RoleSplit(role.Name)
|
||||
if err != nil {
|
||||
roleName = role.Name
|
||||
}
|
||||
|
||||
return codersdk.Role{
|
||||
Name: roleName,
|
||||
OrganizationID: orgIDStr,
|
||||
DisplayName: role.DisplayName,
|
||||
SitePermissions: List(role.Site, Permission),
|
||||
// This is not perfect. If there are organization permissions in another
|
||||
// organization, they will be omitted. This should not be allowed, so
|
||||
// should never happen.
|
||||
OrganizationPermissions: List(role.Org[orgIDStr], Permission),
|
||||
UserPermissions: List(role.User, Permission),
|
||||
}
|
||||
}
|
||||
|
||||
func Permission(permission rbac.Permission) codersdk.Permission {
|
||||
return codersdk.Permission{
|
||||
Negate: permission.Negate,
|
||||
ResourceType: codersdk.RBACResource(permission.ResourceType),
|
||||
Action: codersdk.RBACAction(permission.Action),
|
||||
}
|
||||
}
|
||||
|
||||
func RoleToRBAC(role codersdk.Role) rbac.Role {
|
||||
orgPerms := map[string][]rbac.Permission{}
|
||||
if role.OrganizationID != "" {
|
||||
orgPerms = map[string][]rbac.Permission{
|
||||
role.OrganizationID: List(role.OrganizationPermissions, PermissionToRBAC),
|
||||
}
|
||||
}
|
||||
|
||||
return rbac.Role{
|
||||
Name: rbac.RoleName(role.Name, role.OrganizationID),
|
||||
DisplayName: role.DisplayName,
|
||||
Site: List(role.SitePermissions, PermissionToRBAC),
|
||||
Org: orgPerms,
|
||||
User: List(role.UserPermissions, PermissionToRBAC),
|
||||
}
|
||||
}
|
||||
|
||||
func PermissionToRBAC(permission codersdk.Permission) rbac.Permission {
|
||||
return rbac.Permission{
|
||||
Negate: permission.Negate,
|
||||
ResourceType: string(permission.ResourceType),
|
||||
Action: policy.Action(permission.Action),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
package dbauthz_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbmem"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
// TestUpsertCustomRoles verifies creating custom roles cannot escalate permissions.
|
||||
func TestUpsertCustomRoles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
userID := uuid.New()
|
||||
subjectFromRoles := func(roles rbac.ExpandableRoles) rbac.Subject {
|
||||
return rbac.Subject{
|
||||
FriendlyName: "Test user",
|
||||
ID: userID.String(),
|
||||
Roles: roles,
|
||||
Groups: nil,
|
||||
Scope: rbac.ScopeAll,
|
||||
}
|
||||
}
|
||||
|
||||
canAssignRole := rbac.Role{
|
||||
Name: "can-assign",
|
||||
DisplayName: "",
|
||||
Site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceAssignRole.Type: {policy.ActionCreate},
|
||||
}),
|
||||
}
|
||||
|
||||
merge := func(u ...interface{}) rbac.Roles {
|
||||
all := make([]rbac.Role, 0)
|
||||
for _, v := range u {
|
||||
v := v
|
||||
switch t := v.(type) {
|
||||
case rbac.Role:
|
||||
all = append(all, t)
|
||||
case rbac.ExpandableRoles:
|
||||
all = append(all, must(t.Expand())...)
|
||||
case string:
|
||||
all = append(all, must(rbac.RoleByName(t)))
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
orgID := uuid.New()
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
subject rbac.ExpandableRoles
|
||||
|
||||
// Perms to create on new custom role
|
||||
site []rbac.Permission
|
||||
org map[string][]rbac.Permission
|
||||
user []rbac.Permission
|
||||
errorContains string
|
||||
}{
|
||||
{
|
||||
// No roles, so no assign role
|
||||
name: "no-roles",
|
||||
subject: rbac.RoleNames([]string{}),
|
||||
errorContains: "forbidden",
|
||||
},
|
||||
{
|
||||
// This works because the new role has 0 perms
|
||||
name: "empty",
|
||||
subject: merge(canAssignRole),
|
||||
},
|
||||
{
|
||||
name: "mixed-scopes",
|
||||
subject: merge(canAssignRole, rbac.RoleOwner()),
|
||||
site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
org: map[string][]rbac.Permission{
|
||||
uuid.New().String(): rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
errorContains: "cannot assign both org and site permissions",
|
||||
},
|
||||
{
|
||||
name: "multiple-org",
|
||||
subject: merge(canAssignRole, rbac.RoleOwner()),
|
||||
org: map[string][]rbac.Permission{
|
||||
uuid.New().String(): rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
uuid.New().String(): rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
errorContains: "cannot assign permissions to more than 1",
|
||||
},
|
||||
{
|
||||
name: "invalid-action",
|
||||
subject: merge(canAssignRole, rbac.RoleOwner()),
|
||||
site: rbac.Permissions(map[string][]policy.Action{
|
||||
// Action does not go with resource
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionViewInsights},
|
||||
}),
|
||||
errorContains: "invalid action",
|
||||
},
|
||||
{
|
||||
name: "invalid-resource",
|
||||
subject: merge(canAssignRole, rbac.RoleOwner()),
|
||||
site: rbac.Permissions(map[string][]policy.Action{
|
||||
"foobar": {policy.ActionViewInsights},
|
||||
}),
|
||||
errorContains: "invalid resource",
|
||||
},
|
||||
{
|
||||
// Not allowing these at this time.
|
||||
name: "negative-permission",
|
||||
subject: merge(canAssignRole, rbac.RoleOwner()),
|
||||
site: []rbac.Permission{
|
||||
{
|
||||
Negate: true,
|
||||
ResourceType: rbac.ResourceWorkspace.Type,
|
||||
Action: policy.ActionRead,
|
||||
},
|
||||
},
|
||||
errorContains: "no negative permissions",
|
||||
},
|
||||
{
|
||||
name: "wildcard", // not allowed
|
||||
subject: merge(canAssignRole, rbac.RoleOwner()),
|
||||
site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.WildcardSymbol},
|
||||
}),
|
||||
errorContains: "no wildcard symbols",
|
||||
},
|
||||
// escalation checks
|
||||
{
|
||||
name: "read-workspace-escalation",
|
||||
subject: merge(canAssignRole),
|
||||
site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
errorContains: "not allowed to grant this permission",
|
||||
},
|
||||
{
|
||||
name: "read-workspace-outside-org",
|
||||
subject: merge(canAssignRole, rbac.RoleOrgAdmin(orgID)),
|
||||
org: map[string][]rbac.Permission{
|
||||
// The org admin is for a different org
|
||||
uuid.NewString(): rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
errorContains: "not allowed to grant this permission",
|
||||
},
|
||||
{
|
||||
name: "user-escalation",
|
||||
// These roles do not grant user perms
|
||||
subject: merge(canAssignRole, rbac.RoleOrgAdmin(orgID)),
|
||||
user: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
errorContains: "not allowed to grant this permission",
|
||||
},
|
||||
{
|
||||
name: "template-admin-escalation",
|
||||
subject: merge(canAssignRole, rbac.RoleTemplateAdmin()),
|
||||
site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead}, // ok!
|
||||
rbac.ResourceDeploymentConfig.Type: {policy.ActionUpdate}, // not ok!
|
||||
}),
|
||||
user: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead}, // ok!
|
||||
}),
|
||||
errorContains: "deployment_config",
|
||||
},
|
||||
// ok!
|
||||
{
|
||||
name: "read-workspace-template-admin",
|
||||
subject: merge(canAssignRole, rbac.RoleTemplateAdmin()),
|
||||
site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "read-workspace-in-org",
|
||||
subject: merge(canAssignRole, rbac.RoleOrgAdmin(orgID)),
|
||||
org: map[string][]rbac.Permission{
|
||||
// Org admin of this org, this is ok!
|
||||
orgID.String(): rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user-perms",
|
||||
// This is weird, but is ok
|
||||
subject: merge(canAssignRole, rbac.RoleMember()),
|
||||
user: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "site+user-perms",
|
||||
subject: merge(canAssignRole, rbac.RoleMember(), rbac.RoleTemplateAdmin()),
|
||||
site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
user: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionRead},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
db := dbmem.New()
|
||||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: rbac.NewAuthorizer(prometheus.NewRegistry()),
|
||||
}
|
||||
az := dbauthz.New(db, rec, slog.Make(), coderdtest.AccessControlStorePointer())
|
||||
|
||||
subject := subjectFromRoles(tc.subject)
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
ctx = dbauthz.As(ctx, subject)
|
||||
|
||||
_, err := az.UpsertCustomRole(ctx, database.UpsertCustomRoleParams{
|
||||
Name: "test-role",
|
||||
DisplayName: "",
|
||||
SitePermissions: must(json.Marshal(tc.site)),
|
||||
OrgPermissions: must(json.Marshal(tc.org)),
|
||||
UserPermissions: must(json.Marshal(tc.user)),
|
||||
})
|
||||
if tc.errorContains != "" {
|
||||
require.ErrorContains(t, err, tc.errorContains)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+562
-397
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
@@ -98,6 +99,8 @@ func (s *MethodTestSuite) TearDownSuite() {
|
||||
})
|
||||
}
|
||||
|
||||
var testActorID = uuid.New()
|
||||
|
||||
// Subtest is a helper function that returns a function that can be passed to
|
||||
// s.Run(). This function will run the test case for the method that is being
|
||||
// tested. The check parameter is used to assert the results of the method.
|
||||
@@ -119,7 +122,7 @@ func (s *MethodTestSuite) Subtest(testCaseF func(db database.Store, check *expec
|
||||
}
|
||||
az := dbauthz.New(db, rec, slog.Make(), coderdtest.AccessControlStorePointer())
|
||||
actor := rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
ID: testActorID.String(),
|
||||
Roles: rbac.RoleNames{rbac.RoleOwner()},
|
||||
Groups: []string{},
|
||||
Scope: rbac.ScopeAll,
|
||||
@@ -261,7 +264,7 @@ func (s *MethodTestSuite) NotAuthorizedErrorTest(ctx context.Context, az *coderd
|
||||
// any case where the error is nil and the response is an empty slice.
|
||||
if err != nil || !hasEmptySliceResponse(resp) {
|
||||
s.Errorf(err, "method should an error with cancellation")
|
||||
s.ErrorIsf(err, context.Canceled, "error should match context.Cancelled")
|
||||
s.ErrorIsf(err, context.Canceled, "error should match context.Canceled")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -338,7 +341,7 @@ func (m *expects) Errors(err error) *expects {
|
||||
// AssertRBAC contains the object and actions to be asserted.
|
||||
type AssertRBAC struct {
|
||||
Object rbac.Object
|
||||
Actions []rbac.Action
|
||||
Actions []policy.Action
|
||||
}
|
||||
|
||||
// values is a convenience method for creating []reflect.Value.
|
||||
@@ -368,15 +371,15 @@ func values(ins ...any) []reflect.Value {
|
||||
//
|
||||
// Even-numbered inputs are the objects, and odd-numbered inputs are the actions.
|
||||
// Objects must implement rbac.Objecter.
|
||||
// Inputs can be a single rbac.Action, or a slice of rbac.Action.
|
||||
// Inputs can be a single policy.Action, or a slice of policy.Action.
|
||||
//
|
||||
// asserts(workspace, rbac.ActionRead, template, slice(rbac.ActionRead, rbac.ActionWrite), ...)
|
||||
// asserts(workspace, policy.ActionRead, template, slice(policy.ActionRead, policy.ActionWrite), ...)
|
||||
//
|
||||
// is equivalent to
|
||||
//
|
||||
// []AssertRBAC{
|
||||
// {Object: workspace, Actions: []rbac.Action{rbac.ActionRead}},
|
||||
// {Object: template, Actions: []rbac.Action{rbac.ActionRead, rbac.ActionWrite)}},
|
||||
// {Object: workspace, Actions: []policy.Action{policy.ActionRead}},
|
||||
// {Object: template, Actions: []policy.Action{policy.ActionRead, policy.ActionWrite)}},
|
||||
// ...
|
||||
// }
|
||||
func asserts(inputs ...any) []AssertRBAC {
|
||||
@@ -392,19 +395,19 @@ func asserts(inputs ...any) []AssertRBAC {
|
||||
}
|
||||
rbacObj := obj.RBACObject()
|
||||
|
||||
var actions []rbac.Action
|
||||
actions, ok = inputs[i+1].([]rbac.Action)
|
||||
var actions []policy.Action
|
||||
actions, ok = inputs[i+1].([]policy.Action)
|
||||
if !ok {
|
||||
action, ok := inputs[i+1].(rbac.Action)
|
||||
action, ok := inputs[i+1].(policy.Action)
|
||||
if !ok {
|
||||
// Could be the string type.
|
||||
actionAsString, ok := inputs[i+1].(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("action '%q' not a supported action", actionAsString))
|
||||
}
|
||||
action = rbac.Action(actionAsString)
|
||||
action = policy.Action(actionAsString)
|
||||
}
|
||||
actions = []rbac.Action{action}
|
||||
actions = []policy.Action{action}
|
||||
}
|
||||
|
||||
out = append(out, AssertRBAC{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user