diff --git a/.gitattributes b/.gitattributes index bad79cf54d329..d19626bd6d743 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,3 +12,4 @@ provisionersdk/proto/*.go linguist-generated=true *.tfstate.dot linguist-generated=true *.tfplan.dot linguist-generated=true site/src/api/typesGenerated.ts linguist-generated=true +site/src/pages/SetupPage/countries.tsx linguist-generated=true diff --git a/.github/actions/setup-node/action.yaml b/.github/actions/setup-node/action.yaml index ed4ae45045fe6..d6929381ddbe7 100644 --- a/.github/actions/setup-node/action.yaml +++ b/.github/actions/setup-node/action.yaml @@ -17,7 +17,7 @@ runs: - name: Setup Node uses: buildjet/setup-node@v3 with: - node-version: 18.17.0 + node-version: 18.19.0 # See https://github.com/actions/setup-node#caching-global-packages-data cache: "pnpm" cache-dependency-path: ${{ inputs.directory }}/pnpm-lock.yaml diff --git a/.github/actions/setup-sqlc/action.yaml b/.github/actions/setup-sqlc/action.yaml index 151970389f1bd..544d2d4ce923c 100644 --- a/.github/actions/setup-sqlc/action.yaml +++ b/.github/actions/setup-sqlc/action.yaml @@ -7,4 +7,4 @@ runs: - name: Setup sqlc uses: sqlc-dev/setup-sqlc@v4 with: - sqlc-version: "1.24.0" + sqlc-version: "1.25.0" diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 49bb9d57e1106..b2a815a0421a7 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -38,15 +38,12 @@ updates: commit-message: prefix: "chore" labels: [] + open-pull-requests-limit: 15 ignore: # Ignore patch updates for all dependencies - dependency-name: "*" update-types: - version-update:semver-patch - groups: - go: - patterns: - - "*" # Update our Dockerfile. - package-ecosystem: "docker" diff --git a/.github/fly-wsproxies/paris-coder.toml b/.github/fly-wsproxies/paris-coder.toml index 1b33fc2463114..a68ceff07dee5 100644 --- a/.github/fly-wsproxies/paris-coder.toml +++ b/.github/fly-wsproxies/paris-coder.toml @@ -13,6 +13,7 @@ primary_region = "cdg" CODER_HTTP_ADDRESS = "0.0.0.0:3000" CODER_PRIMARY_ACCESS_URL = "https://dev.coder.com" CODER_WILDCARD_ACCESS_URL = "*--apps.paris.fly.dev.coder.com" + CODER_VERBOSE = "true" [http_service] internal_port = 3000 diff --git a/.github/fly-wsproxies/sao-paulo-coder.toml b/.github/fly-wsproxies/sao-paulo-coder.toml index c3b614e3e3ed4..0866d61af45a2 100644 --- a/.github/fly-wsproxies/sao-paulo-coder.toml +++ b/.github/fly-wsproxies/sao-paulo-coder.toml @@ -13,6 +13,7 @@ primary_region = "gru" CODER_HTTP_ADDRESS = "0.0.0.0:3000" CODER_PRIMARY_ACCESS_URL = "https://dev.coder.com" CODER_WILDCARD_ACCESS_URL = "*--apps.sao-paulo.fly.dev.coder.com" + CODER_VERBOSE = "true" [http_service] internal_port = 3000 diff --git a/.github/fly-wsproxies/sydney-coder.toml b/.github/fly-wsproxies/sydney-coder.toml index 98798f188df73..b2fd4d8ed55cf 100644 --- a/.github/fly-wsproxies/sydney-coder.toml +++ b/.github/fly-wsproxies/sydney-coder.toml @@ -13,6 +13,7 @@ primary_region = "syd" CODER_HTTP_ADDRESS = "0.0.0.0:3000" CODER_PRIMARY_ACCESS_URL = "https://dev.coder.com" CODER_WILDCARD_ACCESS_URL = "*--apps.sydney.fly.dev.coder.com" + CODER_VERBOSE = "true" [http_service] internal_port = 3000 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index add1d38dee599..6b628671fe511 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,10 +60,7 @@ jobs: - "examples/lima/**" db: - "**.sql" - - "coderd/database/queries/**" - - "coderd/database/migrations" - - "coderd/database/sqlc.yaml" - - "coderd/database/dump.sql" + - "coderd/database/**" go: - "**.sql" - "**.go" @@ -144,7 +141,7 @@ jobs: # Check for any typos - name: Check for typos - uses: crate-ci/typos@v1.16.25 + uses: crate-ci/typos@v1.17.1 with: config: .github/workflows/typos.toml @@ -191,7 +188,7 @@ jobs: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33 go install golang.org/x/tools/cmd/goimports@latest go install github.com/mikefarah/yq/v4@v4.30.6 - go install github.com/golang/mock/mockgen@v1.6.0 + go install go.uber.org/mock/mockgen@v0.4.0 - name: Install Protoc run: | @@ -224,7 +221,7 @@ jobs: uses: ./.github/actions/setup-node - name: Setup Go - uses: buildjet/setup-go@v4 + uses: buildjet/setup-go@v5 with: # This doesn't need caching. It's super fast anyways! cache: false @@ -324,7 +321,6 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }} needs: - changes - - sqlc-vet # No point in testing the DB if the queries are invalid if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' # This timeout must be greater than the timeout set by `go test` in # `make test-postgres` to ensure we receive a trace of running @@ -454,7 +450,7 @@ jobs: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33 go install golang.org/x/tools/cmd/goimports@latest go install github.com/mikefarah/yq/v4@v4.30.6 - go install github.com/golang/mock/mockgen@v1.6.0 + go install go.uber.org/mock/mockgen@v0.4.0 - name: Install Protoc run: | @@ -596,7 +592,7 @@ jobs: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33 go install golang.org/x/tools/cmd/goimports@latest go install github.com/mikefarah/yq/v4@v4.30.6 - go install github.com/golang/mock/mockgen@v1.6.0 + go install go.uber.org/mock/mockgen@v0.4.0 - name: Setup sqlc uses: ./.github/actions/setup-sqlc @@ -659,7 +655,7 @@ jobs: # to main branch. We are only building this for amd64 platform. (>95% pulls # are for amd64) needs: changes - if: github.ref == 'refs/heads/main' && needs.changes.outputs.docs-only == 'false' + if: needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }} env: DOCKER_CLI_EXPERIMENTAL: "enabled" @@ -685,7 +681,7 @@ jobs: uses: ./.github/actions/setup-go - name: Install nfpm - run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.16.0 + run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.35.1 - name: Install zstd run: sudo apt-get install -y zstd @@ -696,46 +692,70 @@ jobs: go mod download version="$(./scripts/version.sh)" + tag="main-$(echo "$version" | sed 's/+/-/g')" + echo "tag=$tag" >> $GITHUB_OUTPUT + make gen/mark-fresh make -j \ - build/coder_linux_amd64 \ + build/coder_linux_{amd64,arm64,armv7} \ build/coder_"$version"_windows_amd64.zip \ build/coder_"$version"_linux_amd64.{tar.gz,deb} - - name: Build and Push Linux amd64 Docker Image + - name: Build Linux Docker images id: build-docker + env: + CODER_IMAGE_BASE: ghcr.io/coder/coder-preview + CODER_IMAGE_TAG_PREFIX: main + DOCKER_CLI_EXPERIMENTAL: "enabled" run: | set -euxo pipefail + + # build Docker images for each architecture version="$(./scripts/version.sh)" tag="main-$(echo "$version" | sed 's/+/-/g')" - - export CODER_IMAGE_BUILD_BASE_TAG="$(CODER_IMAGE_BASE=coder-base ./scripts/image_tag.sh --version "$version")" - ./scripts/build_docker.sh \ - --arch amd64 \ - --target "ghcr.io/coder/coder-preview:$tag" \ - --version $version \ - --push \ - build/coder_linux_amd64 - - # Tag as main - docker tag "ghcr.io/coder/coder-preview:$tag" ghcr.io/coder/coder-preview:main - docker push ghcr.io/coder/coder-preview:main - - # Store the tag in an output variable so we can use it in other jobs echo "tag=$tag" >> $GITHUB_OUTPUT + # build images for each architecture + make -j build/coder_"$version"_linux_{amd64,arm64,armv7}.tag + + # only push if we are on main branch + if [ "${{ github.ref }}" == "refs/heads/main" ]; then + # build and push multi-arch manifest, this depends on the other images + # being pushed so will automatically push them + make -j push/build/coder_"$version"_linux_{amd64,arm64,armv7}.tag + + # Define specific tags + tags=("$tag" "main" "latest") + + # Create and push a multi-arch manifest for each tag + # we are adding `latest` tag and keeping `main` for backward + # compatibality + for t in "${tags[@]}"; do + ./scripts/build_docker_multiarch.sh \ + --push \ + --target "ghcr.io/coder/coder-preview:$t" \ + --version $version \ + $(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag) + done + fi + - name: Prune old images + if: github.ref == 'refs/heads/main' uses: vlaurin/action-ghcr-prune@v0.5.0 with: token: ${{ secrets.GITHUB_TOKEN }} organization: coder container: coder-preview keep-younger-than: 7 # days + keep-tags: latest keep-tags-regexes: ^pr - prune-tags-regexes: ^main- + prune-tags-regexes: | + ^main- + ^v prune-untagged: true - name: Upload build artifacts + if: github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4 with: name: coder diff --git a/.github/workflows/dogfood.yaml b/.github/workflows/dogfood.yaml index c3d9020f318c6..be349833a60e4 100644 --- a/.github/workflows/dogfood.yaml +++ b/.github/workflows/dogfood.yaml @@ -7,16 +7,15 @@ on: paths: - "dogfood/**" - ".github/workflows/dogfood.yaml" - # Uncomment these lines when testing with CI. - # pull_request: - # paths: - # - "dogfood/**" - # - ".github/workflows/dogfood.yaml" + pull_request: + paths: + - "dogfood/**" + - ".github/workflows/dogfood.yaml" workflow_dispatch: jobs: - deploy_image: - runs-on: buildjet-4vcpu-ubuntu-2204 + build_image: + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 @@ -33,27 +32,33 @@ jobs: tag=${tag//\//--} echo "tag=${tag}" >> $GITHUB_OUTPUT + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub + if: github.ref == 'refs/heads/main' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build and push - uses: docker/build-push-action@v5 + uses: depot/build-push-action@v1 with: + project: b4q6ltmpzh + token: ${{ secrets.DEPOT_TOKEN }} + buildx-fallback: true context: "{{defaultContext}}:dogfood" pull: true - push: true + push: ${{ github.ref == 'refs/heads/main' }} tags: "codercom/oss-dogfood:${{ steps.docker-tag-name.outputs.tag }},codercom/oss-dogfood:latest" - cache-from: type=registry,ref=codercom/oss-dogfood:latest - cache-to: type=inline deploy_template: - needs: deploy_image + needs: build_image + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Checkout @@ -74,7 +79,7 @@ jobs: - name: "Push template" run: | - ./coder templates push $CODER_TEMPLATE_NAME --directory $CODER_TEMPLATE_DIR --yes --name=$CODER_TEMPLATE_VERSION --message="$CODER_TEMPLATE_MESSAGE" + ./coder templates push $CODER_TEMPLATE_NAME --directory $CODER_TEMPLATE_DIR --yes --name=$CODER_TEMPLATE_VERSION --message="$CODER_TEMPLATE_MESSAGE" --variable jfrog_url=${{ secrets.JFROG_URL }} env: # Consumed by Coder CLI CODER_URL: https://dev.coder.com diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 9c657b43ba699..f5045f0bb202a 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -416,7 +416,7 @@ jobs: # Create template cd ./.github/pr-deployments/template - coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes + coder templates push -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes # Create workspace coder create --template="kubernetes" kube --parameter cpu=2 --parameter memory=4 --parameter home_disk_size=2 -y diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 559a477581585..6085e81d0a166 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -103,7 +103,7 @@ jobs: - name: Install nfpm run: | set -euo pipefail - wget -O /tmp/nfpm.deb https://github.com/goreleaser/nfpm/releases/download/v2.18.1/nfpm_amd64.deb + wget -O /tmp/nfpm.deb https://github.com/goreleaser/nfpm/releases/download/v2.35.1/nfpm_2.35.1_amd64.deb sudo dpkg -i /tmp/nfpm.deb rm /tmp/nfpm.deb diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index c236abd1bc3c0..8293ed875d0dd 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -75,7 +75,7 @@ jobs: - name: Install yq run: go run github.com/mikefarah/yq/v4@v4.30.6 - name: Install mockgen - run: go install github.com/golang/mock/mockgen@v1.6.0 + run: go install go.uber.org/mock/mockgen@v0.4.0 - name: Install protoc-gen-go run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 - name: Install protoc-gen-go-drpc @@ -122,7 +122,7 @@ jobs: image_name: ${{ steps.build.outputs.image }} - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@91713af97dc80187565512baba96e4364e983601 + uses: aquasecurity/trivy-action@d43c1f16c00cfd3978dde6c07f4bbcf9eb6993ca with: image-ref: ${{ steps.build.outputs.image }} format: sarif diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 759bd84dd71ad..e1008e75e79eb 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -68,7 +68,7 @@ jobs: repo: context.repo.repo, issue_number: issue.number, state: 'closed', - state_reason: 'not planned' + state_reason: 'not_planned' }); } } else { diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index 23043a35e1ad2..57d1b596ede18 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -14,7 +14,7 @@ darcula = "darcula" Hashi = "Hashi" trialer = "trialer" encrypter = "encrypter" -hel = "hel" # as in helsinki +hel = "hel" # as in helsinki [files] extend-exclude = [ @@ -31,4 +31,5 @@ extend-exclude = [ "**/*.test.tsx", "**/pnpm-lock.yaml", "tailnet/testdata/**", + "site/src/pages/SetupPage/countries.tsx", ] diff --git a/.vscode/settings.json b/.vscode/settings.json index bcbdb7baeb9fa..f9b18af11a55d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,8 +21,8 @@ "contravariance", "cronstrue", "databasefake", - "dbmem", "dbgen", + "dbmem", "dbtype", "DERP", "derphttp", @@ -60,6 +60,7 @@ "idtoken", "Iflag", "incpatch", + "initialisms", "ipnstate", "isatty", "Jobf", @@ -118,13 +119,13 @@ "stretchr", "STTY", "stuntest", - "tanstack", "tailbroker", "tailcfg", "tailexchange", "tailnet", "tailnettest", "Tailscale", + "tanstack", "tbody", "TCGETS", "tcpip", @@ -141,6 +142,7 @@ "tios", "tmpdir", "tokenconfig", + "Topbar", "tparallel", "trialer", "trimprefix", diff --git a/README.md b/README.md index 27634813adf34..f816b7f1aa9a9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@
\n\n", string(body)) }) } + +func TestCSRFExempt(t *testing.T) { + t.Parallel() + + // This test build a workspace with an agent and an app. The app is not + // a real http server, so it will fail to serve requests. We just want + // to make sure the failure is not a CSRF failure, as path based + // apps should be exempt. + t.Run("PathBasedApp", func(t *testing.T) { + t.Parallel() + + client, _, api := coderdtest.NewWithAPI(t, nil) + first := coderdtest.CreateFirstUser(t, client) + owner, err := client.User(context.Background(), "me") + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) + defer cancel() + + // Create a workspace. + const agentSlug = "james" + const appSlug = "web" + wrk := dbfake.WorkspaceBuild(t, api.Database, database.Workspace{ + OwnerID: owner.ID, + OrganizationID: first.OrganizationID, + }). + WithAgent(func(agents []*proto.Agent) []*proto.Agent { + agents[0].Name = agentSlug + agents[0].Apps = []*proto.App{{ + Slug: appSlug, + DisplayName: appSlug, + Subdomain: false, + Url: "/", + }} + + return agents + }). + Do() + + u := client.URL.JoinPath(fmt.Sprintf("/@%s/%s.%s/apps/%s", owner.Username, wrk.Workspace.Name, agentSlug, appSlug)).String() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil) + req.AddCookie(&http.Cookie{ + Name: codersdk.SessionTokenCookie, + Value: client.SessionToken(), + Path: "/", + Domain: client.URL.String(), + }) + require.NoError(t, err) + + resp, err := client.HTTPClient.Do(req) + require.NoError(t, err) + data, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + + // A StatusBadGateway means Coderd tried to proxy to the agent and failed because the agent + // was not there. This means CSRF did not block the app request, which is what we want. + require.Equal(t, http.StatusBadGateway, resp.StatusCode, "status code 500 is CSRF failure") + require.NotContains(t, string(data), "CSRF") + }) +} diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 55060a0998260..91ff7e17538d9 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -62,7 +62,6 @@ import ( "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/gitsshkey" "github.com/coder/coder/v2/coderd/healthcheck" - "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/schedule" @@ -71,6 +70,7 @@ import ( "github.com/coder/coder/v2/coderd/updatecheck" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/coderd/workspaceapps" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/drpc" @@ -107,7 +107,7 @@ type Options struct { Auditor audit.Auditor TLSCertificates []tls.Certificate ExternalAuthConfigs []*externalauth.Config - TrialGenerator func(context.Context, string) error + TrialGenerator func(ctx context.Context, body codersdk.LicensorTrialRequest) error TemplateScheduleStore schedule.TemplateScheduleStore Coordinator tailnet.Coordinator @@ -145,6 +145,7 @@ type Options struct { WorkspaceAppsStatsCollectorOptions workspaceapps.StatsCollectorOptions AllowWorkspaceRenames bool + NewTicker func(duration time.Duration) (<-chan time.Time, func()) } // New constructs a codersdk client connected to an in-memory API instance. @@ -371,7 +372,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can var appHostnameRegex *regexp.Regexp if options.AppHostname != "" { var err error - appHostnameRegex, err = httpapi.CompileHostnamePattern(options.AppHostname) + appHostnameRegex, err = appurl.CompileHostnamePattern(options.AppHostname) require.NoError(t, err) } @@ -451,6 +452,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can StatsBatcher: options.StatsBatcher, WorkspaceAppsStatsCollectorOptions: options.WorkspaceAppsStatsCollectorOptions, AllowWorkspaceRenames: options.AllowWorkspaceRenames, + NewTicker: options.NewTicker, } } @@ -532,8 +534,8 @@ func NewProvisionerDaemon(t testing.TB, coderAPI *coderd.API) io.Closer { assert.NoError(t, err) }() - daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { - return coderAPI.CreateInMemoryProvisionerDaemon(ctx, "test") + daemon := provisionerd.New(func(dialCtx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { + return coderAPI.CreateInMemoryProvisionerDaemon(dialCtx, "test") }, &provisionerd.Options{ Logger: coderAPI.Logger.Named("provisionerd").Leveled(slog.LevelDebug), UpdateInterval: 250 * time.Millisecond, diff --git a/coderd/coderdtest/oidctest/idp.go b/coderd/coderdtest/oidctest/idp.go index 20702be16ab33..e830bb0511165 100644 --- a/coderd/coderdtest/oidctest/idp.go +++ b/coderd/coderdtest/oidctest/idp.go @@ -24,6 +24,7 @@ import ( "github.com/go-jose/go-jose/v3" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -33,10 +34,17 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/externalauth" + "github.com/coder/coder/v2/coderd/promoauth" "github.com/coder/coder/v2/coderd/util/syncmap" "github.com/coder/coder/v2/codersdk" ) +type token struct { + issued time.Time + email string + exp time.Time +} + // FakeIDP is a functional OIDC provider. // It only supports 1 OIDC client. type FakeIDP struct { @@ -63,7 +71,7 @@ type FakeIDP struct { // That is the various access tokens, refresh tokens, states, etc. codeToStateMap *syncmap.Map[string, string] // Token -> Email - accessTokens *syncmap.Map[string, string] + accessTokens *syncmap.Map[string, token] // Refresh Token -> Email refreshTokensUsed *syncmap.Map[string, bool] refreshTokens *syncmap.Map[string, string] @@ -76,13 +84,19 @@ type FakeIDP struct { // "Authorized Redirect URLs". This can be used to emulate that. hookValidRedirectURL func(redirectURL string) error hookUserInfo func(email string) (jwt.MapClaims, error) - hookMutateToken func(token map[string]interface{}) - fakeCoderd func(req *http.Request) (*http.Response, error) - hookOnRefresh func(email string) error + // defaultIDClaims is if a new client connects and we didn't preset + // some claims. + defaultIDClaims jwt.MapClaims + hookMutateToken func(token map[string]interface{}) + fakeCoderd func(req *http.Request) (*http.Response, error) + hookOnRefresh func(email string) error // Custom authentication for the client. This is useful if you want // to test something like PKI auth vs a client_secret. hookAuthenticateClient func(t testing.TB, req *http.Request) (url.Values, error) serve bool + // optional middlewares + middlewares chi.Middlewares + defaultExpire time.Duration } func StatusError(code int, err error) error { @@ -113,6 +127,12 @@ func WithAuthorizedRedirectURL(hook func(redirectURL string) error) func(*FakeID } } +func WithMiddlewares(mws ...func(http.Handler) http.Handler) func(*FakeIDP) { + return func(f *FakeIDP) { + f.middlewares = append(f.middlewares, mws...) + } +} + // WithRefresh is called when a refresh token is used. The email is // the email of the user that is being refreshed assuming the claims are correct. func WithRefresh(hook func(email string) error) func(*FakeIDP) { @@ -121,6 +141,23 @@ func WithRefresh(hook func(email string) error) func(*FakeIDP) { } } +func WithDefaultExpire(d time.Duration) func(*FakeIDP) { + return func(f *FakeIDP) { + f.defaultExpire = d + } +} + +func WithStaticCredentials(id, secret string) func(*FakeIDP) { + return func(f *FakeIDP) { + if id != "" { + f.clientID = id + } + if secret != "" { + f.clientSecret = secret + } + } +} + // WithExtra returns extra fields that be accessed on the returned Oauth Token. // These extra fields can override the default fields (id_token, access_token, etc). func WithMutateToken(mutateToken func(token map[string]interface{})) func(*FakeIDP) { @@ -142,6 +179,12 @@ func WithLogging(t testing.TB, options *slogtest.Options) func(*FakeIDP) { } } +func WithLogger(logger slog.Logger) func(*FakeIDP) { + return func(f *FakeIDP) { + f.logger = logger + } +} + // WithStaticUserInfo is optional, but will return the same user info for // every user on the /userinfo endpoint. func WithStaticUserInfo(info jwt.MapClaims) func(*FakeIDP) { @@ -152,6 +195,12 @@ func WithStaticUserInfo(info jwt.MapClaims) func(*FakeIDP) { } } +func WithDefaultIDClaims(claims jwt.MapClaims) func(*FakeIDP) { + return func(f *FakeIDP) { + f.defaultIDClaims = claims + } +} + func WithDynamicUserInfo(userInfoFunc func(email string) (jwt.MapClaims, error)) func(*FakeIDP) { return func(f *FakeIDP) { f.hookUserInfo = userInfoFunc @@ -192,7 +241,7 @@ func NewFakeIDP(t testing.TB, opts ...FakeIDPOpt) *FakeIDP { clientSecret: uuid.NewString(), logger: slog.Make(), codeToStateMap: syncmap.New[string, string](), - accessTokens: syncmap.New[string, string](), + accessTokens: syncmap.New[string, token](), refreshTokens: syncmap.New[string, string](), refreshTokensUsed: syncmap.New[string, bool](), stateToIDTokenClaims: syncmap.New[string, jwt.MapClaims](), @@ -200,6 +249,7 @@ func NewFakeIDP(t testing.TB, opts ...FakeIDPOpt) *FakeIDP { hookOnRefresh: func(_ string) error { return nil }, hookUserInfo: func(email string) (jwt.MapClaims, error) { return jwt.MapClaims{}, nil }, hookValidRedirectURL: func(redirectURL string) error { return nil }, + defaultExpire: time.Minute * 5, } for _, opt := range opts { @@ -223,6 +273,10 @@ func (f *FakeIDP) WellknownConfig() ProviderJSON { return f.provider } +func (f *FakeIDP) IssuerURL() *url.URL { + return f.issuerURL +} + func (f *FakeIDP) updateIssuerURL(t testing.TB, issuer string) { t.Helper() @@ -242,6 +296,7 @@ func (f *FakeIDP) updateIssuerURL(t testing.TB, issuer string) { Algorithms: []string{ "RS256", }, + ExternalAuthURL: u.ResolveReference(&url.URL{Path: "/external-auth-validate/user"}).String(), } } @@ -249,8 +304,23 @@ func (f *FakeIDP) updateIssuerURL(t testing.TB, issuer string) { func (f *FakeIDP) realServer(t testing.TB) *httptest.Server { t.Helper() + srvURL := "localhost:0" + issURL, err := url.Parse(f.issuer) + if err == nil { + if issURL.Hostname() == "localhost" || issURL.Hostname() == "127.0.0.1" { + srvURL = issURL.Host + } + } + + l, err := net.Listen("tcp", srvURL) + require.NoError(t, err, "failed to create listener") + ctx, cancel := context.WithCancel(context.Background()) - srv := httptest.NewUnstartedServer(f.handler) + srv := &httptest.Server{ + Listener: l, + Config: &http.Server{Handler: f.handler, ReadHeaderTimeout: time.Second * 5}, + } + srv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx } @@ -397,6 +467,44 @@ func (f *FakeIDP) ExternalLogin(t testing.TB, client *codersdk.Client, opts ...f _ = res.Body.Close() } +// CreateAuthCode emulates a user clicking "allow" on the IDP page. When doing +// unit tests, it's easier to skip this step sometimes. It does make an actual +// request to the IDP, so it should be equivalent to doing this "manually" with +// actual requests. +func (f *FakeIDP) CreateAuthCode(t testing.TB, state string, opts ...func(r *http.Request)) string { + // We need to store some claims, because this is also an OIDC provider, and + // it expects some claims to be present. + f.stateToIDTokenClaims.Store(state, jwt.MapClaims{}) + + u := f.cfg.AuthCodeURL(state) + r, err := http.NewRequestWithContext(context.Background(), http.MethodPost, u, nil) + require.NoError(t, err, "failed to create auth request") + + for _, opt := range opts { + opt(r) + } + + rw := httptest.NewRecorder() + f.handler.ServeHTTP(rw, r) + resp := rw.Result() + defer resp.Body.Close() + + require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode, "expected redirect") + to := resp.Header.Get("Location") + require.NotEmpty(t, to, "expected redirect location") + + toURL, err := url.Parse(to) + require.NoError(t, err, "failed to parse redirect location") + + code := toURL.Query().Get("code") + require.NotEmpty(t, code, "expected code in redirect location") + + newState := toURL.Query().Get("state") + require.Equal(t, state, newState, "expected state to match") + + return code +} + // OIDCCallback will emulate the IDP redirecting back to the Coder callback. // This is helpful if no Coderd exists because the IDP needs to redirect to // something. @@ -434,6 +542,8 @@ type ProviderJSON struct { JWKSURL string `json:"jwks_uri"` UserInfoURL string `json:"userinfo_endpoint"` Algorithms []string `json:"id_token_signing_alg_values_supported"` + // This is custom + ExternalAuthURL string `json:"external_auth_url"` } // newCode enforces the code exchanged is actually a valid code @@ -446,9 +556,13 @@ func (f *FakeIDP) newCode(state string) string { // newToken enforces the access token exchanged is actually a valid access token // created by the IDP. -func (f *FakeIDP) newToken(email string) string { +func (f *FakeIDP) newToken(email string, expires time.Time) string { accessToken := uuid.NewString() - f.accessTokens.Store(accessToken, email) + f.accessTokens.Store(accessToken, token{ + issued: time.Now(), + email: email, + exp: expires, + }) return accessToken } @@ -464,10 +578,15 @@ func (f *FakeIDP) authenticateBearerTokenRequest(t testing.TB, req *http.Request auth := req.Header.Get("Authorization") token := strings.TrimPrefix(auth, "Bearer ") - _, ok := f.accessTokens.Load(token) + authToken, ok := f.accessTokens.Load(token) if !ok { return "", xerrors.New("invalid access token") } + + if !authToken.exp.IsZero() && authToken.exp.Before(time.Now()) { + return "", xerrors.New("access token expired") + } + return token, nil } @@ -526,6 +645,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { t.Helper() mux := chi.NewMux() + mux.Use(f.middlewares...) // This endpoint is required to initialize the OIDC provider. // It is used to get the OIDC configuration. mux.Get("/.well-known/openid-configuration", func(rw http.ResponseWriter, r *http.Request) { @@ -591,7 +711,8 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { mux.Handle(tokenPath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { values, err := f.authenticateOIDCClientRequest(t, r) f.logger.Info(r.Context(), "http idp call token", - slog.Error(err), + slog.F("valid", err == nil), + slog.F("grant_type", values.Get("grant_type")), slog.F("values", values.Encode()), ) if err != nil { @@ -626,7 +747,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { // Always invalidate the code after it is used. f.codeToStateMap.Delete(code) - idTokenClaims, ok := f.stateToIDTokenClaims.Load(stateStr) + idTokenClaims, ok := f.getClaims(f.stateToIDTokenClaims, stateStr) if !ok { t.Errorf("missing id token claims") http.Error(rw, "missing id token claims", http.StatusBadRequest) @@ -646,7 +767,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { return } - idTokenClaims, ok := f.refreshIDTokenClaims.Load(refreshToken) + idTokenClaims, ok := f.getClaims(f.refreshIDTokenClaims, refreshToken) if !ok { t.Errorf("missing id token claims in refresh") http.Error(rw, "missing id token claims in refresh", http.StatusBadRequest) @@ -669,15 +790,15 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { return } - exp := time.Now().Add(time.Minute * 5) + exp := time.Now().Add(f.defaultExpire) claims["exp"] = exp.UnixMilli() email := getEmail(claims) refreshToken := f.newRefreshTokens(email) token := map[string]interface{}{ - "access_token": f.newToken(email), + "access_token": f.newToken(email, exp), "refresh_token": refreshToken, "token_type": "Bearer", - "expires_in": int64((time.Minute * 5).Seconds()), + "expires_in": int64((f.defaultExpire).Seconds()), "id_token": f.encodeClaims(t, claims), } if f.hookMutateToken != nil { @@ -692,25 +813,31 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { validateMW := func(rw http.ResponseWriter, r *http.Request) (email string, ok bool) { token, err := f.authenticateBearerTokenRequest(t, r) - f.logger.Info(r.Context(), "http call idp user info", - slog.Error(err), - slog.F("url", r.URL.String()), - ) if err != nil { - http.Error(rw, fmt.Sprintf("invalid user info request: %s", err.Error()), http.StatusBadRequest) + http.Error(rw, fmt.Sprintf("invalid user info request: %s", err.Error()), http.StatusUnauthorized) return "", false } - email, ok = f.accessTokens.Load(token) + authToken, ok := f.accessTokens.Load(token) if !ok { t.Errorf("access token user for user_info has no email to indicate which user") - http.Error(rw, "invalid access token, missing user info", http.StatusBadRequest) + http.Error(rw, "invalid access token, missing user info", http.StatusUnauthorized) return "", false } - return email, true + + if !authToken.exp.IsZero() && authToken.exp.Before(time.Now()) { + http.Error(rw, "auth token expired", http.StatusUnauthorized) + return "", false + } + + return authToken.email, true } mux.Handle(userInfoPath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { email, ok := validateMW(rw, r) + f.logger.Info(r.Context(), "http userinfo endpoint", + slog.F("valid", ok), + slog.F("email", email), + ) if !ok { return } @@ -728,6 +855,10 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { // should be strict, and this one needs to handle sub routes. mux.Mount("/external-auth-validate/", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { email, ok := validateMW(rw, r) + f.logger.Info(r.Context(), "http external auth validate", + slog.F("valid", ok), + slog.F("email", email), + ) if !ok { return } @@ -879,7 +1010,7 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu } f.externalProviderID = id f.externalAuthValidate = func(email string, rw http.ResponseWriter, r *http.Request) { - newPath := strings.TrimPrefix(r.URL.Path, fmt.Sprintf("/external-auth-validate/%s", id)) + newPath := strings.TrimPrefix(r.URL.Path, "/external-auth-validate") switch newPath { // /user is ALWAYS supported under the `/` path too. case "/user", "/", "": @@ -901,29 +1032,36 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu handle(email, rw, r) } } + instrumentF := promoauth.NewFactory(prometheus.NewRegistry()) cfg := &externalauth.Config{ - OAuth2Config: f.OIDCConfig(t, nil), - ID: id, + DisplayName: id, + InstrumentedOAuth2Config: instrumentF.New(f.clientID, f.OIDCConfig(t, nil)), + ID: id, // No defaults for these fields by omitting the type Type: "", DisplayIcon: f.WellknownConfig().UserInfoURL, // Omit the /user for the validate so we can easily append to it when modifying // the cfg for advanced tests. - ValidateURL: f.issuerURL.ResolveReference(&url.URL{Path: fmt.Sprintf("/external-auth-validate/%s", id)}).String(), + ValidateURL: f.issuerURL.ResolveReference(&url.URL{Path: "/external-auth-validate/"}).String(), } for _, opt := range opts { opt(cfg) } + f.updateIssuerURL(t, f.issuer) return cfg } +func (f *FakeIDP) AppCredentials() (clientID string, clientSecret string) { + return f.clientID, f.clientSecret +} + // OIDCConfig returns the OIDC config to use for Coderd. func (f *FakeIDP) OIDCConfig(t testing.TB, scopes []string, opts ...func(cfg *coderd.OIDCConfig)) *coderd.OIDCConfig { t.Helper() + if len(scopes) == 0 { scopes = []string{"openid", "email", "profile"} } - oauthCfg := &oauth2.Config{ ClientID: f.clientID, ClientSecret: f.clientSecret, @@ -966,10 +1104,20 @@ func (f *FakeIDP) OIDCConfig(t testing.TB, scopes []string, opts ...func(cfg *co } f.cfg = oauthCfg - return cfg } +func (f *FakeIDP) getClaims(m *syncmap.Map[string, jwt.MapClaims], key string) (jwt.MapClaims, bool) { + v, ok := m.Load(key) + if !ok { + if f.defaultIDClaims != nil { + return f.defaultIDClaims, true + } + return nil, false + } + return v, true +} + func httpErrorCode(defaultCode int, err error) int { var stautsErr statusHookError status := defaultCode diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 329f593ba9d4c..c88b8d5c8a685 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -14,9 +14,9 @@ import ( "tailscale.com/tailcfg" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/parameter" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/tailnet" @@ -120,6 +120,7 @@ func User(user database.User, organizationIDs []uuid.UUID) codersdk.User { convertedUser := codersdk.User{ ID: user.ID, Email: user.Email, + Name: user.Name, CreatedAt: user.CreatedAt, LastSeenAt: user.LastSeenAt, Username: user.Username, @@ -380,7 +381,7 @@ func AppSubdomain(dbApp database.WorkspaceApp, agentName, workspaceName, ownerNa if appSlug == "" { appSlug = dbApp.DisplayName } - return httpapi.ApplicationURL{ + return appurl.ApplicationURL{ // We never generate URLs with a prefix. We only allow prefixes when // parsing URLs from the hostname. Users that want this feature can // write out their own URLs. @@ -416,3 +417,19 @@ func Apps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, ownerNa } return apps } + +func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.ProvisionerDaemon { + result := codersdk.ProvisionerDaemon{ + ID: dbDaemon.ID, + CreatedAt: dbDaemon.CreatedAt, + LastSeenAt: codersdk.NullTime{NullTime: dbDaemon.LastSeenAt}, + Name: dbDaemon.Name, + Tags: dbDaemon.Tags, + Version: dbDaemon.Version, + APIVersion: dbDaemon.APIVersion, + } + for _, provisionerType := range dbDaemon.Provisioners { + result.Provisioners = append(result.Provisioners, codersdk.ProvisionerType(provisionerType)) + } + return result +} diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 6e236e3442baf..a5b295e2e35eb 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -695,6 +695,15 @@ func (q *querier) ArchiveUnusedTemplateVersions(ctx context.Context, arg databas return q.db.ArchiveUnusedTemplateVersions(ctx, arg) } +func (q *querier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg database.BatchUpdateWorkspaceLastUsedAtParams) error { + // Could be any workspace and checking auth to each workspace is overkill for the purpose + // of this function. + if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceWorkspace.All()); err != nil { + return err + } + return q.db.BatchUpdateWorkspaceLastUsedAt(ctx, arg) +} + func (q *querier) CleanTailnetCoordinators(ctx context.Context) error { if err := q.authorizeContext(ctx, rbac.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 0d23f33c9c02e..d9444278722e7 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1549,6 +1549,13 @@ func (s *MethodTestSuite) TestWorkspace() { ID: ws.ID, }).Asserts(ws, rbac.ActionUpdate).Returns() })) + s.Run("BatchUpdateWorkspaceLastUsedAt", s.Subtest(func(db database.Store, check *expects) { + ws1 := dbgen.Workspace(s.T(), db, database.Workspace{}) + ws2 := dbgen.Workspace(s.T(), db, database.Workspace{}) + check.Args(database.BatchUpdateWorkspaceLastUsedAtParams{ + IDs: []uuid.UUID{ws1.ID, ws2.ID}, + }).Asserts(rbac.ResourceWorkspace.All(), rbac.ActionUpdate).Returns() + })) s.Run("UpdateWorkspaceTTL", s.Subtest(func(db database.Store, check *expects) { ws := dbgen.Workspace(s.T(), db, database.Workspace{}) check.Args(database.UpdateWorkspaceTTLParams{ diff --git a/coderd/database/dbauthz/setup_test.go b/coderd/database/dbauthz/setup_test.go index 403d23d508213..d3a8ae6b378eb 100644 --- a/coderd/database/dbauthz/setup_test.go +++ b/coderd/database/dbauthz/setup_test.go @@ -9,11 +9,11 @@ import ( "strings" "testing" - "github.com/golang/mock/gomock" "github.com/google/uuid" "github.com/open-policy-agent/opa/topdown" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" "golang.org/x/xerrors" "cdr.dev/slog" diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 4cac09d1dc44f..ea49c78065657 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "testing" + "time" "github.com/google/uuid" "github.com/sqlc-dev/pqtype" @@ -19,6 +20,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/provisionersdk" sdkproto "github.com/coder/coder/v2/provisionersdk/proto" ) @@ -47,6 +49,11 @@ type WorkspaceBuildBuilder struct { resources []*sdkproto.Resource params []database.WorkspaceBuildParameter agentToken string + dispo workspaceBuildDisposition +} + +type workspaceBuildDisposition struct { + starting bool } // WorkspaceBuild generates a workspace build for the provided workspace. @@ -100,6 +107,12 @@ func (b WorkspaceBuildBuilder) WithAgent(mutations ...func([]*sdkproto.Agent) [] return b } +func (b WorkspaceBuildBuilder) Starting() WorkspaceBuildBuilder { + //nolint: revive // returns modified struct + b.dispo.starting = true + return b +} + // Do generates all the resources associated with a workspace build. // Template and TemplateVersion will be optionally populated if no // TemplateID is set on the provided workspace. @@ -161,25 +174,48 @@ func (b WorkspaceBuildBuilder) Do() WorkspaceResponse { FileID: uuid.New(), Type: database.ProvisionerJobTypeWorkspaceBuild, Input: payload, - Tags: nil, + Tags: map[string]string{}, TraceMetadata: pqtype.NullRawMessage{}, }) require.NoError(b.t, err, "insert job") - err = b.db.UpdateProvisionerJobWithCompleteByID(ownerCtx, database.UpdateProvisionerJobWithCompleteByIDParams{ - ID: job.ID, - UpdatedAt: dbtime.Now(), - Error: sql.NullString{}, - ErrorCode: sql.NullString{}, - CompletedAt: sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - }, - }) - require.NoError(b.t, err, "complete job") + if b.dispo.starting { + // might need to do this multiple times if we got a template version + // import job as well + for { + j, err := b.db.AcquireProvisionerJob(ownerCtx, database.AcquireProvisionerJobParams{ + StartedAt: sql.NullTime{ + Time: dbtime.Now(), + Valid: true, + }, + WorkerID: uuid.NullUUID{ + UUID: uuid.New(), + Valid: true, + }, + Types: []database.ProvisionerType{database.ProvisionerTypeEcho}, + Tags: []byte(`{"scope": "organization"}`), + }) + require.NoError(b.t, err, "acquire starting job") + if j.ID == job.ID { + break + } + } + } else { + err = b.db.UpdateProvisionerJobWithCompleteByID(ownerCtx, database.UpdateProvisionerJobWithCompleteByIDParams{ + ID: job.ID, + UpdatedAt: dbtime.Now(), + Error: sql.NullString{}, + ErrorCode: sql.NullString{}, + CompletedAt: sql.NullTime{ + Time: dbtime.Now(), + Valid: true, + }, + }) + require.NoError(b.t, err, "complete job") + ProvisionerJobResources(b.t, b.db, job.ID, b.seed.Transition, b.resources...).Do() + } resp.Build = dbgen.WorkspaceBuild(b.t, b.db, b.seed) - ProvisionerJobResources(b.t, b.db, job.ID, b.seed.Transition, b.resources...).Do() for i := range b.params { b.params[i].WorkspaceBuildID = resp.Build.ID @@ -340,6 +376,53 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse { return resp } +type JobCompleteBuilder struct { + t testing.TB + db database.Store + jobID uuid.UUID + ps pubsub.Pubsub +} + +type JobCompleteResponse struct { + CompletedAt time.Time +} + +func JobComplete(t testing.TB, db database.Store, jobID uuid.UUID) JobCompleteBuilder { + return JobCompleteBuilder{ + t: t, + db: db, + jobID: jobID, + } +} + +func (b JobCompleteBuilder) Pubsub(ps pubsub.Pubsub) JobCompleteBuilder { + // nolint: revive // returns modified struct + b.ps = ps + return b +} + +func (b JobCompleteBuilder) Do() JobCompleteResponse { + r := JobCompleteResponse{CompletedAt: dbtime.Now()} + err := b.db.UpdateProvisionerJobWithCompleteByID(ownerCtx, database.UpdateProvisionerJobWithCompleteByIDParams{ + ID: b.jobID, + UpdatedAt: r.CompletedAt, + Error: sql.NullString{}, + ErrorCode: sql.NullString{}, + CompletedAt: sql.NullTime{ + Time: r.CompletedAt, + Valid: true, + }, + }) + require.NoError(b.t, err, "complete job") + if b.ps != nil { + data, err := json.Marshal(provisionersdk.ProvisionerJobLogsNotifyMessage{EndOfLogs: true}) + require.NoError(b.t, err) + err = b.ps.Publish(provisionersdk.ProvisionerJobLogsNotifyChannel(b.jobID), data) + require.NoError(b.t, err) + } + return r +} + func must[V any](v V, err error) V { if err != nil { panic(err) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 6df7befb0e37a..a4101151d2858 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -72,6 +72,9 @@ func Template(t testing.TB, db database.Store, seed database.Template) database. seed.OrganizationID.String(): []rbac.Action{rbac.ActionRead}, } } + if seed.UserACL == nil { + seed.UserACL = database.TemplateACL{} + } err := db.InsertTemplate(genCtx, database.InsertTemplateParams{ ID: id, CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index e9fdd47987ff2..0800fb5dd0a54 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -21,10 +21,10 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" - "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/regosql" "github.com/coder/coder/v2/coderd/util/slice" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk" ) @@ -963,6 +963,31 @@ func (q *FakeQuerier) ArchiveUnusedTemplateVersions(_ context.Context, arg datab return archived, nil } +func (q *FakeQuerier) BatchUpdateWorkspaceLastUsedAt(_ context.Context, arg database.BatchUpdateWorkspaceLastUsedAtParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + // temporary map to avoid O(q.workspaces*arg.workspaceIds) + m := make(map[uuid.UUID]struct{}) + for _, id := range arg.IDs { + m[id] = struct{}{} + } + n := 0 + for i := 0; i < len(q.workspaces); i++ { + if _, found := m[q.workspaces[i].ID]; !found { + continue + } + q.workspaces[i].LastUsedAt = arg.LastUsedAt + n++ + } + return nil +} + func (*FakeQuerier) CleanTailnetCoordinators(_ context.Context) error { return ErrUnimplemented } @@ -4541,11 +4566,11 @@ func (q *FakeQuerier) GetWorkspaceProxyByHostname(_ context.Context, params data // Compile the app hostname regex. This is slow sadly. if params.AllowWildcardHostname { - wildcardRegexp, err := httpapi.CompileHostnamePattern(proxy.WildcardHostname) + wildcardRegexp, err := appurl.CompileHostnamePattern(proxy.WildcardHostname) if err != nil { return database.WorkspaceProxy{}, xerrors.Errorf("compile hostname pattern %q for proxy %q (%s): %w", proxy.WildcardHostname, proxy.Name, proxy.ID.String(), err) } - if _, ok := httpapi.ExecuteHostnamePattern(wildcardRegexp, params.Hostname); ok { + if _, ok := appurl.ExecuteHostnamePattern(wildcardRegexp, params.Hostname); ok { return proxy, nil } } @@ -6373,6 +6398,8 @@ func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd tpl.DisplayName = arg.DisplayName tpl.Description = arg.Description tpl.Icon = arg.Icon + tpl.GroupACL = arg.GroupACL + tpl.AllowUserCancelWorkspaceJobs = arg.AllowUserCancelWorkspaceJobs q.templates[idx] = tpl return nil } @@ -6666,6 +6693,7 @@ func (q *FakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUs user.Email = arg.Email user.Username = arg.Username user.AvatarURL = arg.AvatarURL + user.Name = arg.Name q.users[index] = user return user, nil } @@ -6791,6 +6819,7 @@ func (q *FakeQuerier) UpdateWorkspaceAgentConnectionByID(_ context.Context, arg agent.LastConnectedAt = arg.LastConnectedAt agent.DisconnectedAt = arg.DisconnectedAt agent.UpdatedAt = arg.UpdatedAt + agent.LastConnectedReplicaID = arg.LastConnectedReplicaID q.workspaceAgents[index] = agent return nil } @@ -7279,6 +7308,7 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up ReplicaID: uuid.NullUUID{}, LastSeenAt: arg.LastSeenAt, Version: arg.Version, + APIVersion: arg.APIVersion, } q.provisionerDaemons = append(q.provisionerDaemons, d) return d, nil diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index d11b376b371c9..625871500dbeb 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -114,6 +114,13 @@ func (m metricsStore) ArchiveUnusedTemplateVersions(ctx context.Context, arg dat return r0, r1 } +func (m metricsStore) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg database.BatchUpdateWorkspaceLastUsedAtParams) error { + start := time.Now() + r0 := m.s.BatchUpdateWorkspaceLastUsedAt(ctx, arg) + m.queryLatencies.WithLabelValues("BatchUpdateWorkspaceLastUsedAt").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) CleanTailnetCoordinators(ctx context.Context) error { start := time.Now() err := m.s.CleanTailnetCoordinators(ctx) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 64c4e73ef1f48..bfb93405f5524 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/coder/coder/v2/coderd/database (interfaces: Store) +// +// Generated by this command: +// +// mockgen -destination ./dbmock.go -package dbmock github.com/coder/coder/v2/coderd/database Store +// // Package dbmock is a generated GoMock package. package dbmock @@ -12,8 +17,8 @@ import ( database "github.com/coder/coder/v2/coderd/database" rbac "github.com/coder/coder/v2/coderd/rbac" - gomock "github.com/golang/mock/gomock" uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" ) // MockStore is a mock of Store interface. @@ -48,7 +53,7 @@ func (m *MockStore) AcquireLock(arg0 context.Context, arg1 int64) error { } // AcquireLock indicates an expected call of AcquireLock. -func (mr *MockStoreMockRecorder) AcquireLock(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AcquireLock(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireLock", reflect.TypeOf((*MockStore)(nil).AcquireLock), arg0, arg1) } @@ -63,7 +68,7 @@ func (m *MockStore) AcquireProvisionerJob(arg0 context.Context, arg1 database.Ac } // AcquireProvisionerJob indicates an expected call of AcquireProvisionerJob. -func (mr *MockStoreMockRecorder) AcquireProvisionerJob(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AcquireProvisionerJob(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireProvisionerJob", reflect.TypeOf((*MockStore)(nil).AcquireProvisionerJob), arg0, arg1) } @@ -77,7 +82,7 @@ func (m *MockStore) ActivityBumpWorkspace(arg0 context.Context, arg1 database.Ac } // ActivityBumpWorkspace indicates an expected call of ActivityBumpWorkspace. -func (mr *MockStoreMockRecorder) ActivityBumpWorkspace(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ActivityBumpWorkspace(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActivityBumpWorkspace", reflect.TypeOf((*MockStore)(nil).ActivityBumpWorkspace), arg0, arg1) } @@ -92,7 +97,7 @@ func (m *MockStore) AllUserIDs(arg0 context.Context) ([]uuid.UUID, error) { } // AllUserIDs indicates an expected call of AllUserIDs. -func (mr *MockStoreMockRecorder) AllUserIDs(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) AllUserIDs(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllUserIDs", reflect.TypeOf((*MockStore)(nil).AllUserIDs), arg0) } @@ -107,11 +112,25 @@ func (m *MockStore) ArchiveUnusedTemplateVersions(arg0 context.Context, arg1 dat } // ArchiveUnusedTemplateVersions indicates an expected call of ArchiveUnusedTemplateVersions. -func (mr *MockStoreMockRecorder) ArchiveUnusedTemplateVersions(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) ArchiveUnusedTemplateVersions(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ArchiveUnusedTemplateVersions", reflect.TypeOf((*MockStore)(nil).ArchiveUnusedTemplateVersions), arg0, arg1) } +// BatchUpdateWorkspaceLastUsedAt mocks base method. +func (m *MockStore) BatchUpdateWorkspaceLastUsedAt(arg0 context.Context, arg1 database.BatchUpdateWorkspaceLastUsedAtParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BatchUpdateWorkspaceLastUsedAt", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// BatchUpdateWorkspaceLastUsedAt indicates an expected call of BatchUpdateWorkspaceLastUsedAt. +func (mr *MockStoreMockRecorder) BatchUpdateWorkspaceLastUsedAt(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchUpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).BatchUpdateWorkspaceLastUsedAt), arg0, arg1) +} + // CleanTailnetCoordinators mocks base method. func (m *MockStore) CleanTailnetCoordinators(arg0 context.Context) error { m.ctrl.T.Helper() @@ -121,7 +140,7 @@ func (m *MockStore) CleanTailnetCoordinators(arg0 context.Context) error { } // CleanTailnetCoordinators indicates an expected call of CleanTailnetCoordinators. -func (mr *MockStoreMockRecorder) CleanTailnetCoordinators(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CleanTailnetCoordinators(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanTailnetCoordinators", reflect.TypeOf((*MockStore)(nil).CleanTailnetCoordinators), arg0) } @@ -135,7 +154,7 @@ func (m *MockStore) CleanTailnetLostPeers(arg0 context.Context) error { } // CleanTailnetLostPeers indicates an expected call of CleanTailnetLostPeers. -func (mr *MockStoreMockRecorder) CleanTailnetLostPeers(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CleanTailnetLostPeers(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanTailnetLostPeers", reflect.TypeOf((*MockStore)(nil).CleanTailnetLostPeers), arg0) } @@ -149,7 +168,7 @@ func (m *MockStore) CleanTailnetTunnels(arg0 context.Context) error { } // CleanTailnetTunnels indicates an expected call of CleanTailnetTunnels. -func (mr *MockStoreMockRecorder) CleanTailnetTunnels(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) CleanTailnetTunnels(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanTailnetTunnels", reflect.TypeOf((*MockStore)(nil).CleanTailnetTunnels), arg0) } @@ -163,7 +182,7 @@ func (m *MockStore) DeleteAPIKeyByID(arg0 context.Context, arg1 string) error { } // DeleteAPIKeyByID indicates an expected call of DeleteAPIKeyByID. -func (mr *MockStoreMockRecorder) DeleteAPIKeyByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteAPIKeyByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAPIKeyByID", reflect.TypeOf((*MockStore)(nil).DeleteAPIKeyByID), arg0, arg1) } @@ -177,7 +196,7 @@ func (m *MockStore) DeleteAPIKeysByUserID(arg0 context.Context, arg1 uuid.UUID) } // DeleteAPIKeysByUserID indicates an expected call of DeleteAPIKeysByUserID. -func (mr *MockStoreMockRecorder) DeleteAPIKeysByUserID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteAPIKeysByUserID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteAPIKeysByUserID), arg0, arg1) } @@ -191,7 +210,7 @@ func (m *MockStore) DeleteAllTailnetClientSubscriptions(arg0 context.Context, ar } // DeleteAllTailnetClientSubscriptions indicates an expected call of DeleteAllTailnetClientSubscriptions. -func (mr *MockStoreMockRecorder) DeleteAllTailnetClientSubscriptions(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteAllTailnetClientSubscriptions(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllTailnetClientSubscriptions", reflect.TypeOf((*MockStore)(nil).DeleteAllTailnetClientSubscriptions), arg0, arg1) } @@ -205,7 +224,7 @@ func (m *MockStore) DeleteAllTailnetTunnels(arg0 context.Context, arg1 database. } // DeleteAllTailnetTunnels indicates an expected call of DeleteAllTailnetTunnels. -func (mr *MockStoreMockRecorder) DeleteAllTailnetTunnels(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteAllTailnetTunnels(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllTailnetTunnels", reflect.TypeOf((*MockStore)(nil).DeleteAllTailnetTunnels), arg0, arg1) } @@ -219,7 +238,7 @@ func (m *MockStore) DeleteApplicationConnectAPIKeysByUserID(arg0 context.Context } // DeleteApplicationConnectAPIKeysByUserID indicates an expected call of DeleteApplicationConnectAPIKeysByUserID. -func (mr *MockStoreMockRecorder) DeleteApplicationConnectAPIKeysByUserID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteApplicationConnectAPIKeysByUserID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApplicationConnectAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteApplicationConnectAPIKeysByUserID), arg0, arg1) } @@ -233,7 +252,7 @@ func (m *MockStore) DeleteCoordinator(arg0 context.Context, arg1 uuid.UUID) erro } // DeleteCoordinator indicates an expected call of DeleteCoordinator. -func (mr *MockStoreMockRecorder) DeleteCoordinator(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteCoordinator(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCoordinator", reflect.TypeOf((*MockStore)(nil).DeleteCoordinator), arg0, arg1) } @@ -247,7 +266,7 @@ func (m *MockStore) DeleteExternalAuthLink(arg0 context.Context, arg1 database.D } // DeleteExternalAuthLink indicates an expected call of DeleteExternalAuthLink. -func (mr *MockStoreMockRecorder) DeleteExternalAuthLink(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteExternalAuthLink(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExternalAuthLink", reflect.TypeOf((*MockStore)(nil).DeleteExternalAuthLink), arg0, arg1) } @@ -261,7 +280,7 @@ func (m *MockStore) DeleteGitSSHKey(arg0 context.Context, arg1 uuid.UUID) error } // DeleteGitSSHKey indicates an expected call of DeleteGitSSHKey. -func (mr *MockStoreMockRecorder) DeleteGitSSHKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteGitSSHKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGitSSHKey", reflect.TypeOf((*MockStore)(nil).DeleteGitSSHKey), arg0, arg1) } @@ -275,7 +294,7 @@ func (m *MockStore) DeleteGroupByID(arg0 context.Context, arg1 uuid.UUID) error } // DeleteGroupByID indicates an expected call of DeleteGroupByID. -func (mr *MockStoreMockRecorder) DeleteGroupByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteGroupByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGroupByID", reflect.TypeOf((*MockStore)(nil).DeleteGroupByID), arg0, arg1) } @@ -289,7 +308,7 @@ func (m *MockStore) DeleteGroupMemberFromGroup(arg0 context.Context, arg1 databa } // DeleteGroupMemberFromGroup indicates an expected call of DeleteGroupMemberFromGroup. -func (mr *MockStoreMockRecorder) DeleteGroupMemberFromGroup(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteGroupMemberFromGroup(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGroupMemberFromGroup", reflect.TypeOf((*MockStore)(nil).DeleteGroupMemberFromGroup), arg0, arg1) } @@ -303,7 +322,7 @@ func (m *MockStore) DeleteGroupMembersByOrgAndUser(arg0 context.Context, arg1 da } // DeleteGroupMembersByOrgAndUser indicates an expected call of DeleteGroupMembersByOrgAndUser. -func (mr *MockStoreMockRecorder) DeleteGroupMembersByOrgAndUser(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteGroupMembersByOrgAndUser(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGroupMembersByOrgAndUser", reflect.TypeOf((*MockStore)(nil).DeleteGroupMembersByOrgAndUser), arg0, arg1) } @@ -318,7 +337,7 @@ func (m *MockStore) DeleteLicense(arg0 context.Context, arg1 int32) (int32, erro } // DeleteLicense indicates an expected call of DeleteLicense. -func (mr *MockStoreMockRecorder) DeleteLicense(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteLicense(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLicense", reflect.TypeOf((*MockStore)(nil).DeleteLicense), arg0, arg1) } @@ -332,7 +351,7 @@ func (m *MockStore) DeleteOAuth2ProviderAppByID(arg0 context.Context, arg1 uuid. } // DeleteOAuth2ProviderAppByID indicates an expected call of DeleteOAuth2ProviderAppByID. -func (mr *MockStoreMockRecorder) DeleteOAuth2ProviderAppByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteOAuth2ProviderAppByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOAuth2ProviderAppByID", reflect.TypeOf((*MockStore)(nil).DeleteOAuth2ProviderAppByID), arg0, arg1) } @@ -346,7 +365,7 @@ func (m *MockStore) DeleteOAuth2ProviderAppSecretByID(arg0 context.Context, arg1 } // DeleteOAuth2ProviderAppSecretByID indicates an expected call of DeleteOAuth2ProviderAppSecretByID. -func (mr *MockStoreMockRecorder) DeleteOAuth2ProviderAppSecretByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteOAuth2ProviderAppSecretByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOAuth2ProviderAppSecretByID", reflect.TypeOf((*MockStore)(nil).DeleteOAuth2ProviderAppSecretByID), arg0, arg1) } @@ -360,7 +379,7 @@ func (m *MockStore) DeleteOldProvisionerDaemons(arg0 context.Context) error { } // DeleteOldProvisionerDaemons indicates an expected call of DeleteOldProvisionerDaemons. -func (mr *MockStoreMockRecorder) DeleteOldProvisionerDaemons(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteOldProvisionerDaemons(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldProvisionerDaemons", reflect.TypeOf((*MockStore)(nil).DeleteOldProvisionerDaemons), arg0) } @@ -374,7 +393,7 @@ func (m *MockStore) DeleteOldWorkspaceAgentLogs(arg0 context.Context) error { } // DeleteOldWorkspaceAgentLogs indicates an expected call of DeleteOldWorkspaceAgentLogs. -func (mr *MockStoreMockRecorder) DeleteOldWorkspaceAgentLogs(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteOldWorkspaceAgentLogs(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldWorkspaceAgentLogs", reflect.TypeOf((*MockStore)(nil).DeleteOldWorkspaceAgentLogs), arg0) } @@ -388,7 +407,7 @@ func (m *MockStore) DeleteOldWorkspaceAgentStats(arg0 context.Context) error { } // DeleteOldWorkspaceAgentStats indicates an expected call of DeleteOldWorkspaceAgentStats. -func (mr *MockStoreMockRecorder) DeleteOldWorkspaceAgentStats(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteOldWorkspaceAgentStats(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldWorkspaceAgentStats", reflect.TypeOf((*MockStore)(nil).DeleteOldWorkspaceAgentStats), arg0) } @@ -402,7 +421,7 @@ func (m *MockStore) DeleteReplicasUpdatedBefore(arg0 context.Context, arg1 time. } // DeleteReplicasUpdatedBefore indicates an expected call of DeleteReplicasUpdatedBefore. -func (mr *MockStoreMockRecorder) DeleteReplicasUpdatedBefore(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteReplicasUpdatedBefore(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteReplicasUpdatedBefore", reflect.TypeOf((*MockStore)(nil).DeleteReplicasUpdatedBefore), arg0, arg1) } @@ -417,7 +436,7 @@ func (m *MockStore) DeleteTailnetAgent(arg0 context.Context, arg1 database.Delet } // DeleteTailnetAgent indicates an expected call of DeleteTailnetAgent. -func (mr *MockStoreMockRecorder) DeleteTailnetAgent(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteTailnetAgent(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetAgent", reflect.TypeOf((*MockStore)(nil).DeleteTailnetAgent), arg0, arg1) } @@ -432,7 +451,7 @@ func (m *MockStore) DeleteTailnetClient(arg0 context.Context, arg1 database.Dele } // DeleteTailnetClient indicates an expected call of DeleteTailnetClient. -func (mr *MockStoreMockRecorder) DeleteTailnetClient(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteTailnetClient(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetClient", reflect.TypeOf((*MockStore)(nil).DeleteTailnetClient), arg0, arg1) } @@ -446,7 +465,7 @@ func (m *MockStore) DeleteTailnetClientSubscription(arg0 context.Context, arg1 d } // DeleteTailnetClientSubscription indicates an expected call of DeleteTailnetClientSubscription. -func (mr *MockStoreMockRecorder) DeleteTailnetClientSubscription(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteTailnetClientSubscription(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetClientSubscription", reflect.TypeOf((*MockStore)(nil).DeleteTailnetClientSubscription), arg0, arg1) } @@ -461,7 +480,7 @@ func (m *MockStore) DeleteTailnetPeer(arg0 context.Context, arg1 database.Delete } // DeleteTailnetPeer indicates an expected call of DeleteTailnetPeer. -func (mr *MockStoreMockRecorder) DeleteTailnetPeer(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteTailnetPeer(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetPeer", reflect.TypeOf((*MockStore)(nil).DeleteTailnetPeer), arg0, arg1) } @@ -476,7 +495,7 @@ func (m *MockStore) DeleteTailnetTunnel(arg0 context.Context, arg1 database.Dele } // DeleteTailnetTunnel indicates an expected call of DeleteTailnetTunnel. -func (mr *MockStoreMockRecorder) DeleteTailnetTunnel(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteTailnetTunnel(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetTunnel", reflect.TypeOf((*MockStore)(nil).DeleteTailnetTunnel), arg0, arg1) } @@ -491,7 +510,7 @@ func (m *MockStore) GetAPIKeyByID(arg0 context.Context, arg1 string) (database.A } // GetAPIKeyByID indicates an expected call of GetAPIKeyByID. -func (mr *MockStoreMockRecorder) GetAPIKeyByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAPIKeyByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIKeyByID", reflect.TypeOf((*MockStore)(nil).GetAPIKeyByID), arg0, arg1) } @@ -506,7 +525,7 @@ func (m *MockStore) GetAPIKeyByName(arg0 context.Context, arg1 database.GetAPIKe } // GetAPIKeyByName indicates an expected call of GetAPIKeyByName. -func (mr *MockStoreMockRecorder) GetAPIKeyByName(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAPIKeyByName(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIKeyByName", reflect.TypeOf((*MockStore)(nil).GetAPIKeyByName), arg0, arg1) } @@ -521,7 +540,7 @@ func (m *MockStore) GetAPIKeysByLoginType(arg0 context.Context, arg1 database.Lo } // GetAPIKeysByLoginType indicates an expected call of GetAPIKeysByLoginType. -func (mr *MockStoreMockRecorder) GetAPIKeysByLoginType(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAPIKeysByLoginType(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIKeysByLoginType", reflect.TypeOf((*MockStore)(nil).GetAPIKeysByLoginType), arg0, arg1) } @@ -536,7 +555,7 @@ func (m *MockStore) GetAPIKeysByUserID(arg0 context.Context, arg1 database.GetAP } // GetAPIKeysByUserID indicates an expected call of GetAPIKeysByUserID. -func (mr *MockStoreMockRecorder) GetAPIKeysByUserID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAPIKeysByUserID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).GetAPIKeysByUserID), arg0, arg1) } @@ -551,7 +570,7 @@ func (m *MockStore) GetAPIKeysLastUsedAfter(arg0 context.Context, arg1 time.Time } // GetAPIKeysLastUsedAfter indicates an expected call of GetAPIKeysLastUsedAfter. -func (mr *MockStoreMockRecorder) GetAPIKeysLastUsedAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAPIKeysLastUsedAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIKeysLastUsedAfter", reflect.TypeOf((*MockStore)(nil).GetAPIKeysLastUsedAfter), arg0, arg1) } @@ -566,7 +585,7 @@ func (m *MockStore) GetActiveUserCount(arg0 context.Context) (int64, error) { } // GetActiveUserCount indicates an expected call of GetActiveUserCount. -func (mr *MockStoreMockRecorder) GetActiveUserCount(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetActiveUserCount(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveUserCount", reflect.TypeOf((*MockStore)(nil).GetActiveUserCount), arg0) } @@ -581,7 +600,7 @@ func (m *MockStore) GetActiveWorkspaceBuildsByTemplateID(arg0 context.Context, a } // GetActiveWorkspaceBuildsByTemplateID indicates an expected call of GetActiveWorkspaceBuildsByTemplateID. -func (mr *MockStoreMockRecorder) GetActiveWorkspaceBuildsByTemplateID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetActiveWorkspaceBuildsByTemplateID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveWorkspaceBuildsByTemplateID", reflect.TypeOf((*MockStore)(nil).GetActiveWorkspaceBuildsByTemplateID), arg0, arg1) } @@ -596,7 +615,7 @@ func (m *MockStore) GetAllTailnetAgents(arg0 context.Context) ([]database.Tailne } // GetAllTailnetAgents indicates an expected call of GetAllTailnetAgents. -func (mr *MockStoreMockRecorder) GetAllTailnetAgents(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAllTailnetAgents(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTailnetAgents", reflect.TypeOf((*MockStore)(nil).GetAllTailnetAgents), arg0) } @@ -611,7 +630,7 @@ func (m *MockStore) GetAllTailnetCoordinators(arg0 context.Context) ([]database. } // GetAllTailnetCoordinators indicates an expected call of GetAllTailnetCoordinators. -func (mr *MockStoreMockRecorder) GetAllTailnetCoordinators(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAllTailnetCoordinators(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTailnetCoordinators", reflect.TypeOf((*MockStore)(nil).GetAllTailnetCoordinators), arg0) } @@ -626,7 +645,7 @@ func (m *MockStore) GetAllTailnetPeers(arg0 context.Context) ([]database.Tailnet } // GetAllTailnetPeers indicates an expected call of GetAllTailnetPeers. -func (mr *MockStoreMockRecorder) GetAllTailnetPeers(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAllTailnetPeers(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTailnetPeers", reflect.TypeOf((*MockStore)(nil).GetAllTailnetPeers), arg0) } @@ -641,7 +660,7 @@ func (m *MockStore) GetAllTailnetTunnels(arg0 context.Context) ([]database.Tailn } // GetAllTailnetTunnels indicates an expected call of GetAllTailnetTunnels. -func (mr *MockStoreMockRecorder) GetAllTailnetTunnels(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAllTailnetTunnels(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTailnetTunnels", reflect.TypeOf((*MockStore)(nil).GetAllTailnetTunnels), arg0) } @@ -656,7 +675,7 @@ func (m *MockStore) GetAppSecurityKey(arg0 context.Context) (string, error) { } // GetAppSecurityKey indicates an expected call of GetAppSecurityKey. -func (mr *MockStoreMockRecorder) GetAppSecurityKey(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAppSecurityKey(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAppSecurityKey", reflect.TypeOf((*MockStore)(nil).GetAppSecurityKey), arg0) } @@ -671,7 +690,7 @@ func (m *MockStore) GetApplicationName(arg0 context.Context) (string, error) { } // GetApplicationName indicates an expected call of GetApplicationName. -func (mr *MockStoreMockRecorder) GetApplicationName(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetApplicationName(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplicationName", reflect.TypeOf((*MockStore)(nil).GetApplicationName), arg0) } @@ -686,7 +705,7 @@ func (m *MockStore) GetAuditLogsOffset(arg0 context.Context, arg1 database.GetAu } // GetAuditLogsOffset indicates an expected call of GetAuditLogsOffset. -func (mr *MockStoreMockRecorder) GetAuditLogsOffset(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAuditLogsOffset(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuditLogsOffset", reflect.TypeOf((*MockStore)(nil).GetAuditLogsOffset), arg0, arg1) } @@ -701,7 +720,7 @@ func (m *MockStore) GetAuthorizationUserRoles(arg0 context.Context, arg1 uuid.UU } // GetAuthorizationUserRoles indicates an expected call of GetAuthorizationUserRoles. -func (mr *MockStoreMockRecorder) GetAuthorizationUserRoles(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAuthorizationUserRoles(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizationUserRoles", reflect.TypeOf((*MockStore)(nil).GetAuthorizationUserRoles), arg0, arg1) } @@ -716,7 +735,7 @@ func (m *MockStore) GetAuthorizedTemplates(arg0 context.Context, arg1 database.G } // GetAuthorizedTemplates indicates an expected call of GetAuthorizedTemplates. -func (mr *MockStoreMockRecorder) GetAuthorizedTemplates(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAuthorizedTemplates(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedTemplates", reflect.TypeOf((*MockStore)(nil).GetAuthorizedTemplates), arg0, arg1, arg2) } @@ -731,7 +750,7 @@ func (m *MockStore) GetAuthorizedUsers(arg0 context.Context, arg1 database.GetUs } // GetAuthorizedUsers indicates an expected call of GetAuthorizedUsers. -func (mr *MockStoreMockRecorder) GetAuthorizedUsers(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAuthorizedUsers(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedUsers", reflect.TypeOf((*MockStore)(nil).GetAuthorizedUsers), arg0, arg1, arg2) } @@ -746,7 +765,7 @@ func (m *MockStore) GetAuthorizedWorkspaces(arg0 context.Context, arg1 database. } // GetAuthorizedWorkspaces indicates an expected call of GetAuthorizedWorkspaces. -func (mr *MockStoreMockRecorder) GetAuthorizedWorkspaces(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetAuthorizedWorkspaces(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspaces", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspaces), arg0, arg1, arg2) } @@ -761,7 +780,7 @@ func (m *MockStore) GetDBCryptKeys(arg0 context.Context) ([]database.DBCryptKey, } // GetDBCryptKeys indicates an expected call of GetDBCryptKeys. -func (mr *MockStoreMockRecorder) GetDBCryptKeys(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDBCryptKeys(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDBCryptKeys", reflect.TypeOf((*MockStore)(nil).GetDBCryptKeys), arg0) } @@ -776,7 +795,7 @@ func (m *MockStore) GetDERPMeshKey(arg0 context.Context) (string, error) { } // GetDERPMeshKey indicates an expected call of GetDERPMeshKey. -func (mr *MockStoreMockRecorder) GetDERPMeshKey(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDERPMeshKey(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDERPMeshKey", reflect.TypeOf((*MockStore)(nil).GetDERPMeshKey), arg0) } @@ -791,7 +810,7 @@ func (m *MockStore) GetDefaultProxyConfig(arg0 context.Context) (database.GetDef } // GetDefaultProxyConfig indicates an expected call of GetDefaultProxyConfig. -func (mr *MockStoreMockRecorder) GetDefaultProxyConfig(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDefaultProxyConfig(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultProxyConfig", reflect.TypeOf((*MockStore)(nil).GetDefaultProxyConfig), arg0) } @@ -806,7 +825,7 @@ func (m *MockStore) GetDeploymentDAUs(arg0 context.Context, arg1 int32) ([]datab } // GetDeploymentDAUs indicates an expected call of GetDeploymentDAUs. -func (mr *MockStoreMockRecorder) GetDeploymentDAUs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDeploymentDAUs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploymentDAUs", reflect.TypeOf((*MockStore)(nil).GetDeploymentDAUs), arg0, arg1) } @@ -821,7 +840,7 @@ func (m *MockStore) GetDeploymentID(arg0 context.Context) (string, error) { } // GetDeploymentID indicates an expected call of GetDeploymentID. -func (mr *MockStoreMockRecorder) GetDeploymentID(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDeploymentID(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploymentID", reflect.TypeOf((*MockStore)(nil).GetDeploymentID), arg0) } @@ -836,7 +855,7 @@ func (m *MockStore) GetDeploymentWorkspaceAgentStats(arg0 context.Context, arg1 } // GetDeploymentWorkspaceAgentStats indicates an expected call of GetDeploymentWorkspaceAgentStats. -func (mr *MockStoreMockRecorder) GetDeploymentWorkspaceAgentStats(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDeploymentWorkspaceAgentStats(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploymentWorkspaceAgentStats", reflect.TypeOf((*MockStore)(nil).GetDeploymentWorkspaceAgentStats), arg0, arg1) } @@ -851,7 +870,7 @@ func (m *MockStore) GetDeploymentWorkspaceStats(arg0 context.Context) (database. } // GetDeploymentWorkspaceStats indicates an expected call of GetDeploymentWorkspaceStats. -func (mr *MockStoreMockRecorder) GetDeploymentWorkspaceStats(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDeploymentWorkspaceStats(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploymentWorkspaceStats", reflect.TypeOf((*MockStore)(nil).GetDeploymentWorkspaceStats), arg0) } @@ -866,7 +885,7 @@ func (m *MockStore) GetExternalAuthLink(arg0 context.Context, arg1 database.GetE } // GetExternalAuthLink indicates an expected call of GetExternalAuthLink. -func (mr *MockStoreMockRecorder) GetExternalAuthLink(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetExternalAuthLink(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExternalAuthLink", reflect.TypeOf((*MockStore)(nil).GetExternalAuthLink), arg0, arg1) } @@ -881,7 +900,7 @@ func (m *MockStore) GetExternalAuthLinksByUserID(arg0 context.Context, arg1 uuid } // GetExternalAuthLinksByUserID indicates an expected call of GetExternalAuthLinksByUserID. -func (mr *MockStoreMockRecorder) GetExternalAuthLinksByUserID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetExternalAuthLinksByUserID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExternalAuthLinksByUserID", reflect.TypeOf((*MockStore)(nil).GetExternalAuthLinksByUserID), arg0, arg1) } @@ -896,7 +915,7 @@ func (m *MockStore) GetFileByHashAndCreator(arg0 context.Context, arg1 database. } // GetFileByHashAndCreator indicates an expected call of GetFileByHashAndCreator. -func (mr *MockStoreMockRecorder) GetFileByHashAndCreator(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetFileByHashAndCreator(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileByHashAndCreator", reflect.TypeOf((*MockStore)(nil).GetFileByHashAndCreator), arg0, arg1) } @@ -911,7 +930,7 @@ func (m *MockStore) GetFileByID(arg0 context.Context, arg1 uuid.UUID) (database. } // GetFileByID indicates an expected call of GetFileByID. -func (mr *MockStoreMockRecorder) GetFileByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetFileByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileByID", reflect.TypeOf((*MockStore)(nil).GetFileByID), arg0, arg1) } @@ -926,7 +945,7 @@ func (m *MockStore) GetFileTemplates(arg0 context.Context, arg1 uuid.UUID) ([]da } // GetFileTemplates indicates an expected call of GetFileTemplates. -func (mr *MockStoreMockRecorder) GetFileTemplates(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetFileTemplates(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileTemplates", reflect.TypeOf((*MockStore)(nil).GetFileTemplates), arg0, arg1) } @@ -941,7 +960,7 @@ func (m *MockStore) GetGitSSHKey(arg0 context.Context, arg1 uuid.UUID) (database } // GetGitSSHKey indicates an expected call of GetGitSSHKey. -func (mr *MockStoreMockRecorder) GetGitSSHKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGitSSHKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGitSSHKey", reflect.TypeOf((*MockStore)(nil).GetGitSSHKey), arg0, arg1) } @@ -956,7 +975,7 @@ func (m *MockStore) GetGroupByID(arg0 context.Context, arg1 uuid.UUID) (database } // GetGroupByID indicates an expected call of GetGroupByID. -func (mr *MockStoreMockRecorder) GetGroupByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByID", reflect.TypeOf((*MockStore)(nil).GetGroupByID), arg0, arg1) } @@ -971,7 +990,7 @@ func (m *MockStore) GetGroupByOrgAndName(arg0 context.Context, arg1 database.Get } // GetGroupByOrgAndName indicates an expected call of GetGroupByOrgAndName. -func (mr *MockStoreMockRecorder) GetGroupByOrgAndName(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupByOrgAndName(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupByOrgAndName", reflect.TypeOf((*MockStore)(nil).GetGroupByOrgAndName), arg0, arg1) } @@ -986,7 +1005,7 @@ func (m *MockStore) GetGroupMembers(arg0 context.Context, arg1 uuid.UUID) ([]dat } // GetGroupMembers indicates an expected call of GetGroupMembers. -func (mr *MockStoreMockRecorder) GetGroupMembers(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupMembers(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupMembers", reflect.TypeOf((*MockStore)(nil).GetGroupMembers), arg0, arg1) } @@ -1001,7 +1020,7 @@ func (m *MockStore) GetGroupsByOrganizationID(arg0 context.Context, arg1 uuid.UU } // GetGroupsByOrganizationID indicates an expected call of GetGroupsByOrganizationID. -func (mr *MockStoreMockRecorder) GetGroupsByOrganizationID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetGroupsByOrganizationID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsByOrganizationID", reflect.TypeOf((*MockStore)(nil).GetGroupsByOrganizationID), arg0, arg1) } @@ -1016,7 +1035,7 @@ func (m *MockStore) GetHealthSettings(arg0 context.Context) (string, error) { } // GetHealthSettings indicates an expected call of GetHealthSettings. -func (mr *MockStoreMockRecorder) GetHealthSettings(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetHealthSettings(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHealthSettings", reflect.TypeOf((*MockStore)(nil).GetHealthSettings), arg0) } @@ -1031,7 +1050,7 @@ func (m *MockStore) GetHungProvisionerJobs(arg0 context.Context, arg1 time.Time) } // GetHungProvisionerJobs indicates an expected call of GetHungProvisionerJobs. -func (mr *MockStoreMockRecorder) GetHungProvisionerJobs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetHungProvisionerJobs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHungProvisionerJobs", reflect.TypeOf((*MockStore)(nil).GetHungProvisionerJobs), arg0, arg1) } @@ -1046,7 +1065,7 @@ func (m *MockStore) GetLastUpdateCheck(arg0 context.Context) (string, error) { } // GetLastUpdateCheck indicates an expected call of GetLastUpdateCheck. -func (mr *MockStoreMockRecorder) GetLastUpdateCheck(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetLastUpdateCheck(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastUpdateCheck", reflect.TypeOf((*MockStore)(nil).GetLastUpdateCheck), arg0) } @@ -1061,7 +1080,7 @@ func (m *MockStore) GetLatestWorkspaceBuildByWorkspaceID(arg0 context.Context, a } // GetLatestWorkspaceBuildByWorkspaceID indicates an expected call of GetLatestWorkspaceBuildByWorkspaceID. -func (mr *MockStoreMockRecorder) GetLatestWorkspaceBuildByWorkspaceID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetLatestWorkspaceBuildByWorkspaceID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestWorkspaceBuildByWorkspaceID", reflect.TypeOf((*MockStore)(nil).GetLatestWorkspaceBuildByWorkspaceID), arg0, arg1) } @@ -1076,7 +1095,7 @@ func (m *MockStore) GetLatestWorkspaceBuilds(arg0 context.Context) ([]database.W } // GetLatestWorkspaceBuilds indicates an expected call of GetLatestWorkspaceBuilds. -func (mr *MockStoreMockRecorder) GetLatestWorkspaceBuilds(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetLatestWorkspaceBuilds(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestWorkspaceBuilds", reflect.TypeOf((*MockStore)(nil).GetLatestWorkspaceBuilds), arg0) } @@ -1091,7 +1110,7 @@ func (m *MockStore) GetLatestWorkspaceBuildsByWorkspaceIDs(arg0 context.Context, } // GetLatestWorkspaceBuildsByWorkspaceIDs indicates an expected call of GetLatestWorkspaceBuildsByWorkspaceIDs. -func (mr *MockStoreMockRecorder) GetLatestWorkspaceBuildsByWorkspaceIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetLatestWorkspaceBuildsByWorkspaceIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestWorkspaceBuildsByWorkspaceIDs", reflect.TypeOf((*MockStore)(nil).GetLatestWorkspaceBuildsByWorkspaceIDs), arg0, arg1) } @@ -1106,7 +1125,7 @@ func (m *MockStore) GetLicenseByID(arg0 context.Context, arg1 int32) (database.L } // GetLicenseByID indicates an expected call of GetLicenseByID. -func (mr *MockStoreMockRecorder) GetLicenseByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetLicenseByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLicenseByID", reflect.TypeOf((*MockStore)(nil).GetLicenseByID), arg0, arg1) } @@ -1121,7 +1140,7 @@ func (m *MockStore) GetLicenses(arg0 context.Context) ([]database.License, error } // GetLicenses indicates an expected call of GetLicenses. -func (mr *MockStoreMockRecorder) GetLicenses(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetLicenses(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLicenses", reflect.TypeOf((*MockStore)(nil).GetLicenses), arg0) } @@ -1136,7 +1155,7 @@ func (m *MockStore) GetLogoURL(arg0 context.Context) (string, error) { } // GetLogoURL indicates an expected call of GetLogoURL. -func (mr *MockStoreMockRecorder) GetLogoURL(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetLogoURL(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogoURL", reflect.TypeOf((*MockStore)(nil).GetLogoURL), arg0) } @@ -1151,7 +1170,7 @@ func (m *MockStore) GetOAuth2ProviderAppByID(arg0 context.Context, arg1 uuid.UUI } // GetOAuth2ProviderAppByID indicates an expected call of GetOAuth2ProviderAppByID. -func (mr *MockStoreMockRecorder) GetOAuth2ProviderAppByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOAuth2ProviderAppByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2ProviderAppByID", reflect.TypeOf((*MockStore)(nil).GetOAuth2ProviderAppByID), arg0, arg1) } @@ -1166,7 +1185,7 @@ func (m *MockStore) GetOAuth2ProviderAppSecretByID(arg0 context.Context, arg1 uu } // GetOAuth2ProviderAppSecretByID indicates an expected call of GetOAuth2ProviderAppSecretByID. -func (mr *MockStoreMockRecorder) GetOAuth2ProviderAppSecretByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOAuth2ProviderAppSecretByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2ProviderAppSecretByID", reflect.TypeOf((*MockStore)(nil).GetOAuth2ProviderAppSecretByID), arg0, arg1) } @@ -1181,7 +1200,7 @@ func (m *MockStore) GetOAuth2ProviderAppSecretsByAppID(arg0 context.Context, arg } // GetOAuth2ProviderAppSecretsByAppID indicates an expected call of GetOAuth2ProviderAppSecretsByAppID. -func (mr *MockStoreMockRecorder) GetOAuth2ProviderAppSecretsByAppID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOAuth2ProviderAppSecretsByAppID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2ProviderAppSecretsByAppID", reflect.TypeOf((*MockStore)(nil).GetOAuth2ProviderAppSecretsByAppID), arg0, arg1) } @@ -1196,7 +1215,7 @@ func (m *MockStore) GetOAuth2ProviderApps(arg0 context.Context) ([]database.OAut } // GetOAuth2ProviderApps indicates an expected call of GetOAuth2ProviderApps. -func (mr *MockStoreMockRecorder) GetOAuth2ProviderApps(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOAuth2ProviderApps(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuth2ProviderApps", reflect.TypeOf((*MockStore)(nil).GetOAuth2ProviderApps), arg0) } @@ -1211,7 +1230,7 @@ func (m *MockStore) GetOAuthSigningKey(arg0 context.Context) (string, error) { } // GetOAuthSigningKey indicates an expected call of GetOAuthSigningKey. -func (mr *MockStoreMockRecorder) GetOAuthSigningKey(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOAuthSigningKey(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOAuthSigningKey", reflect.TypeOf((*MockStore)(nil).GetOAuthSigningKey), arg0) } @@ -1226,7 +1245,7 @@ func (m *MockStore) GetOrganizationByID(arg0 context.Context, arg1 uuid.UUID) (d } // GetOrganizationByID indicates an expected call of GetOrganizationByID. -func (mr *MockStoreMockRecorder) GetOrganizationByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOrganizationByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationByID", reflect.TypeOf((*MockStore)(nil).GetOrganizationByID), arg0, arg1) } @@ -1241,7 +1260,7 @@ func (m *MockStore) GetOrganizationByName(arg0 context.Context, arg1 string) (da } // GetOrganizationByName indicates an expected call of GetOrganizationByName. -func (mr *MockStoreMockRecorder) GetOrganizationByName(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOrganizationByName(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationByName", reflect.TypeOf((*MockStore)(nil).GetOrganizationByName), arg0, arg1) } @@ -1256,7 +1275,7 @@ func (m *MockStore) GetOrganizationIDsByMemberIDs(arg0 context.Context, arg1 []u } // GetOrganizationIDsByMemberIDs indicates an expected call of GetOrganizationIDsByMemberIDs. -func (mr *MockStoreMockRecorder) GetOrganizationIDsByMemberIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOrganizationIDsByMemberIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationIDsByMemberIDs", reflect.TypeOf((*MockStore)(nil).GetOrganizationIDsByMemberIDs), arg0, arg1) } @@ -1271,7 +1290,7 @@ func (m *MockStore) GetOrganizationMemberByUserID(arg0 context.Context, arg1 dat } // GetOrganizationMemberByUserID indicates an expected call of GetOrganizationMemberByUserID. -func (mr *MockStoreMockRecorder) GetOrganizationMemberByUserID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOrganizationMemberByUserID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationMemberByUserID", reflect.TypeOf((*MockStore)(nil).GetOrganizationMemberByUserID), arg0, arg1) } @@ -1286,7 +1305,7 @@ func (m *MockStore) GetOrganizationMembershipsByUserID(arg0 context.Context, arg } // GetOrganizationMembershipsByUserID indicates an expected call of GetOrganizationMembershipsByUserID. -func (mr *MockStoreMockRecorder) GetOrganizationMembershipsByUserID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOrganizationMembershipsByUserID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationMembershipsByUserID", reflect.TypeOf((*MockStore)(nil).GetOrganizationMembershipsByUserID), arg0, arg1) } @@ -1301,7 +1320,7 @@ func (m *MockStore) GetOrganizations(arg0 context.Context) ([]database.Organizat } // GetOrganizations indicates an expected call of GetOrganizations. -func (mr *MockStoreMockRecorder) GetOrganizations(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOrganizations(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizations", reflect.TypeOf((*MockStore)(nil).GetOrganizations), arg0) } @@ -1316,7 +1335,7 @@ func (m *MockStore) GetOrganizationsByUserID(arg0 context.Context, arg1 uuid.UUI } // GetOrganizationsByUserID indicates an expected call of GetOrganizationsByUserID. -func (mr *MockStoreMockRecorder) GetOrganizationsByUserID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetOrganizationsByUserID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationsByUserID", reflect.TypeOf((*MockStore)(nil).GetOrganizationsByUserID), arg0, arg1) } @@ -1331,7 +1350,7 @@ func (m *MockStore) GetParameterSchemasByJobID(arg0 context.Context, arg1 uuid.U } // GetParameterSchemasByJobID indicates an expected call of GetParameterSchemasByJobID. -func (mr *MockStoreMockRecorder) GetParameterSchemasByJobID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetParameterSchemasByJobID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParameterSchemasByJobID", reflect.TypeOf((*MockStore)(nil).GetParameterSchemasByJobID), arg0, arg1) } @@ -1346,7 +1365,7 @@ func (m *MockStore) GetPreviousTemplateVersion(arg0 context.Context, arg1 databa } // GetPreviousTemplateVersion indicates an expected call of GetPreviousTemplateVersion. -func (mr *MockStoreMockRecorder) GetPreviousTemplateVersion(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetPreviousTemplateVersion(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreviousTemplateVersion", reflect.TypeOf((*MockStore)(nil).GetPreviousTemplateVersion), arg0, arg1) } @@ -1361,7 +1380,7 @@ func (m *MockStore) GetProvisionerDaemons(arg0 context.Context) ([]database.Prov } // GetProvisionerDaemons indicates an expected call of GetProvisionerDaemons. -func (mr *MockStoreMockRecorder) GetProvisionerDaemons(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetProvisionerDaemons(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerDaemons", reflect.TypeOf((*MockStore)(nil).GetProvisionerDaemons), arg0) } @@ -1376,7 +1395,7 @@ func (m *MockStore) GetProvisionerJobByID(arg0 context.Context, arg1 uuid.UUID) } // GetProvisionerJobByID indicates an expected call of GetProvisionerJobByID. -func (mr *MockStoreMockRecorder) GetProvisionerJobByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetProvisionerJobByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobByID", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobByID), arg0, arg1) } @@ -1391,7 +1410,7 @@ func (m *MockStore) GetProvisionerJobsByIDs(arg0 context.Context, arg1 []uuid.UU } // GetProvisionerJobsByIDs indicates an expected call of GetProvisionerJobsByIDs. -func (mr *MockStoreMockRecorder) GetProvisionerJobsByIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetProvisionerJobsByIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobsByIDs", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobsByIDs), arg0, arg1) } @@ -1406,7 +1425,7 @@ func (m *MockStore) GetProvisionerJobsByIDsWithQueuePosition(arg0 context.Contex } // GetProvisionerJobsByIDsWithQueuePosition indicates an expected call of GetProvisionerJobsByIDsWithQueuePosition. -func (mr *MockStoreMockRecorder) GetProvisionerJobsByIDsWithQueuePosition(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetProvisionerJobsByIDsWithQueuePosition(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobsByIDsWithQueuePosition", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobsByIDsWithQueuePosition), arg0, arg1) } @@ -1421,7 +1440,7 @@ func (m *MockStore) GetProvisionerJobsCreatedAfter(arg0 context.Context, arg1 ti } // GetProvisionerJobsCreatedAfter indicates an expected call of GetProvisionerJobsCreatedAfter. -func (mr *MockStoreMockRecorder) GetProvisionerJobsCreatedAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetProvisionerJobsCreatedAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerJobsCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetProvisionerJobsCreatedAfter), arg0, arg1) } @@ -1436,7 +1455,7 @@ func (m *MockStore) GetProvisionerLogsAfterID(arg0 context.Context, arg1 databas } // GetProvisionerLogsAfterID indicates an expected call of GetProvisionerLogsAfterID. -func (mr *MockStoreMockRecorder) GetProvisionerLogsAfterID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetProvisionerLogsAfterID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerLogsAfterID", reflect.TypeOf((*MockStore)(nil).GetProvisionerLogsAfterID), arg0, arg1) } @@ -1451,7 +1470,7 @@ func (m *MockStore) GetQuotaAllowanceForUser(arg0 context.Context, arg1 uuid.UUI } // GetQuotaAllowanceForUser indicates an expected call of GetQuotaAllowanceForUser. -func (mr *MockStoreMockRecorder) GetQuotaAllowanceForUser(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetQuotaAllowanceForUser(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQuotaAllowanceForUser", reflect.TypeOf((*MockStore)(nil).GetQuotaAllowanceForUser), arg0, arg1) } @@ -1466,7 +1485,7 @@ func (m *MockStore) GetQuotaConsumedForUser(arg0 context.Context, arg1 uuid.UUID } // GetQuotaConsumedForUser indicates an expected call of GetQuotaConsumedForUser. -func (mr *MockStoreMockRecorder) GetQuotaConsumedForUser(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetQuotaConsumedForUser(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQuotaConsumedForUser", reflect.TypeOf((*MockStore)(nil).GetQuotaConsumedForUser), arg0, arg1) } @@ -1481,7 +1500,7 @@ func (m *MockStore) GetReplicaByID(arg0 context.Context, arg1 uuid.UUID) (databa } // GetReplicaByID indicates an expected call of GetReplicaByID. -func (mr *MockStoreMockRecorder) GetReplicaByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetReplicaByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicaByID", reflect.TypeOf((*MockStore)(nil).GetReplicaByID), arg0, arg1) } @@ -1496,7 +1515,7 @@ func (m *MockStore) GetReplicasUpdatedAfter(arg0 context.Context, arg1 time.Time } // GetReplicasUpdatedAfter indicates an expected call of GetReplicasUpdatedAfter. -func (mr *MockStoreMockRecorder) GetReplicasUpdatedAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetReplicasUpdatedAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicasUpdatedAfter", reflect.TypeOf((*MockStore)(nil).GetReplicasUpdatedAfter), arg0, arg1) } @@ -1511,7 +1530,7 @@ func (m *MockStore) GetServiceBanner(arg0 context.Context) (string, error) { } // GetServiceBanner indicates an expected call of GetServiceBanner. -func (mr *MockStoreMockRecorder) GetServiceBanner(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetServiceBanner(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceBanner", reflect.TypeOf((*MockStore)(nil).GetServiceBanner), arg0) } @@ -1526,7 +1545,7 @@ func (m *MockStore) GetTailnetAgents(arg0 context.Context, arg1 uuid.UUID) ([]da } // GetTailnetAgents indicates an expected call of GetTailnetAgents. -func (mr *MockStoreMockRecorder) GetTailnetAgents(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTailnetAgents(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetAgents", reflect.TypeOf((*MockStore)(nil).GetTailnetAgents), arg0, arg1) } @@ -1541,7 +1560,7 @@ func (m *MockStore) GetTailnetClientsForAgent(arg0 context.Context, arg1 uuid.UU } // GetTailnetClientsForAgent indicates an expected call of GetTailnetClientsForAgent. -func (mr *MockStoreMockRecorder) GetTailnetClientsForAgent(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTailnetClientsForAgent(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetClientsForAgent", reflect.TypeOf((*MockStore)(nil).GetTailnetClientsForAgent), arg0, arg1) } @@ -1556,7 +1575,7 @@ func (m *MockStore) GetTailnetPeers(arg0 context.Context, arg1 uuid.UUID) ([]dat } // GetTailnetPeers indicates an expected call of GetTailnetPeers. -func (mr *MockStoreMockRecorder) GetTailnetPeers(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTailnetPeers(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetPeers", reflect.TypeOf((*MockStore)(nil).GetTailnetPeers), arg0, arg1) } @@ -1571,7 +1590,7 @@ func (m *MockStore) GetTailnetTunnelPeerBindings(arg0 context.Context, arg1 uuid } // GetTailnetTunnelPeerBindings indicates an expected call of GetTailnetTunnelPeerBindings. -func (mr *MockStoreMockRecorder) GetTailnetTunnelPeerBindings(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTailnetTunnelPeerBindings(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetTunnelPeerBindings", reflect.TypeOf((*MockStore)(nil).GetTailnetTunnelPeerBindings), arg0, arg1) } @@ -1586,7 +1605,7 @@ func (m *MockStore) GetTailnetTunnelPeerIDs(arg0 context.Context, arg1 uuid.UUID } // GetTailnetTunnelPeerIDs indicates an expected call of GetTailnetTunnelPeerIDs. -func (mr *MockStoreMockRecorder) GetTailnetTunnelPeerIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTailnetTunnelPeerIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetTunnelPeerIDs", reflect.TypeOf((*MockStore)(nil).GetTailnetTunnelPeerIDs), arg0, arg1) } @@ -1601,7 +1620,7 @@ func (m *MockStore) GetTemplateAppInsights(arg0 context.Context, arg1 database.G } // GetTemplateAppInsights indicates an expected call of GetTemplateAppInsights. -func (mr *MockStoreMockRecorder) GetTemplateAppInsights(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateAppInsights(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateAppInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateAppInsights), arg0, arg1) } @@ -1616,7 +1635,7 @@ func (m *MockStore) GetTemplateAppInsightsByTemplate(arg0 context.Context, arg1 } // GetTemplateAppInsightsByTemplate indicates an expected call of GetTemplateAppInsightsByTemplate. -func (mr *MockStoreMockRecorder) GetTemplateAppInsightsByTemplate(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateAppInsightsByTemplate(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateAppInsightsByTemplate", reflect.TypeOf((*MockStore)(nil).GetTemplateAppInsightsByTemplate), arg0, arg1) } @@ -1631,7 +1650,7 @@ func (m *MockStore) GetTemplateAverageBuildTime(arg0 context.Context, arg1 datab } // GetTemplateAverageBuildTime indicates an expected call of GetTemplateAverageBuildTime. -func (mr *MockStoreMockRecorder) GetTemplateAverageBuildTime(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateAverageBuildTime(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateAverageBuildTime", reflect.TypeOf((*MockStore)(nil).GetTemplateAverageBuildTime), arg0, arg1) } @@ -1646,7 +1665,7 @@ func (m *MockStore) GetTemplateByID(arg0 context.Context, arg1 uuid.UUID) (datab } // GetTemplateByID indicates an expected call of GetTemplateByID. -func (mr *MockStoreMockRecorder) GetTemplateByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateByID", reflect.TypeOf((*MockStore)(nil).GetTemplateByID), arg0, arg1) } @@ -1661,7 +1680,7 @@ func (m *MockStore) GetTemplateByOrganizationAndName(arg0 context.Context, arg1 } // GetTemplateByOrganizationAndName indicates an expected call of GetTemplateByOrganizationAndName. -func (mr *MockStoreMockRecorder) GetTemplateByOrganizationAndName(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateByOrganizationAndName(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateByOrganizationAndName", reflect.TypeOf((*MockStore)(nil).GetTemplateByOrganizationAndName), arg0, arg1) } @@ -1676,7 +1695,7 @@ func (m *MockStore) GetTemplateDAUs(arg0 context.Context, arg1 database.GetTempl } // GetTemplateDAUs indicates an expected call of GetTemplateDAUs. -func (mr *MockStoreMockRecorder) GetTemplateDAUs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateDAUs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateDAUs", reflect.TypeOf((*MockStore)(nil).GetTemplateDAUs), arg0, arg1) } @@ -1691,7 +1710,7 @@ func (m *MockStore) GetTemplateGroupRoles(arg0 context.Context, arg1 uuid.UUID) } // GetTemplateGroupRoles indicates an expected call of GetTemplateGroupRoles. -func (mr *MockStoreMockRecorder) GetTemplateGroupRoles(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateGroupRoles(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateGroupRoles", reflect.TypeOf((*MockStore)(nil).GetTemplateGroupRoles), arg0, arg1) } @@ -1706,7 +1725,7 @@ func (m *MockStore) GetTemplateInsights(arg0 context.Context, arg1 database.GetT } // GetTemplateInsights indicates an expected call of GetTemplateInsights. -func (mr *MockStoreMockRecorder) GetTemplateInsights(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateInsights(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateInsights), arg0, arg1) } @@ -1721,7 +1740,7 @@ func (m *MockStore) GetTemplateInsightsByInterval(arg0 context.Context, arg1 dat } // GetTemplateInsightsByInterval indicates an expected call of GetTemplateInsightsByInterval. -func (mr *MockStoreMockRecorder) GetTemplateInsightsByInterval(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateInsightsByInterval(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateInsightsByInterval", reflect.TypeOf((*MockStore)(nil).GetTemplateInsightsByInterval), arg0, arg1) } @@ -1736,7 +1755,7 @@ func (m *MockStore) GetTemplateInsightsByTemplate(arg0 context.Context, arg1 dat } // GetTemplateInsightsByTemplate indicates an expected call of GetTemplateInsightsByTemplate. -func (mr *MockStoreMockRecorder) GetTemplateInsightsByTemplate(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateInsightsByTemplate(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateInsightsByTemplate", reflect.TypeOf((*MockStore)(nil).GetTemplateInsightsByTemplate), arg0, arg1) } @@ -1751,7 +1770,7 @@ func (m *MockStore) GetTemplateParameterInsights(arg0 context.Context, arg1 data } // GetTemplateParameterInsights indicates an expected call of GetTemplateParameterInsights. -func (mr *MockStoreMockRecorder) GetTemplateParameterInsights(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateParameterInsights(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateParameterInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateParameterInsights), arg0, arg1) } @@ -1766,7 +1785,7 @@ func (m *MockStore) GetTemplateUserRoles(arg0 context.Context, arg1 uuid.UUID) ( } // GetTemplateUserRoles indicates an expected call of GetTemplateUserRoles. -func (mr *MockStoreMockRecorder) GetTemplateUserRoles(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateUserRoles(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateUserRoles", reflect.TypeOf((*MockStore)(nil).GetTemplateUserRoles), arg0, arg1) } @@ -1781,7 +1800,7 @@ func (m *MockStore) GetTemplateVersionByID(arg0 context.Context, arg1 uuid.UUID) } // GetTemplateVersionByID indicates an expected call of GetTemplateVersionByID. -func (mr *MockStoreMockRecorder) GetTemplateVersionByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateVersionByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateVersionByID", reflect.TypeOf((*MockStore)(nil).GetTemplateVersionByID), arg0, arg1) } @@ -1796,7 +1815,7 @@ func (m *MockStore) GetTemplateVersionByJobID(arg0 context.Context, arg1 uuid.UU } // GetTemplateVersionByJobID indicates an expected call of GetTemplateVersionByJobID. -func (mr *MockStoreMockRecorder) GetTemplateVersionByJobID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateVersionByJobID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateVersionByJobID", reflect.TypeOf((*MockStore)(nil).GetTemplateVersionByJobID), arg0, arg1) } @@ -1811,7 +1830,7 @@ func (m *MockStore) GetTemplateVersionByTemplateIDAndName(arg0 context.Context, } // GetTemplateVersionByTemplateIDAndName indicates an expected call of GetTemplateVersionByTemplateIDAndName. -func (mr *MockStoreMockRecorder) GetTemplateVersionByTemplateIDAndName(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateVersionByTemplateIDAndName(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateVersionByTemplateIDAndName", reflect.TypeOf((*MockStore)(nil).GetTemplateVersionByTemplateIDAndName), arg0, arg1) } @@ -1826,7 +1845,7 @@ func (m *MockStore) GetTemplateVersionParameters(arg0 context.Context, arg1 uuid } // GetTemplateVersionParameters indicates an expected call of GetTemplateVersionParameters. -func (mr *MockStoreMockRecorder) GetTemplateVersionParameters(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateVersionParameters(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateVersionParameters", reflect.TypeOf((*MockStore)(nil).GetTemplateVersionParameters), arg0, arg1) } @@ -1841,7 +1860,7 @@ func (m *MockStore) GetTemplateVersionVariables(arg0 context.Context, arg1 uuid. } // GetTemplateVersionVariables indicates an expected call of GetTemplateVersionVariables. -func (mr *MockStoreMockRecorder) GetTemplateVersionVariables(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateVersionVariables(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateVersionVariables", reflect.TypeOf((*MockStore)(nil).GetTemplateVersionVariables), arg0, arg1) } @@ -1856,7 +1875,7 @@ func (m *MockStore) GetTemplateVersionsByIDs(arg0 context.Context, arg1 []uuid.U } // GetTemplateVersionsByIDs indicates an expected call of GetTemplateVersionsByIDs. -func (mr *MockStoreMockRecorder) GetTemplateVersionsByIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateVersionsByIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateVersionsByIDs", reflect.TypeOf((*MockStore)(nil).GetTemplateVersionsByIDs), arg0, arg1) } @@ -1871,7 +1890,7 @@ func (m *MockStore) GetTemplateVersionsByTemplateID(arg0 context.Context, arg1 d } // GetTemplateVersionsByTemplateID indicates an expected call of GetTemplateVersionsByTemplateID. -func (mr *MockStoreMockRecorder) GetTemplateVersionsByTemplateID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateVersionsByTemplateID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateVersionsByTemplateID", reflect.TypeOf((*MockStore)(nil).GetTemplateVersionsByTemplateID), arg0, arg1) } @@ -1886,7 +1905,7 @@ func (m *MockStore) GetTemplateVersionsCreatedAfter(arg0 context.Context, arg1 t } // GetTemplateVersionsCreatedAfter indicates an expected call of GetTemplateVersionsCreatedAfter. -func (mr *MockStoreMockRecorder) GetTemplateVersionsCreatedAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplateVersionsCreatedAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateVersionsCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetTemplateVersionsCreatedAfter), arg0, arg1) } @@ -1901,7 +1920,7 @@ func (m *MockStore) GetTemplates(arg0 context.Context) ([]database.Template, err } // GetTemplates indicates an expected call of GetTemplates. -func (mr *MockStoreMockRecorder) GetTemplates(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplates(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplates", reflect.TypeOf((*MockStore)(nil).GetTemplates), arg0) } @@ -1916,7 +1935,7 @@ func (m *MockStore) GetTemplatesWithFilter(arg0 context.Context, arg1 database.G } // GetTemplatesWithFilter indicates an expected call of GetTemplatesWithFilter. -func (mr *MockStoreMockRecorder) GetTemplatesWithFilter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetTemplatesWithFilter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplatesWithFilter", reflect.TypeOf((*MockStore)(nil).GetTemplatesWithFilter), arg0, arg1) } @@ -1931,7 +1950,7 @@ func (m *MockStore) GetUnexpiredLicenses(arg0 context.Context) ([]database.Licen } // GetUnexpiredLicenses indicates an expected call of GetUnexpiredLicenses. -func (mr *MockStoreMockRecorder) GetUnexpiredLicenses(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUnexpiredLicenses(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnexpiredLicenses", reflect.TypeOf((*MockStore)(nil).GetUnexpiredLicenses), arg0) } @@ -1946,7 +1965,7 @@ func (m *MockStore) GetUserActivityInsights(arg0 context.Context, arg1 database. } // GetUserActivityInsights indicates an expected call of GetUserActivityInsights. -func (mr *MockStoreMockRecorder) GetUserActivityInsights(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserActivityInsights(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserActivityInsights", reflect.TypeOf((*MockStore)(nil).GetUserActivityInsights), arg0, arg1) } @@ -1961,7 +1980,7 @@ func (m *MockStore) GetUserByEmailOrUsername(arg0 context.Context, arg1 database } // GetUserByEmailOrUsername indicates an expected call of GetUserByEmailOrUsername. -func (mr *MockStoreMockRecorder) GetUserByEmailOrUsername(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserByEmailOrUsername(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmailOrUsername", reflect.TypeOf((*MockStore)(nil).GetUserByEmailOrUsername), arg0, arg1) } @@ -1976,7 +1995,7 @@ func (m *MockStore) GetUserByID(arg0 context.Context, arg1 uuid.UUID) (database. } // GetUserByID indicates an expected call of GetUserByID. -func (mr *MockStoreMockRecorder) GetUserByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockStore)(nil).GetUserByID), arg0, arg1) } @@ -1991,7 +2010,7 @@ func (m *MockStore) GetUserCount(arg0 context.Context) (int64, error) { } // GetUserCount indicates an expected call of GetUserCount. -func (mr *MockStoreMockRecorder) GetUserCount(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserCount(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCount", reflect.TypeOf((*MockStore)(nil).GetUserCount), arg0) } @@ -2006,7 +2025,7 @@ func (m *MockStore) GetUserLatencyInsights(arg0 context.Context, arg1 database.G } // GetUserLatencyInsights indicates an expected call of GetUserLatencyInsights. -func (mr *MockStoreMockRecorder) GetUserLatencyInsights(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserLatencyInsights(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserLatencyInsights", reflect.TypeOf((*MockStore)(nil).GetUserLatencyInsights), arg0, arg1) } @@ -2021,7 +2040,7 @@ func (m *MockStore) GetUserLinkByLinkedID(arg0 context.Context, arg1 string) (da } // GetUserLinkByLinkedID indicates an expected call of GetUserLinkByLinkedID. -func (mr *MockStoreMockRecorder) GetUserLinkByLinkedID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserLinkByLinkedID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserLinkByLinkedID", reflect.TypeOf((*MockStore)(nil).GetUserLinkByLinkedID), arg0, arg1) } @@ -2036,7 +2055,7 @@ func (m *MockStore) GetUserLinkByUserIDLoginType(arg0 context.Context, arg1 data } // GetUserLinkByUserIDLoginType indicates an expected call of GetUserLinkByUserIDLoginType. -func (mr *MockStoreMockRecorder) GetUserLinkByUserIDLoginType(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserLinkByUserIDLoginType(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserLinkByUserIDLoginType", reflect.TypeOf((*MockStore)(nil).GetUserLinkByUserIDLoginType), arg0, arg1) } @@ -2051,7 +2070,7 @@ func (m *MockStore) GetUserLinksByUserID(arg0 context.Context, arg1 uuid.UUID) ( } // GetUserLinksByUserID indicates an expected call of GetUserLinksByUserID. -func (mr *MockStoreMockRecorder) GetUserLinksByUserID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUserLinksByUserID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserLinksByUserID", reflect.TypeOf((*MockStore)(nil).GetUserLinksByUserID), arg0, arg1) } @@ -2066,7 +2085,7 @@ func (m *MockStore) GetUsers(arg0 context.Context, arg1 database.GetUsersParams) } // GetUsers indicates an expected call of GetUsers. -func (mr *MockStoreMockRecorder) GetUsers(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUsers(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsers", reflect.TypeOf((*MockStore)(nil).GetUsers), arg0, arg1) } @@ -2081,7 +2100,7 @@ func (m *MockStore) GetUsersByIDs(arg0 context.Context, arg1 []uuid.UUID) ([]dat } // GetUsersByIDs indicates an expected call of GetUsersByIDs. -func (mr *MockStoreMockRecorder) GetUsersByIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetUsersByIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByIDs", reflect.TypeOf((*MockStore)(nil).GetUsersByIDs), arg0, arg1) } @@ -2096,7 +2115,7 @@ func (m *MockStore) GetWorkspaceAgentAndOwnerByAuthToken(arg0 context.Context, a } // GetWorkspaceAgentAndOwnerByAuthToken indicates an expected call of GetWorkspaceAgentAndOwnerByAuthToken. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentAndOwnerByAuthToken(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentAndOwnerByAuthToken(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentAndOwnerByAuthToken", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentAndOwnerByAuthToken), arg0, arg1) } @@ -2111,7 +2130,7 @@ func (m *MockStore) GetWorkspaceAgentByID(arg0 context.Context, arg1 uuid.UUID) } // GetWorkspaceAgentByID indicates an expected call of GetWorkspaceAgentByID. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentByID), arg0, arg1) } @@ -2126,7 +2145,7 @@ func (m *MockStore) GetWorkspaceAgentByInstanceID(arg0 context.Context, arg1 str } // GetWorkspaceAgentByInstanceID indicates an expected call of GetWorkspaceAgentByInstanceID. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentByInstanceID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentByInstanceID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentByInstanceID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentByInstanceID), arg0, arg1) } @@ -2141,7 +2160,7 @@ func (m *MockStore) GetWorkspaceAgentLifecycleStateByID(arg0 context.Context, ar } // GetWorkspaceAgentLifecycleStateByID indicates an expected call of GetWorkspaceAgentLifecycleStateByID. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentLifecycleStateByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentLifecycleStateByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentLifecycleStateByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentLifecycleStateByID), arg0, arg1) } @@ -2156,7 +2175,7 @@ func (m *MockStore) GetWorkspaceAgentLogSourcesByAgentIDs(arg0 context.Context, } // GetWorkspaceAgentLogSourcesByAgentIDs indicates an expected call of GetWorkspaceAgentLogSourcesByAgentIDs. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentLogSourcesByAgentIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentLogSourcesByAgentIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentLogSourcesByAgentIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentLogSourcesByAgentIDs), arg0, arg1) } @@ -2171,7 +2190,7 @@ func (m *MockStore) GetWorkspaceAgentLogsAfter(arg0 context.Context, arg1 databa } // GetWorkspaceAgentLogsAfter indicates an expected call of GetWorkspaceAgentLogsAfter. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentLogsAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentLogsAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentLogsAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentLogsAfter), arg0, arg1) } @@ -2186,7 +2205,7 @@ func (m *MockStore) GetWorkspaceAgentMetadata(arg0 context.Context, arg1 databas } // GetWorkspaceAgentMetadata indicates an expected call of GetWorkspaceAgentMetadata. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentMetadata(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentMetadata(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentMetadata), arg0, arg1) } @@ -2201,7 +2220,7 @@ func (m *MockStore) GetWorkspaceAgentScriptsByAgentIDs(arg0 context.Context, arg } // GetWorkspaceAgentScriptsByAgentIDs indicates an expected call of GetWorkspaceAgentScriptsByAgentIDs. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentScriptsByAgentIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentScriptsByAgentIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentScriptsByAgentIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentScriptsByAgentIDs), arg0, arg1) } @@ -2216,7 +2235,7 @@ func (m *MockStore) GetWorkspaceAgentStats(arg0 context.Context, arg1 time.Time) } // GetWorkspaceAgentStats indicates an expected call of GetWorkspaceAgentStats. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentStats(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentStats(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentStats", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentStats), arg0, arg1) } @@ -2231,7 +2250,7 @@ func (m *MockStore) GetWorkspaceAgentStatsAndLabels(arg0 context.Context, arg1 t } // GetWorkspaceAgentStatsAndLabels indicates an expected call of GetWorkspaceAgentStatsAndLabels. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentStatsAndLabels(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentStatsAndLabels(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentStatsAndLabels", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentStatsAndLabels), arg0, arg1) } @@ -2246,7 +2265,7 @@ func (m *MockStore) GetWorkspaceAgentsByResourceIDs(arg0 context.Context, arg1 [ } // GetWorkspaceAgentsByResourceIDs indicates an expected call of GetWorkspaceAgentsByResourceIDs. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByResourceIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentsByResourceIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsByResourceIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsByResourceIDs), arg0, arg1) } @@ -2261,7 +2280,7 @@ func (m *MockStore) GetWorkspaceAgentsCreatedAfter(arg0 context.Context, arg1 ti } // GetWorkspaceAgentsCreatedAfter indicates an expected call of GetWorkspaceAgentsCreatedAfter. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentsCreatedAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentsCreatedAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsCreatedAfter), arg0, arg1) } @@ -2276,7 +2295,7 @@ func (m *MockStore) GetWorkspaceAgentsInLatestBuildByWorkspaceID(arg0 context.Co } // GetWorkspaceAgentsInLatestBuildByWorkspaceID indicates an expected call of GetWorkspaceAgentsInLatestBuildByWorkspaceID. -func (mr *MockStoreMockRecorder) GetWorkspaceAgentsInLatestBuildByWorkspaceID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAgentsInLatestBuildByWorkspaceID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAgentsInLatestBuildByWorkspaceID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAgentsInLatestBuildByWorkspaceID), arg0, arg1) } @@ -2291,7 +2310,7 @@ func (m *MockStore) GetWorkspaceAppByAgentIDAndSlug(arg0 context.Context, arg1 d } // GetWorkspaceAppByAgentIDAndSlug indicates an expected call of GetWorkspaceAppByAgentIDAndSlug. -func (mr *MockStoreMockRecorder) GetWorkspaceAppByAgentIDAndSlug(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAppByAgentIDAndSlug(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAppByAgentIDAndSlug", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAppByAgentIDAndSlug), arg0, arg1) } @@ -2306,7 +2325,7 @@ func (m *MockStore) GetWorkspaceAppsByAgentID(arg0 context.Context, arg1 uuid.UU } // GetWorkspaceAppsByAgentID indicates an expected call of GetWorkspaceAppsByAgentID. -func (mr *MockStoreMockRecorder) GetWorkspaceAppsByAgentID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAppsByAgentID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAppsByAgentID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAppsByAgentID), arg0, arg1) } @@ -2321,7 +2340,7 @@ func (m *MockStore) GetWorkspaceAppsByAgentIDs(arg0 context.Context, arg1 []uuid } // GetWorkspaceAppsByAgentIDs indicates an expected call of GetWorkspaceAppsByAgentIDs. -func (mr *MockStoreMockRecorder) GetWorkspaceAppsByAgentIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAppsByAgentIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAppsByAgentIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAppsByAgentIDs), arg0, arg1) } @@ -2336,7 +2355,7 @@ func (m *MockStore) GetWorkspaceAppsCreatedAfter(arg0 context.Context, arg1 time } // GetWorkspaceAppsCreatedAfter indicates an expected call of GetWorkspaceAppsCreatedAfter. -func (mr *MockStoreMockRecorder) GetWorkspaceAppsCreatedAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceAppsCreatedAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAppsCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAppsCreatedAfter), arg0, arg1) } @@ -2351,7 +2370,7 @@ func (m *MockStore) GetWorkspaceBuildByID(arg0 context.Context, arg1 uuid.UUID) } // GetWorkspaceBuildByID indicates an expected call of GetWorkspaceBuildByID. -func (mr *MockStoreMockRecorder) GetWorkspaceBuildByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceBuildByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildByID), arg0, arg1) } @@ -2366,7 +2385,7 @@ func (m *MockStore) GetWorkspaceBuildByJobID(arg0 context.Context, arg1 uuid.UUI } // GetWorkspaceBuildByJobID indicates an expected call of GetWorkspaceBuildByJobID. -func (mr *MockStoreMockRecorder) GetWorkspaceBuildByJobID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceBuildByJobID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildByJobID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildByJobID), arg0, arg1) } @@ -2381,7 +2400,7 @@ func (m *MockStore) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(arg0 context.Co } // GetWorkspaceBuildByWorkspaceIDAndBuildNumber indicates an expected call of GetWorkspaceBuildByWorkspaceIDAndBuildNumber. -func (mr *MockStoreMockRecorder) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildByWorkspaceIDAndBuildNumber", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildByWorkspaceIDAndBuildNumber), arg0, arg1) } @@ -2396,7 +2415,7 @@ func (m *MockStore) GetWorkspaceBuildParameters(arg0 context.Context, arg1 uuid. } // GetWorkspaceBuildParameters indicates an expected call of GetWorkspaceBuildParameters. -func (mr *MockStoreMockRecorder) GetWorkspaceBuildParameters(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceBuildParameters(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildParameters", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildParameters), arg0, arg1) } @@ -2411,7 +2430,7 @@ func (m *MockStore) GetWorkspaceBuildsByWorkspaceID(arg0 context.Context, arg1 d } // GetWorkspaceBuildsByWorkspaceID indicates an expected call of GetWorkspaceBuildsByWorkspaceID. -func (mr *MockStoreMockRecorder) GetWorkspaceBuildsByWorkspaceID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceBuildsByWorkspaceID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildsByWorkspaceID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildsByWorkspaceID), arg0, arg1) } @@ -2426,7 +2445,7 @@ func (m *MockStore) GetWorkspaceBuildsCreatedAfter(arg0 context.Context, arg1 ti } // GetWorkspaceBuildsCreatedAfter indicates an expected call of GetWorkspaceBuildsCreatedAfter. -func (mr *MockStoreMockRecorder) GetWorkspaceBuildsCreatedAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceBuildsCreatedAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildsCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildsCreatedAfter), arg0, arg1) } @@ -2441,7 +2460,7 @@ func (m *MockStore) GetWorkspaceByAgentID(arg0 context.Context, arg1 uuid.UUID) } // GetWorkspaceByAgentID indicates an expected call of GetWorkspaceByAgentID. -func (mr *MockStoreMockRecorder) GetWorkspaceByAgentID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceByAgentID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceByAgentID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceByAgentID), arg0, arg1) } @@ -2456,7 +2475,7 @@ func (m *MockStore) GetWorkspaceByID(arg0 context.Context, arg1 uuid.UUID) (data } // GetWorkspaceByID indicates an expected call of GetWorkspaceByID. -func (mr *MockStoreMockRecorder) GetWorkspaceByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceByID), arg0, arg1) } @@ -2471,7 +2490,7 @@ func (m *MockStore) GetWorkspaceByOwnerIDAndName(arg0 context.Context, arg1 data } // GetWorkspaceByOwnerIDAndName indicates an expected call of GetWorkspaceByOwnerIDAndName. -func (mr *MockStoreMockRecorder) GetWorkspaceByOwnerIDAndName(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceByOwnerIDAndName(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceByOwnerIDAndName", reflect.TypeOf((*MockStore)(nil).GetWorkspaceByOwnerIDAndName), arg0, arg1) } @@ -2486,7 +2505,7 @@ func (m *MockStore) GetWorkspaceByWorkspaceAppID(arg0 context.Context, arg1 uuid } // GetWorkspaceByWorkspaceAppID indicates an expected call of GetWorkspaceByWorkspaceAppID. -func (mr *MockStoreMockRecorder) GetWorkspaceByWorkspaceAppID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceByWorkspaceAppID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceByWorkspaceAppID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceByWorkspaceAppID), arg0, arg1) } @@ -2501,7 +2520,7 @@ func (m *MockStore) GetWorkspaceProxies(arg0 context.Context) ([]database.Worksp } // GetWorkspaceProxies indicates an expected call of GetWorkspaceProxies. -func (mr *MockStoreMockRecorder) GetWorkspaceProxies(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceProxies(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceProxies", reflect.TypeOf((*MockStore)(nil).GetWorkspaceProxies), arg0) } @@ -2516,7 +2535,7 @@ func (m *MockStore) GetWorkspaceProxyByHostname(arg0 context.Context, arg1 datab } // GetWorkspaceProxyByHostname indicates an expected call of GetWorkspaceProxyByHostname. -func (mr *MockStoreMockRecorder) GetWorkspaceProxyByHostname(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceProxyByHostname(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceProxyByHostname", reflect.TypeOf((*MockStore)(nil).GetWorkspaceProxyByHostname), arg0, arg1) } @@ -2531,7 +2550,7 @@ func (m *MockStore) GetWorkspaceProxyByID(arg0 context.Context, arg1 uuid.UUID) } // GetWorkspaceProxyByID indicates an expected call of GetWorkspaceProxyByID. -func (mr *MockStoreMockRecorder) GetWorkspaceProxyByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceProxyByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceProxyByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceProxyByID), arg0, arg1) } @@ -2546,7 +2565,7 @@ func (m *MockStore) GetWorkspaceProxyByName(arg0 context.Context, arg1 string) ( } // GetWorkspaceProxyByName indicates an expected call of GetWorkspaceProxyByName. -func (mr *MockStoreMockRecorder) GetWorkspaceProxyByName(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceProxyByName(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceProxyByName", reflect.TypeOf((*MockStore)(nil).GetWorkspaceProxyByName), arg0, arg1) } @@ -2561,7 +2580,7 @@ func (m *MockStore) GetWorkspaceResourceByID(arg0 context.Context, arg1 uuid.UUI } // GetWorkspaceResourceByID indicates an expected call of GetWorkspaceResourceByID. -func (mr *MockStoreMockRecorder) GetWorkspaceResourceByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceResourceByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceResourceByID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceResourceByID), arg0, arg1) } @@ -2576,7 +2595,7 @@ func (m *MockStore) GetWorkspaceResourceMetadataByResourceIDs(arg0 context.Conte } // GetWorkspaceResourceMetadataByResourceIDs indicates an expected call of GetWorkspaceResourceMetadataByResourceIDs. -func (mr *MockStoreMockRecorder) GetWorkspaceResourceMetadataByResourceIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceResourceMetadataByResourceIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceResourceMetadataByResourceIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceResourceMetadataByResourceIDs), arg0, arg1) } @@ -2591,7 +2610,7 @@ func (m *MockStore) GetWorkspaceResourceMetadataCreatedAfter(arg0 context.Contex } // GetWorkspaceResourceMetadataCreatedAfter indicates an expected call of GetWorkspaceResourceMetadataCreatedAfter. -func (mr *MockStoreMockRecorder) GetWorkspaceResourceMetadataCreatedAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceResourceMetadataCreatedAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceResourceMetadataCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceResourceMetadataCreatedAfter), arg0, arg1) } @@ -2606,7 +2625,7 @@ func (m *MockStore) GetWorkspaceResourcesByJobID(arg0 context.Context, arg1 uuid } // GetWorkspaceResourcesByJobID indicates an expected call of GetWorkspaceResourcesByJobID. -func (mr *MockStoreMockRecorder) GetWorkspaceResourcesByJobID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceResourcesByJobID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceResourcesByJobID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceResourcesByJobID), arg0, arg1) } @@ -2621,7 +2640,7 @@ func (m *MockStore) GetWorkspaceResourcesByJobIDs(arg0 context.Context, arg1 []u } // GetWorkspaceResourcesByJobIDs indicates an expected call of GetWorkspaceResourcesByJobIDs. -func (mr *MockStoreMockRecorder) GetWorkspaceResourcesByJobIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceResourcesByJobIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceResourcesByJobIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceResourcesByJobIDs), arg0, arg1) } @@ -2636,7 +2655,7 @@ func (m *MockStore) GetWorkspaceResourcesCreatedAfter(arg0 context.Context, arg1 } // GetWorkspaceResourcesCreatedAfter indicates an expected call of GetWorkspaceResourcesCreatedAfter. -func (mr *MockStoreMockRecorder) GetWorkspaceResourcesCreatedAfter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceResourcesCreatedAfter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceResourcesCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceResourcesCreatedAfter), arg0, arg1) } @@ -2651,7 +2670,7 @@ func (m *MockStore) GetWorkspaceUniqueOwnerCountByTemplateIDs(arg0 context.Conte } // GetWorkspaceUniqueOwnerCountByTemplateIDs indicates an expected call of GetWorkspaceUniqueOwnerCountByTemplateIDs. -func (mr *MockStoreMockRecorder) GetWorkspaceUniqueOwnerCountByTemplateIDs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceUniqueOwnerCountByTemplateIDs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceUniqueOwnerCountByTemplateIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceUniqueOwnerCountByTemplateIDs), arg0, arg1) } @@ -2666,7 +2685,7 @@ func (m *MockStore) GetWorkspaces(arg0 context.Context, arg1 database.GetWorkspa } // GetWorkspaces indicates an expected call of GetWorkspaces. -func (mr *MockStoreMockRecorder) GetWorkspaces(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaces(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaces", reflect.TypeOf((*MockStore)(nil).GetWorkspaces), arg0, arg1) } @@ -2681,7 +2700,7 @@ func (m *MockStore) GetWorkspacesEligibleForTransition(arg0 context.Context, arg } // GetWorkspacesEligibleForTransition indicates an expected call of GetWorkspacesEligibleForTransition. -func (mr *MockStoreMockRecorder) GetWorkspacesEligibleForTransition(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspacesEligibleForTransition(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesEligibleForTransition", reflect.TypeOf((*MockStore)(nil).GetWorkspacesEligibleForTransition), arg0, arg1) } @@ -2695,7 +2714,7 @@ func (m *MockStore) InTx(arg0 func(database.Store) error, arg1 *sql.TxOptions) e } // InTx indicates an expected call of InTx. -func (mr *MockStoreMockRecorder) InTx(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InTx(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InTx", reflect.TypeOf((*MockStore)(nil).InTx), arg0, arg1) } @@ -2710,7 +2729,7 @@ func (m *MockStore) InsertAPIKey(arg0 context.Context, arg1 database.InsertAPIKe } // InsertAPIKey indicates an expected call of InsertAPIKey. -func (mr *MockStoreMockRecorder) InsertAPIKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertAPIKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAPIKey", reflect.TypeOf((*MockStore)(nil).InsertAPIKey), arg0, arg1) } @@ -2725,7 +2744,7 @@ func (m *MockStore) InsertAllUsersGroup(arg0 context.Context, arg1 uuid.UUID) (d } // InsertAllUsersGroup indicates an expected call of InsertAllUsersGroup. -func (mr *MockStoreMockRecorder) InsertAllUsersGroup(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertAllUsersGroup(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAllUsersGroup", reflect.TypeOf((*MockStore)(nil).InsertAllUsersGroup), arg0, arg1) } @@ -2740,7 +2759,7 @@ func (m *MockStore) InsertAuditLog(arg0 context.Context, arg1 database.InsertAud } // InsertAuditLog indicates an expected call of InsertAuditLog. -func (mr *MockStoreMockRecorder) InsertAuditLog(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertAuditLog(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAuditLog", reflect.TypeOf((*MockStore)(nil).InsertAuditLog), arg0, arg1) } @@ -2754,7 +2773,7 @@ func (m *MockStore) InsertDBCryptKey(arg0 context.Context, arg1 database.InsertD } // InsertDBCryptKey indicates an expected call of InsertDBCryptKey. -func (mr *MockStoreMockRecorder) InsertDBCryptKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertDBCryptKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertDBCryptKey", reflect.TypeOf((*MockStore)(nil).InsertDBCryptKey), arg0, arg1) } @@ -2768,7 +2787,7 @@ func (m *MockStore) InsertDERPMeshKey(arg0 context.Context, arg1 string) error { } // InsertDERPMeshKey indicates an expected call of InsertDERPMeshKey. -func (mr *MockStoreMockRecorder) InsertDERPMeshKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertDERPMeshKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertDERPMeshKey", reflect.TypeOf((*MockStore)(nil).InsertDERPMeshKey), arg0, arg1) } @@ -2782,7 +2801,7 @@ func (m *MockStore) InsertDeploymentID(arg0 context.Context, arg1 string) error } // InsertDeploymentID indicates an expected call of InsertDeploymentID. -func (mr *MockStoreMockRecorder) InsertDeploymentID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertDeploymentID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertDeploymentID", reflect.TypeOf((*MockStore)(nil).InsertDeploymentID), arg0, arg1) } @@ -2797,7 +2816,7 @@ func (m *MockStore) InsertExternalAuthLink(arg0 context.Context, arg1 database.I } // InsertExternalAuthLink indicates an expected call of InsertExternalAuthLink. -func (mr *MockStoreMockRecorder) InsertExternalAuthLink(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertExternalAuthLink(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertExternalAuthLink", reflect.TypeOf((*MockStore)(nil).InsertExternalAuthLink), arg0, arg1) } @@ -2812,7 +2831,7 @@ func (m *MockStore) InsertFile(arg0 context.Context, arg1 database.InsertFilePar } // InsertFile indicates an expected call of InsertFile. -func (mr *MockStoreMockRecorder) InsertFile(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertFile(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertFile", reflect.TypeOf((*MockStore)(nil).InsertFile), arg0, arg1) } @@ -2827,7 +2846,7 @@ func (m *MockStore) InsertGitSSHKey(arg0 context.Context, arg1 database.InsertGi } // InsertGitSSHKey indicates an expected call of InsertGitSSHKey. -func (mr *MockStoreMockRecorder) InsertGitSSHKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertGitSSHKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertGitSSHKey", reflect.TypeOf((*MockStore)(nil).InsertGitSSHKey), arg0, arg1) } @@ -2842,7 +2861,7 @@ func (m *MockStore) InsertGroup(arg0 context.Context, arg1 database.InsertGroupP } // InsertGroup indicates an expected call of InsertGroup. -func (mr *MockStoreMockRecorder) InsertGroup(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertGroup(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertGroup", reflect.TypeOf((*MockStore)(nil).InsertGroup), arg0, arg1) } @@ -2856,7 +2875,7 @@ func (m *MockStore) InsertGroupMember(arg0 context.Context, arg1 database.Insert } // InsertGroupMember indicates an expected call of InsertGroupMember. -func (mr *MockStoreMockRecorder) InsertGroupMember(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertGroupMember(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertGroupMember", reflect.TypeOf((*MockStore)(nil).InsertGroupMember), arg0, arg1) } @@ -2871,7 +2890,7 @@ func (m *MockStore) InsertLicense(arg0 context.Context, arg1 database.InsertLice } // InsertLicense indicates an expected call of InsertLicense. -func (mr *MockStoreMockRecorder) InsertLicense(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertLicense(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertLicense", reflect.TypeOf((*MockStore)(nil).InsertLicense), arg0, arg1) } @@ -2886,7 +2905,7 @@ func (m *MockStore) InsertMissingGroups(arg0 context.Context, arg1 database.Inse } // InsertMissingGroups indicates an expected call of InsertMissingGroups. -func (mr *MockStoreMockRecorder) InsertMissingGroups(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertMissingGroups(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertMissingGroups", reflect.TypeOf((*MockStore)(nil).InsertMissingGroups), arg0, arg1) } @@ -2901,7 +2920,7 @@ func (m *MockStore) InsertOAuth2ProviderApp(arg0 context.Context, arg1 database. } // InsertOAuth2ProviderApp indicates an expected call of InsertOAuth2ProviderApp. -func (mr *MockStoreMockRecorder) InsertOAuth2ProviderApp(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertOAuth2ProviderApp(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOAuth2ProviderApp", reflect.TypeOf((*MockStore)(nil).InsertOAuth2ProviderApp), arg0, arg1) } @@ -2916,7 +2935,7 @@ func (m *MockStore) InsertOAuth2ProviderAppSecret(arg0 context.Context, arg1 dat } // InsertOAuth2ProviderAppSecret indicates an expected call of InsertOAuth2ProviderAppSecret. -func (mr *MockStoreMockRecorder) InsertOAuth2ProviderAppSecret(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertOAuth2ProviderAppSecret(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOAuth2ProviderAppSecret", reflect.TypeOf((*MockStore)(nil).InsertOAuth2ProviderAppSecret), arg0, arg1) } @@ -2931,7 +2950,7 @@ func (m *MockStore) InsertOrganization(arg0 context.Context, arg1 database.Inser } // InsertOrganization indicates an expected call of InsertOrganization. -func (mr *MockStoreMockRecorder) InsertOrganization(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertOrganization(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOrganization", reflect.TypeOf((*MockStore)(nil).InsertOrganization), arg0, arg1) } @@ -2946,7 +2965,7 @@ func (m *MockStore) InsertOrganizationMember(arg0 context.Context, arg1 database } // InsertOrganizationMember indicates an expected call of InsertOrganizationMember. -func (mr *MockStoreMockRecorder) InsertOrganizationMember(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertOrganizationMember(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOrganizationMember", reflect.TypeOf((*MockStore)(nil).InsertOrganizationMember), arg0, arg1) } @@ -2961,7 +2980,7 @@ func (m *MockStore) InsertProvisionerJob(arg0 context.Context, arg1 database.Ins } // InsertProvisionerJob indicates an expected call of InsertProvisionerJob. -func (mr *MockStoreMockRecorder) InsertProvisionerJob(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertProvisionerJob(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertProvisionerJob", reflect.TypeOf((*MockStore)(nil).InsertProvisionerJob), arg0, arg1) } @@ -2976,7 +2995,7 @@ func (m *MockStore) InsertProvisionerJobLogs(arg0 context.Context, arg1 database } // InsertProvisionerJobLogs indicates an expected call of InsertProvisionerJobLogs. -func (mr *MockStoreMockRecorder) InsertProvisionerJobLogs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertProvisionerJobLogs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertProvisionerJobLogs", reflect.TypeOf((*MockStore)(nil).InsertProvisionerJobLogs), arg0, arg1) } @@ -2991,7 +3010,7 @@ func (m *MockStore) InsertReplica(arg0 context.Context, arg1 database.InsertRepl } // InsertReplica indicates an expected call of InsertReplica. -func (mr *MockStoreMockRecorder) InsertReplica(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertReplica(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertReplica", reflect.TypeOf((*MockStore)(nil).InsertReplica), arg0, arg1) } @@ -3005,7 +3024,7 @@ func (m *MockStore) InsertTemplate(arg0 context.Context, arg1 database.InsertTem } // InsertTemplate indicates an expected call of InsertTemplate. -func (mr *MockStoreMockRecorder) InsertTemplate(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertTemplate(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTemplate", reflect.TypeOf((*MockStore)(nil).InsertTemplate), arg0, arg1) } @@ -3019,7 +3038,7 @@ func (m *MockStore) InsertTemplateVersion(arg0 context.Context, arg1 database.In } // InsertTemplateVersion indicates an expected call of InsertTemplateVersion. -func (mr *MockStoreMockRecorder) InsertTemplateVersion(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertTemplateVersion(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTemplateVersion", reflect.TypeOf((*MockStore)(nil).InsertTemplateVersion), arg0, arg1) } @@ -3034,7 +3053,7 @@ func (m *MockStore) InsertTemplateVersionParameter(arg0 context.Context, arg1 da } // InsertTemplateVersionParameter indicates an expected call of InsertTemplateVersionParameter. -func (mr *MockStoreMockRecorder) InsertTemplateVersionParameter(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertTemplateVersionParameter(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTemplateVersionParameter", reflect.TypeOf((*MockStore)(nil).InsertTemplateVersionParameter), arg0, arg1) } @@ -3049,7 +3068,7 @@ func (m *MockStore) InsertTemplateVersionVariable(arg0 context.Context, arg1 dat } // InsertTemplateVersionVariable indicates an expected call of InsertTemplateVersionVariable. -func (mr *MockStoreMockRecorder) InsertTemplateVersionVariable(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertTemplateVersionVariable(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTemplateVersionVariable", reflect.TypeOf((*MockStore)(nil).InsertTemplateVersionVariable), arg0, arg1) } @@ -3064,7 +3083,7 @@ func (m *MockStore) InsertUser(arg0 context.Context, arg1 database.InsertUserPar } // InsertUser indicates an expected call of InsertUser. -func (mr *MockStoreMockRecorder) InsertUser(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertUser(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUser", reflect.TypeOf((*MockStore)(nil).InsertUser), arg0, arg1) } @@ -3078,7 +3097,7 @@ func (m *MockStore) InsertUserGroupsByName(arg0 context.Context, arg1 database.I } // InsertUserGroupsByName indicates an expected call of InsertUserGroupsByName. -func (mr *MockStoreMockRecorder) InsertUserGroupsByName(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertUserGroupsByName(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUserGroupsByName", reflect.TypeOf((*MockStore)(nil).InsertUserGroupsByName), arg0, arg1) } @@ -3093,7 +3112,7 @@ func (m *MockStore) InsertUserLink(arg0 context.Context, arg1 database.InsertUse } // InsertUserLink indicates an expected call of InsertUserLink. -func (mr *MockStoreMockRecorder) InsertUserLink(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertUserLink(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUserLink", reflect.TypeOf((*MockStore)(nil).InsertUserLink), arg0, arg1) } @@ -3108,7 +3127,7 @@ func (m *MockStore) InsertWorkspace(arg0 context.Context, arg1 database.InsertWo } // InsertWorkspace indicates an expected call of InsertWorkspace. -func (mr *MockStoreMockRecorder) InsertWorkspace(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspace(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspace", reflect.TypeOf((*MockStore)(nil).InsertWorkspace), arg0, arg1) } @@ -3123,7 +3142,7 @@ func (m *MockStore) InsertWorkspaceAgent(arg0 context.Context, arg1 database.Ins } // InsertWorkspaceAgent indicates an expected call of InsertWorkspaceAgent. -func (mr *MockStoreMockRecorder) InsertWorkspaceAgent(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceAgent(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgent", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgent), arg0, arg1) } @@ -3138,7 +3157,7 @@ func (m *MockStore) InsertWorkspaceAgentLogSources(arg0 context.Context, arg1 da } // InsertWorkspaceAgentLogSources indicates an expected call of InsertWorkspaceAgentLogSources. -func (mr *MockStoreMockRecorder) InsertWorkspaceAgentLogSources(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceAgentLogSources(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentLogSources", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentLogSources), arg0, arg1) } @@ -3153,7 +3172,7 @@ func (m *MockStore) InsertWorkspaceAgentLogs(arg0 context.Context, arg1 database } // InsertWorkspaceAgentLogs indicates an expected call of InsertWorkspaceAgentLogs. -func (mr *MockStoreMockRecorder) InsertWorkspaceAgentLogs(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceAgentLogs(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentLogs", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentLogs), arg0, arg1) } @@ -3167,7 +3186,7 @@ func (m *MockStore) InsertWorkspaceAgentMetadata(arg0 context.Context, arg1 data } // InsertWorkspaceAgentMetadata indicates an expected call of InsertWorkspaceAgentMetadata. -func (mr *MockStoreMockRecorder) InsertWorkspaceAgentMetadata(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceAgentMetadata(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentMetadata), arg0, arg1) } @@ -3182,7 +3201,7 @@ func (m *MockStore) InsertWorkspaceAgentScripts(arg0 context.Context, arg1 datab } // InsertWorkspaceAgentScripts indicates an expected call of InsertWorkspaceAgentScripts. -func (mr *MockStoreMockRecorder) InsertWorkspaceAgentScripts(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceAgentScripts(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentScripts", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentScripts), arg0, arg1) } @@ -3197,7 +3216,7 @@ func (m *MockStore) InsertWorkspaceAgentStat(arg0 context.Context, arg1 database } // InsertWorkspaceAgentStat indicates an expected call of InsertWorkspaceAgentStat. -func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStat(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStat(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentStat", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentStat), arg0, arg1) } @@ -3211,7 +3230,7 @@ func (m *MockStore) InsertWorkspaceAgentStats(arg0 context.Context, arg1 databas } // InsertWorkspaceAgentStats indicates an expected call of InsertWorkspaceAgentStats. -func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStats(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceAgentStats(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAgentStats", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAgentStats), arg0, arg1) } @@ -3226,7 +3245,7 @@ func (m *MockStore) InsertWorkspaceApp(arg0 context.Context, arg1 database.Inser } // InsertWorkspaceApp indicates an expected call of InsertWorkspaceApp. -func (mr *MockStoreMockRecorder) InsertWorkspaceApp(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceApp(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceApp", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceApp), arg0, arg1) } @@ -3240,7 +3259,7 @@ func (m *MockStore) InsertWorkspaceAppStats(arg0 context.Context, arg1 database. } // InsertWorkspaceAppStats indicates an expected call of InsertWorkspaceAppStats. -func (mr *MockStoreMockRecorder) InsertWorkspaceAppStats(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceAppStats(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAppStats", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAppStats), arg0, arg1) } @@ -3254,7 +3273,7 @@ func (m *MockStore) InsertWorkspaceBuild(arg0 context.Context, arg1 database.Ins } // InsertWorkspaceBuild indicates an expected call of InsertWorkspaceBuild. -func (mr *MockStoreMockRecorder) InsertWorkspaceBuild(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceBuild(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceBuild", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceBuild), arg0, arg1) } @@ -3268,7 +3287,7 @@ func (m *MockStore) InsertWorkspaceBuildParameters(arg0 context.Context, arg1 da } // InsertWorkspaceBuildParameters indicates an expected call of InsertWorkspaceBuildParameters. -func (mr *MockStoreMockRecorder) InsertWorkspaceBuildParameters(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceBuildParameters(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceBuildParameters", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceBuildParameters), arg0, arg1) } @@ -3283,7 +3302,7 @@ func (m *MockStore) InsertWorkspaceProxy(arg0 context.Context, arg1 database.Ins } // InsertWorkspaceProxy indicates an expected call of InsertWorkspaceProxy. -func (mr *MockStoreMockRecorder) InsertWorkspaceProxy(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceProxy(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceProxy", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceProxy), arg0, arg1) } @@ -3298,7 +3317,7 @@ func (m *MockStore) InsertWorkspaceResource(arg0 context.Context, arg1 database. } // InsertWorkspaceResource indicates an expected call of InsertWorkspaceResource. -func (mr *MockStoreMockRecorder) InsertWorkspaceResource(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceResource(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResource", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResource), arg0, arg1) } @@ -3313,7 +3332,7 @@ func (m *MockStore) InsertWorkspaceResourceMetadata(arg0 context.Context, arg1 d } // InsertWorkspaceResourceMetadata indicates an expected call of InsertWorkspaceResourceMetadata. -func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResourceMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResourceMetadata), arg0, arg1) } @@ -3328,7 +3347,7 @@ func (m *MockStore) Ping(arg0 context.Context) (time.Duration, error) { } // Ping indicates an expected call of Ping. -func (mr *MockStoreMockRecorder) Ping(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) Ping(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockStore)(nil).Ping), arg0) } @@ -3343,7 +3362,7 @@ func (m *MockStore) RegisterWorkspaceProxy(arg0 context.Context, arg1 database.R } // RegisterWorkspaceProxy indicates an expected call of RegisterWorkspaceProxy. -func (mr *MockStoreMockRecorder) RegisterWorkspaceProxy(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) RegisterWorkspaceProxy(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterWorkspaceProxy", reflect.TypeOf((*MockStore)(nil).RegisterWorkspaceProxy), arg0, arg1) } @@ -3357,7 +3376,7 @@ func (m *MockStore) RevokeDBCryptKey(arg0 context.Context, arg1 string) error { } // RevokeDBCryptKey indicates an expected call of RevokeDBCryptKey. -func (mr *MockStoreMockRecorder) RevokeDBCryptKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) RevokeDBCryptKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeDBCryptKey", reflect.TypeOf((*MockStore)(nil).RevokeDBCryptKey), arg0, arg1) } @@ -3372,7 +3391,7 @@ func (m *MockStore) TryAcquireLock(arg0 context.Context, arg1 int64) (bool, erro } // TryAcquireLock indicates an expected call of TryAcquireLock. -func (mr *MockStoreMockRecorder) TryAcquireLock(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) TryAcquireLock(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TryAcquireLock", reflect.TypeOf((*MockStore)(nil).TryAcquireLock), arg0, arg1) } @@ -3386,7 +3405,7 @@ func (m *MockStore) UnarchiveTemplateVersion(arg0 context.Context, arg1 database } // UnarchiveTemplateVersion indicates an expected call of UnarchiveTemplateVersion. -func (mr *MockStoreMockRecorder) UnarchiveTemplateVersion(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UnarchiveTemplateVersion(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnarchiveTemplateVersion", reflect.TypeOf((*MockStore)(nil).UnarchiveTemplateVersion), arg0, arg1) } @@ -3400,7 +3419,7 @@ func (m *MockStore) UpdateAPIKeyByID(arg0 context.Context, arg1 database.UpdateA } // UpdateAPIKeyByID indicates an expected call of UpdateAPIKeyByID. -func (mr *MockStoreMockRecorder) UpdateAPIKeyByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateAPIKeyByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAPIKeyByID", reflect.TypeOf((*MockStore)(nil).UpdateAPIKeyByID), arg0, arg1) } @@ -3415,7 +3434,7 @@ func (m *MockStore) UpdateExternalAuthLink(arg0 context.Context, arg1 database.U } // UpdateExternalAuthLink indicates an expected call of UpdateExternalAuthLink. -func (mr *MockStoreMockRecorder) UpdateExternalAuthLink(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateExternalAuthLink(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateExternalAuthLink", reflect.TypeOf((*MockStore)(nil).UpdateExternalAuthLink), arg0, arg1) } @@ -3430,7 +3449,7 @@ func (m *MockStore) UpdateGitSSHKey(arg0 context.Context, arg1 database.UpdateGi } // UpdateGitSSHKey indicates an expected call of UpdateGitSSHKey. -func (mr *MockStoreMockRecorder) UpdateGitSSHKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateGitSSHKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGitSSHKey", reflect.TypeOf((*MockStore)(nil).UpdateGitSSHKey), arg0, arg1) } @@ -3445,7 +3464,7 @@ func (m *MockStore) UpdateGroupByID(arg0 context.Context, arg1 database.UpdateGr } // UpdateGroupByID indicates an expected call of UpdateGroupByID. -func (mr *MockStoreMockRecorder) UpdateGroupByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateGroupByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGroupByID", reflect.TypeOf((*MockStore)(nil).UpdateGroupByID), arg0, arg1) } @@ -3460,7 +3479,7 @@ func (m *MockStore) UpdateInactiveUsersToDormant(arg0 context.Context, arg1 data } // UpdateInactiveUsersToDormant indicates an expected call of UpdateInactiveUsersToDormant. -func (mr *MockStoreMockRecorder) UpdateInactiveUsersToDormant(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateInactiveUsersToDormant(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInactiveUsersToDormant", reflect.TypeOf((*MockStore)(nil).UpdateInactiveUsersToDormant), arg0, arg1) } @@ -3475,7 +3494,7 @@ func (m *MockStore) UpdateMemberRoles(arg0 context.Context, arg1 database.Update } // UpdateMemberRoles indicates an expected call of UpdateMemberRoles. -func (mr *MockStoreMockRecorder) UpdateMemberRoles(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateMemberRoles(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMemberRoles", reflect.TypeOf((*MockStore)(nil).UpdateMemberRoles), arg0, arg1) } @@ -3490,7 +3509,7 @@ func (m *MockStore) UpdateOAuth2ProviderAppByID(arg0 context.Context, arg1 datab } // UpdateOAuth2ProviderAppByID indicates an expected call of UpdateOAuth2ProviderAppByID. -func (mr *MockStoreMockRecorder) UpdateOAuth2ProviderAppByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateOAuth2ProviderAppByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOAuth2ProviderAppByID", reflect.TypeOf((*MockStore)(nil).UpdateOAuth2ProviderAppByID), arg0, arg1) } @@ -3505,7 +3524,7 @@ func (m *MockStore) UpdateOAuth2ProviderAppSecretByID(arg0 context.Context, arg1 } // UpdateOAuth2ProviderAppSecretByID indicates an expected call of UpdateOAuth2ProviderAppSecretByID. -func (mr *MockStoreMockRecorder) UpdateOAuth2ProviderAppSecretByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateOAuth2ProviderAppSecretByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOAuth2ProviderAppSecretByID", reflect.TypeOf((*MockStore)(nil).UpdateOAuth2ProviderAppSecretByID), arg0, arg1) } @@ -3519,7 +3538,7 @@ func (m *MockStore) UpdateProvisionerDaemonLastSeenAt(arg0 context.Context, arg1 } // UpdateProvisionerDaemonLastSeenAt indicates an expected call of UpdateProvisionerDaemonLastSeenAt. -func (mr *MockStoreMockRecorder) UpdateProvisionerDaemonLastSeenAt(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateProvisionerDaemonLastSeenAt(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProvisionerDaemonLastSeenAt", reflect.TypeOf((*MockStore)(nil).UpdateProvisionerDaemonLastSeenAt), arg0, arg1) } @@ -3533,7 +3552,7 @@ func (m *MockStore) UpdateProvisionerJobByID(arg0 context.Context, arg1 database } // UpdateProvisionerJobByID indicates an expected call of UpdateProvisionerJobByID. -func (mr *MockStoreMockRecorder) UpdateProvisionerJobByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateProvisionerJobByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProvisionerJobByID", reflect.TypeOf((*MockStore)(nil).UpdateProvisionerJobByID), arg0, arg1) } @@ -3547,7 +3566,7 @@ func (m *MockStore) UpdateProvisionerJobWithCancelByID(arg0 context.Context, arg } // UpdateProvisionerJobWithCancelByID indicates an expected call of UpdateProvisionerJobWithCancelByID. -func (mr *MockStoreMockRecorder) UpdateProvisionerJobWithCancelByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateProvisionerJobWithCancelByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProvisionerJobWithCancelByID", reflect.TypeOf((*MockStore)(nil).UpdateProvisionerJobWithCancelByID), arg0, arg1) } @@ -3561,7 +3580,7 @@ func (m *MockStore) UpdateProvisionerJobWithCompleteByID(arg0 context.Context, a } // UpdateProvisionerJobWithCompleteByID indicates an expected call of UpdateProvisionerJobWithCompleteByID. -func (mr *MockStoreMockRecorder) UpdateProvisionerJobWithCompleteByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateProvisionerJobWithCompleteByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProvisionerJobWithCompleteByID", reflect.TypeOf((*MockStore)(nil).UpdateProvisionerJobWithCompleteByID), arg0, arg1) } @@ -3576,7 +3595,7 @@ func (m *MockStore) UpdateReplica(arg0 context.Context, arg1 database.UpdateRepl } // UpdateReplica indicates an expected call of UpdateReplica. -func (mr *MockStoreMockRecorder) UpdateReplica(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateReplica(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateReplica", reflect.TypeOf((*MockStore)(nil).UpdateReplica), arg0, arg1) } @@ -3590,7 +3609,7 @@ func (m *MockStore) UpdateTemplateACLByID(arg0 context.Context, arg1 database.Up } // UpdateTemplateACLByID indicates an expected call of UpdateTemplateACLByID. -func (mr *MockStoreMockRecorder) UpdateTemplateACLByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateACLByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateACLByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateACLByID), arg0, arg1) } @@ -3604,7 +3623,7 @@ func (m *MockStore) UpdateTemplateAccessControlByID(arg0 context.Context, arg1 d } // UpdateTemplateAccessControlByID indicates an expected call of UpdateTemplateAccessControlByID. -func (mr *MockStoreMockRecorder) UpdateTemplateAccessControlByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateAccessControlByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateAccessControlByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateAccessControlByID), arg0, arg1) } @@ -3618,7 +3637,7 @@ func (m *MockStore) UpdateTemplateActiveVersionByID(arg0 context.Context, arg1 d } // UpdateTemplateActiveVersionByID indicates an expected call of UpdateTemplateActiveVersionByID. -func (mr *MockStoreMockRecorder) UpdateTemplateActiveVersionByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateActiveVersionByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateActiveVersionByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateActiveVersionByID), arg0, arg1) } @@ -3632,7 +3651,7 @@ func (m *MockStore) UpdateTemplateDeletedByID(arg0 context.Context, arg1 databas } // UpdateTemplateDeletedByID indicates an expected call of UpdateTemplateDeletedByID. -func (mr *MockStoreMockRecorder) UpdateTemplateDeletedByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateDeletedByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateDeletedByID), arg0, arg1) } @@ -3646,7 +3665,7 @@ func (m *MockStore) UpdateTemplateMetaByID(arg0 context.Context, arg1 database.U } // UpdateTemplateMetaByID indicates an expected call of UpdateTemplateMetaByID. -func (mr *MockStoreMockRecorder) UpdateTemplateMetaByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateMetaByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateMetaByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateMetaByID), arg0, arg1) } @@ -3660,7 +3679,7 @@ func (m *MockStore) UpdateTemplateScheduleByID(arg0 context.Context, arg1 databa } // UpdateTemplateScheduleByID indicates an expected call of UpdateTemplateScheduleByID. -func (mr *MockStoreMockRecorder) UpdateTemplateScheduleByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateScheduleByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateScheduleByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateScheduleByID), arg0, arg1) } @@ -3674,7 +3693,7 @@ func (m *MockStore) UpdateTemplateVersionByID(arg0 context.Context, arg1 databas } // UpdateTemplateVersionByID indicates an expected call of UpdateTemplateVersionByID. -func (mr *MockStoreMockRecorder) UpdateTemplateVersionByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateVersionByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateVersionByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateVersionByID), arg0, arg1) } @@ -3688,7 +3707,7 @@ func (m *MockStore) UpdateTemplateVersionDescriptionByJobID(arg0 context.Context } // UpdateTemplateVersionDescriptionByJobID indicates an expected call of UpdateTemplateVersionDescriptionByJobID. -func (mr *MockStoreMockRecorder) UpdateTemplateVersionDescriptionByJobID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateVersionDescriptionByJobID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateVersionDescriptionByJobID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateVersionDescriptionByJobID), arg0, arg1) } @@ -3702,7 +3721,7 @@ func (m *MockStore) UpdateTemplateVersionExternalAuthProvidersByJobID(arg0 conte } // UpdateTemplateVersionExternalAuthProvidersByJobID indicates an expected call of UpdateTemplateVersionExternalAuthProvidersByJobID. -func (mr *MockStoreMockRecorder) UpdateTemplateVersionExternalAuthProvidersByJobID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateVersionExternalAuthProvidersByJobID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateVersionExternalAuthProvidersByJobID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateVersionExternalAuthProvidersByJobID), arg0, arg1) } @@ -3716,7 +3735,7 @@ func (m *MockStore) UpdateTemplateWorkspacesLastUsedAt(arg0 context.Context, arg } // UpdateTemplateWorkspacesLastUsedAt indicates an expected call of UpdateTemplateWorkspacesLastUsedAt. -func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateWorkspacesLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateTemplateWorkspacesLastUsedAt), arg0, arg1) } @@ -3731,7 +3750,7 @@ func (m *MockStore) UpdateUserAppearanceSettings(arg0 context.Context, arg1 data } // UpdateUserAppearanceSettings indicates an expected call of UpdateUserAppearanceSettings. -func (mr *MockStoreMockRecorder) UpdateUserAppearanceSettings(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserAppearanceSettings(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserAppearanceSettings", reflect.TypeOf((*MockStore)(nil).UpdateUserAppearanceSettings), arg0, arg1) } @@ -3745,7 +3764,7 @@ func (m *MockStore) UpdateUserDeletedByID(arg0 context.Context, arg1 database.Up } // UpdateUserDeletedByID indicates an expected call of UpdateUserDeletedByID. -func (mr *MockStoreMockRecorder) UpdateUserDeletedByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserDeletedByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateUserDeletedByID), arg0, arg1) } @@ -3759,7 +3778,7 @@ func (m *MockStore) UpdateUserHashedPassword(arg0 context.Context, arg1 database } // UpdateUserHashedPassword indicates an expected call of UpdateUserHashedPassword. -func (mr *MockStoreMockRecorder) UpdateUserHashedPassword(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserHashedPassword(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserHashedPassword", reflect.TypeOf((*MockStore)(nil).UpdateUserHashedPassword), arg0, arg1) } @@ -3774,7 +3793,7 @@ func (m *MockStore) UpdateUserLastSeenAt(arg0 context.Context, arg1 database.Upd } // UpdateUserLastSeenAt indicates an expected call of UpdateUserLastSeenAt. -func (mr *MockStoreMockRecorder) UpdateUserLastSeenAt(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserLastSeenAt(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserLastSeenAt", reflect.TypeOf((*MockStore)(nil).UpdateUserLastSeenAt), arg0, arg1) } @@ -3789,7 +3808,7 @@ func (m *MockStore) UpdateUserLink(arg0 context.Context, arg1 database.UpdateUse } // UpdateUserLink indicates an expected call of UpdateUserLink. -func (mr *MockStoreMockRecorder) UpdateUserLink(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserLink(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserLink", reflect.TypeOf((*MockStore)(nil).UpdateUserLink), arg0, arg1) } @@ -3804,7 +3823,7 @@ func (m *MockStore) UpdateUserLinkedID(arg0 context.Context, arg1 database.Updat } // UpdateUserLinkedID indicates an expected call of UpdateUserLinkedID. -func (mr *MockStoreMockRecorder) UpdateUserLinkedID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserLinkedID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserLinkedID", reflect.TypeOf((*MockStore)(nil).UpdateUserLinkedID), arg0, arg1) } @@ -3819,7 +3838,7 @@ func (m *MockStore) UpdateUserLoginType(arg0 context.Context, arg1 database.Upda } // UpdateUserLoginType indicates an expected call of UpdateUserLoginType. -func (mr *MockStoreMockRecorder) UpdateUserLoginType(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserLoginType(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserLoginType", reflect.TypeOf((*MockStore)(nil).UpdateUserLoginType), arg0, arg1) } @@ -3834,7 +3853,7 @@ func (m *MockStore) UpdateUserProfile(arg0 context.Context, arg1 database.Update } // UpdateUserProfile indicates an expected call of UpdateUserProfile. -func (mr *MockStoreMockRecorder) UpdateUserProfile(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserProfile(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserProfile", reflect.TypeOf((*MockStore)(nil).UpdateUserProfile), arg0, arg1) } @@ -3849,7 +3868,7 @@ func (m *MockStore) UpdateUserQuietHoursSchedule(arg0 context.Context, arg1 data } // UpdateUserQuietHoursSchedule indicates an expected call of UpdateUserQuietHoursSchedule. -func (mr *MockStoreMockRecorder) UpdateUserQuietHoursSchedule(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserQuietHoursSchedule(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserQuietHoursSchedule", reflect.TypeOf((*MockStore)(nil).UpdateUserQuietHoursSchedule), arg0, arg1) } @@ -3864,7 +3883,7 @@ func (m *MockStore) UpdateUserRoles(arg0 context.Context, arg1 database.UpdateUs } // UpdateUserRoles indicates an expected call of UpdateUserRoles. -func (mr *MockStoreMockRecorder) UpdateUserRoles(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserRoles(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserRoles", reflect.TypeOf((*MockStore)(nil).UpdateUserRoles), arg0, arg1) } @@ -3879,7 +3898,7 @@ func (m *MockStore) UpdateUserStatus(arg0 context.Context, arg1 database.UpdateU } // UpdateUserStatus indicates an expected call of UpdateUserStatus. -func (mr *MockStoreMockRecorder) UpdateUserStatus(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateUserStatus(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockStore)(nil).UpdateUserStatus), arg0, arg1) } @@ -3894,7 +3913,7 @@ func (m *MockStore) UpdateWorkspace(arg0 context.Context, arg1 database.UpdateWo } // UpdateWorkspace indicates an expected call of UpdateWorkspace. -func (mr *MockStoreMockRecorder) UpdateWorkspace(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspace(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspace", reflect.TypeOf((*MockStore)(nil).UpdateWorkspace), arg0, arg1) } @@ -3908,7 +3927,7 @@ func (m *MockStore) UpdateWorkspaceAgentConnectionByID(arg0 context.Context, arg } // UpdateWorkspaceAgentConnectionByID indicates an expected call of UpdateWorkspaceAgentConnectionByID. -func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentConnectionByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentConnectionByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentConnectionByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentConnectionByID), arg0, arg1) } @@ -3922,7 +3941,7 @@ func (m *MockStore) UpdateWorkspaceAgentLifecycleStateByID(arg0 context.Context, } // UpdateWorkspaceAgentLifecycleStateByID indicates an expected call of UpdateWorkspaceAgentLifecycleStateByID. -func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentLifecycleStateByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentLifecycleStateByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentLifecycleStateByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentLifecycleStateByID), arg0, arg1) } @@ -3936,7 +3955,7 @@ func (m *MockStore) UpdateWorkspaceAgentLogOverflowByID(arg0 context.Context, ar } // UpdateWorkspaceAgentLogOverflowByID indicates an expected call of UpdateWorkspaceAgentLogOverflowByID. -func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentLogOverflowByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentLogOverflowByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentLogOverflowByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentLogOverflowByID), arg0, arg1) } @@ -3950,7 +3969,7 @@ func (m *MockStore) UpdateWorkspaceAgentMetadata(arg0 context.Context, arg1 data } // UpdateWorkspaceAgentMetadata indicates an expected call of UpdateWorkspaceAgentMetadata. -func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentMetadata(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentMetadata(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentMetadata", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentMetadata), arg0, arg1) } @@ -3964,7 +3983,7 @@ func (m *MockStore) UpdateWorkspaceAgentStartupByID(arg0 context.Context, arg1 d } // UpdateWorkspaceAgentStartupByID indicates an expected call of UpdateWorkspaceAgentStartupByID. -func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentStartupByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceAgentStartupByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAgentStartupByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAgentStartupByID), arg0, arg1) } @@ -3978,7 +3997,7 @@ func (m *MockStore) UpdateWorkspaceAppHealthByID(arg0 context.Context, arg1 data } // UpdateWorkspaceAppHealthByID indicates an expected call of UpdateWorkspaceAppHealthByID. -func (mr *MockStoreMockRecorder) UpdateWorkspaceAppHealthByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceAppHealthByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAppHealthByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAppHealthByID), arg0, arg1) } @@ -3992,7 +4011,7 @@ func (m *MockStore) UpdateWorkspaceAutomaticUpdates(arg0 context.Context, arg1 d } // UpdateWorkspaceAutomaticUpdates indicates an expected call of UpdateWorkspaceAutomaticUpdates. -func (mr *MockStoreMockRecorder) UpdateWorkspaceAutomaticUpdates(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceAutomaticUpdates(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAutomaticUpdates", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAutomaticUpdates), arg0, arg1) } @@ -4006,7 +4025,7 @@ func (m *MockStore) UpdateWorkspaceAutostart(arg0 context.Context, arg1 database } // UpdateWorkspaceAutostart indicates an expected call of UpdateWorkspaceAutostart. -func (mr *MockStoreMockRecorder) UpdateWorkspaceAutostart(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceAutostart(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAutostart", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAutostart), arg0, arg1) } @@ -4020,7 +4039,7 @@ func (m *MockStore) UpdateWorkspaceBuildCostByID(arg0 context.Context, arg1 data } // UpdateWorkspaceBuildCostByID indicates an expected call of UpdateWorkspaceBuildCostByID. -func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildCostByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildCostByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceBuildCostByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceBuildCostByID), arg0, arg1) } @@ -4034,7 +4053,7 @@ func (m *MockStore) UpdateWorkspaceBuildDeadlineByID(arg0 context.Context, arg1 } // UpdateWorkspaceBuildDeadlineByID indicates an expected call of UpdateWorkspaceBuildDeadlineByID. -func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildDeadlineByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildDeadlineByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceBuildDeadlineByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceBuildDeadlineByID), arg0, arg1) } @@ -4048,7 +4067,7 @@ func (m *MockStore) UpdateWorkspaceBuildProvisionerStateByID(arg0 context.Contex } // UpdateWorkspaceBuildProvisionerStateByID indicates an expected call of UpdateWorkspaceBuildProvisionerStateByID. -func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildProvisionerStateByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceBuildProvisionerStateByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceBuildProvisionerStateByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceBuildProvisionerStateByID), arg0, arg1) } @@ -4062,7 +4081,7 @@ func (m *MockStore) UpdateWorkspaceDeletedByID(arg0 context.Context, arg1 databa } // UpdateWorkspaceDeletedByID indicates an expected call of UpdateWorkspaceDeletedByID. -func (mr *MockStoreMockRecorder) UpdateWorkspaceDeletedByID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceDeletedByID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceDeletedByID), arg0, arg1) } @@ -4077,7 +4096,7 @@ func (m *MockStore) UpdateWorkspaceDormantDeletingAt(arg0 context.Context, arg1 } // UpdateWorkspaceDormantDeletingAt indicates an expected call of UpdateWorkspaceDormantDeletingAt. -func (mr *MockStoreMockRecorder) UpdateWorkspaceDormantDeletingAt(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceDormantDeletingAt(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceDormantDeletingAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceDormantDeletingAt), arg0, arg1) } @@ -4091,7 +4110,7 @@ func (m *MockStore) UpdateWorkspaceLastUsedAt(arg0 context.Context, arg1 databas } // UpdateWorkspaceLastUsedAt indicates an expected call of UpdateWorkspaceLastUsedAt. -func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLastUsedAt), arg0, arg1) } @@ -4106,7 +4125,7 @@ func (m *MockStore) UpdateWorkspaceProxy(arg0 context.Context, arg1 database.Upd } // UpdateWorkspaceProxy indicates an expected call of UpdateWorkspaceProxy. -func (mr *MockStoreMockRecorder) UpdateWorkspaceProxy(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceProxy(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceProxy", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceProxy), arg0, arg1) } @@ -4120,7 +4139,7 @@ func (m *MockStore) UpdateWorkspaceProxyDeleted(arg0 context.Context, arg1 datab } // UpdateWorkspaceProxyDeleted indicates an expected call of UpdateWorkspaceProxyDeleted. -func (mr *MockStoreMockRecorder) UpdateWorkspaceProxyDeleted(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceProxyDeleted(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceProxyDeleted", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceProxyDeleted), arg0, arg1) } @@ -4134,7 +4153,7 @@ func (m *MockStore) UpdateWorkspaceTTL(arg0 context.Context, arg1 database.Updat } // UpdateWorkspaceTTL indicates an expected call of UpdateWorkspaceTTL. -func (mr *MockStoreMockRecorder) UpdateWorkspaceTTL(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceTTL(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceTTL", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceTTL), arg0, arg1) } @@ -4148,7 +4167,7 @@ func (m *MockStore) UpdateWorkspacesDormantDeletingAtByTemplateID(arg0 context.C } // UpdateWorkspacesDormantDeletingAtByTemplateID indicates an expected call of UpdateWorkspacesDormantDeletingAtByTemplateID. -func (mr *MockStoreMockRecorder) UpdateWorkspacesDormantDeletingAtByTemplateID(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspacesDormantDeletingAtByTemplateID(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesDormantDeletingAtByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesDormantDeletingAtByTemplateID), arg0, arg1) } @@ -4162,7 +4181,7 @@ func (m *MockStore) UpsertAppSecurityKey(arg0 context.Context, arg1 string) erro } // UpsertAppSecurityKey indicates an expected call of UpsertAppSecurityKey. -func (mr *MockStoreMockRecorder) UpsertAppSecurityKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertAppSecurityKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAppSecurityKey", reflect.TypeOf((*MockStore)(nil).UpsertAppSecurityKey), arg0, arg1) } @@ -4176,7 +4195,7 @@ func (m *MockStore) UpsertApplicationName(arg0 context.Context, arg1 string) err } // UpsertApplicationName indicates an expected call of UpsertApplicationName. -func (mr *MockStoreMockRecorder) UpsertApplicationName(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertApplicationName(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertApplicationName", reflect.TypeOf((*MockStore)(nil).UpsertApplicationName), arg0, arg1) } @@ -4190,7 +4209,7 @@ func (m *MockStore) UpsertDefaultProxy(arg0 context.Context, arg1 database.Upser } // UpsertDefaultProxy indicates an expected call of UpsertDefaultProxy. -func (mr *MockStoreMockRecorder) UpsertDefaultProxy(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertDefaultProxy(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertDefaultProxy", reflect.TypeOf((*MockStore)(nil).UpsertDefaultProxy), arg0, arg1) } @@ -4204,7 +4223,7 @@ func (m *MockStore) UpsertHealthSettings(arg0 context.Context, arg1 string) erro } // UpsertHealthSettings indicates an expected call of UpsertHealthSettings. -func (mr *MockStoreMockRecorder) UpsertHealthSettings(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertHealthSettings(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertHealthSettings", reflect.TypeOf((*MockStore)(nil).UpsertHealthSettings), arg0, arg1) } @@ -4218,7 +4237,7 @@ func (m *MockStore) UpsertLastUpdateCheck(arg0 context.Context, arg1 string) err } // UpsertLastUpdateCheck indicates an expected call of UpsertLastUpdateCheck. -func (mr *MockStoreMockRecorder) UpsertLastUpdateCheck(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertLastUpdateCheck(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertLastUpdateCheck", reflect.TypeOf((*MockStore)(nil).UpsertLastUpdateCheck), arg0, arg1) } @@ -4232,7 +4251,7 @@ func (m *MockStore) UpsertLogoURL(arg0 context.Context, arg1 string) error { } // UpsertLogoURL indicates an expected call of UpsertLogoURL. -func (mr *MockStoreMockRecorder) UpsertLogoURL(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertLogoURL(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertLogoURL", reflect.TypeOf((*MockStore)(nil).UpsertLogoURL), arg0, arg1) } @@ -4246,7 +4265,7 @@ func (m *MockStore) UpsertOAuthSigningKey(arg0 context.Context, arg1 string) err } // UpsertOAuthSigningKey indicates an expected call of UpsertOAuthSigningKey. -func (mr *MockStoreMockRecorder) UpsertOAuthSigningKey(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertOAuthSigningKey(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertOAuthSigningKey", reflect.TypeOf((*MockStore)(nil).UpsertOAuthSigningKey), arg0, arg1) } @@ -4261,7 +4280,7 @@ func (m *MockStore) UpsertProvisionerDaemon(arg0 context.Context, arg1 database. } // UpsertProvisionerDaemon indicates an expected call of UpsertProvisionerDaemon. -func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertProvisionerDaemon(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertProvisionerDaemon", reflect.TypeOf((*MockStore)(nil).UpsertProvisionerDaemon), arg0, arg1) } @@ -4275,7 +4294,7 @@ func (m *MockStore) UpsertServiceBanner(arg0 context.Context, arg1 string) error } // UpsertServiceBanner indicates an expected call of UpsertServiceBanner. -func (mr *MockStoreMockRecorder) UpsertServiceBanner(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertServiceBanner(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertServiceBanner", reflect.TypeOf((*MockStore)(nil).UpsertServiceBanner), arg0, arg1) } @@ -4290,7 +4309,7 @@ func (m *MockStore) UpsertTailnetAgent(arg0 context.Context, arg1 database.Upser } // UpsertTailnetAgent indicates an expected call of UpsertTailnetAgent. -func (mr *MockStoreMockRecorder) UpsertTailnetAgent(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertTailnetAgent(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetAgent", reflect.TypeOf((*MockStore)(nil).UpsertTailnetAgent), arg0, arg1) } @@ -4305,7 +4324,7 @@ func (m *MockStore) UpsertTailnetClient(arg0 context.Context, arg1 database.Upse } // UpsertTailnetClient indicates an expected call of UpsertTailnetClient. -func (mr *MockStoreMockRecorder) UpsertTailnetClient(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertTailnetClient(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetClient", reflect.TypeOf((*MockStore)(nil).UpsertTailnetClient), arg0, arg1) } @@ -4319,7 +4338,7 @@ func (m *MockStore) UpsertTailnetClientSubscription(arg0 context.Context, arg1 d } // UpsertTailnetClientSubscription indicates an expected call of UpsertTailnetClientSubscription. -func (mr *MockStoreMockRecorder) UpsertTailnetClientSubscription(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertTailnetClientSubscription(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetClientSubscription", reflect.TypeOf((*MockStore)(nil).UpsertTailnetClientSubscription), arg0, arg1) } @@ -4334,7 +4353,7 @@ func (m *MockStore) UpsertTailnetCoordinator(arg0 context.Context, arg1 uuid.UUI } // UpsertTailnetCoordinator indicates an expected call of UpsertTailnetCoordinator. -func (mr *MockStoreMockRecorder) UpsertTailnetCoordinator(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertTailnetCoordinator(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetCoordinator", reflect.TypeOf((*MockStore)(nil).UpsertTailnetCoordinator), arg0, arg1) } @@ -4349,7 +4368,7 @@ func (m *MockStore) UpsertTailnetPeer(arg0 context.Context, arg1 database.Upsert } // UpsertTailnetPeer indicates an expected call of UpsertTailnetPeer. -func (mr *MockStoreMockRecorder) UpsertTailnetPeer(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertTailnetPeer(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetPeer", reflect.TypeOf((*MockStore)(nil).UpsertTailnetPeer), arg0, arg1) } @@ -4364,7 +4383,7 @@ func (m *MockStore) UpsertTailnetTunnel(arg0 context.Context, arg1 database.Upse } // UpsertTailnetTunnel indicates an expected call of UpsertTailnetTunnel. -func (mr *MockStoreMockRecorder) UpsertTailnetTunnel(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) UpsertTailnetTunnel(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetTunnel", reflect.TypeOf((*MockStore)(nil).UpsertTailnetTunnel), arg0, arg1) } diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index 59000e463888d..c244bca5d4683 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -218,7 +218,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { CreatedAt: now.Add(-14 * 24 * time.Hour), LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-7 * 24 * time.Hour).Add(time.Minute)}, Version: "1.0.0", - APIVersion: "1.0", + APIVersion: provisionersdk.VersionCurrent.String(), }) require.NoError(t, err) _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ @@ -229,7 +229,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { CreatedAt: now.Add(-8 * 24 * time.Hour), LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-8 * 24 * time.Hour).Add(time.Hour)}, Version: "1.0.0", - APIVersion: "1.0", + APIVersion: provisionersdk.VersionCurrent.String(), }) require.NoError(t, err) _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ @@ -242,7 +242,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { }, CreatedAt: now.Add(-9 * 24 * time.Hour), Version: "1.0.0", - APIVersion: "1.0", + APIVersion: provisionersdk.VersionCurrent.String(), }) require.NoError(t, err) _, err = db.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{ @@ -256,7 +256,7 @@ func TestDeleteOldProvisionerDaemons(t *testing.T) { CreatedAt: now.Add(-6 * 24 * time.Hour), LastSeenAt: sql.NullTime{Valid: true, Time: now.Add(-6 * 24 * time.Hour)}, Version: "1.0.0", - APIVersion: "1.0", + APIVersion: provisionersdk.VersionCurrent.String(), }) require.NoError(t, err) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index ee0d9f92f42f2..f9d1e4311b2b2 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -774,13 +774,16 @@ CREATE TABLE users ( deleted boolean DEFAULT false NOT NULL, last_seen_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL, quiet_hours_schedule text DEFAULT ''::text NOT NULL, - theme_preference text DEFAULT ''::text NOT NULL + theme_preference text DEFAULT ''::text NOT NULL, + name text DEFAULT ''::text NOT NULL ); COMMENT ON COLUMN users.quiet_hours_schedule IS 'Daily (!) cron schedule (with optional CRON_TZ) signifying the start of the user''s quiet hours. If empty, the default quiet hours on the instance is used instead.'; COMMENT ON COLUMN users.theme_preference IS '"" can be interpreted as "the user does not care", falling back to the default theme'; +COMMENT ON COLUMN users.name IS 'Name of the Coder user'; + CREATE VIEW visible_users AS SELECT users.id, users.username, diff --git a/coderd/database/migrations/000183_provisionerd_api_version_prefix.down.sql b/coderd/database/migrations/000183_provisionerd_api_version_prefix.down.sql new file mode 100644 index 0000000000000..298d891caa77e --- /dev/null +++ b/coderd/database/migrations/000183_provisionerd_api_version_prefix.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE ONLY provisioner_daemons + ALTER COLUMN api_version SET DEFAULT '1.0'::text; +UPDATE provisioner_daemons + SET api_version = '1.0' + WHERE api_version = 'v1.0'; diff --git a/coderd/database/migrations/000183_provisionerd_api_version_prefix.up.sql b/coderd/database/migrations/000183_provisionerd_api_version_prefix.up.sql new file mode 100644 index 0000000000000..f06719f003150 --- /dev/null +++ b/coderd/database/migrations/000183_provisionerd_api_version_prefix.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE ONLY provisioner_daemons + ALTER COLUMN api_version SET DEFAULT 'v1.0'::text; +UPDATE provisioner_daemons + SET api_version = 'v1.0' + WHERE api_version = '1.0'; diff --git a/coderd/database/migrations/000184_provisionerd_api_version_rm_prefix.down.sql b/coderd/database/migrations/000184_provisionerd_api_version_rm_prefix.down.sql new file mode 100644 index 0000000000000..f06719f003150 --- /dev/null +++ b/coderd/database/migrations/000184_provisionerd_api_version_rm_prefix.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE ONLY provisioner_daemons + ALTER COLUMN api_version SET DEFAULT 'v1.0'::text; +UPDATE provisioner_daemons + SET api_version = 'v1.0' + WHERE api_version = '1.0'; diff --git a/coderd/database/migrations/000184_provisionerd_api_version_rm_prefix.up.sql b/coderd/database/migrations/000184_provisionerd_api_version_rm_prefix.up.sql new file mode 100644 index 0000000000000..298d891caa77e --- /dev/null +++ b/coderd/database/migrations/000184_provisionerd_api_version_rm_prefix.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE ONLY provisioner_daemons + ALTER COLUMN api_version SET DEFAULT '1.0'::text; +UPDATE provisioner_daemons + SET api_version = '1.0' + WHERE api_version = 'v1.0'; diff --git a/coderd/database/migrations/000185_add_user_name.down.sql b/coderd/database/migrations/000185_add_user_name.down.sql new file mode 100644 index 0000000000000..1592aac27486d --- /dev/null +++ b/coderd/database/migrations/000185_add_user_name.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN name; diff --git a/coderd/database/migrations/000185_add_user_name.up.sql b/coderd/database/migrations/000185_add_user_name.up.sql new file mode 100644 index 0000000000000..01ca0ea374f3b --- /dev/null +++ b/coderd/database/migrations/000185_add_user_name.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE users ADD COLUMN name text NOT NULL DEFAULT ''; + +COMMENT ON COLUMN users.name IS 'Name of the Coder user'; + diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 81375e66c88c5..7443f1231a848 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -318,6 +318,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, &i.Count, ); err != nil { return nil, err diff --git a/coderd/database/models.go b/coderd/database/models.go index e8c8ae2c31e50..5308f88b35a79 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database @@ -2144,6 +2144,8 @@ type User struct { QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"` // "" can be interpreted as "the user does not care", falling back to the default theme ThemePreference string `db:"theme_preference" json:"theme_preference"` + // Name of the Coder user + Name string `db:"name" json:"name"` } type UserLink struct { diff --git a/coderd/database/pubsub/pubsub.go b/coderd/database/pubsub/pubsub.go index f661e885c2848..731466efd78e2 100644 --- a/coderd/database/pubsub/pubsub.go +++ b/coderd/database/pubsub/pubsub.go @@ -162,13 +162,15 @@ func (q *msgQueue) dropped() { // Pubsub implementation using PostgreSQL. type pgPubsub struct { - ctx context.Context - cancel context.CancelFunc - listenDone chan struct{} - pgListener *pq.Listener - db *sql.DB - mut sync.Mutex - queues map[string]map[uuid.UUID]*msgQueue + ctx context.Context + cancel context.CancelFunc + listenDone chan struct{} + pgListener *pq.Listener + db *sql.DB + mut sync.Mutex + queues map[string]map[uuid.UUID]*msgQueue + closedListener bool + closeListenerErr error } // BufferSize is the maximum number of unhandled messages we will buffer @@ -240,15 +242,29 @@ func (p *pgPubsub) Publish(event string, message []byte) error { // Close closes the pubsub instance. func (p *pgPubsub) Close() error { p.cancel() - err := p.pgListener.Close() + err := p.closeListener() <-p.listenDone return err } +// closeListener closes the pgListener, unless it has already been closed. +func (p *pgPubsub) closeListener() error { + p.mut.Lock() + defer p.mut.Unlock() + if p.closedListener { + return p.closeListenerErr + } + p.closeListenerErr = p.pgListener.Close() + p.closedListener = true + return p.closeListenerErr +} + // listen begins receiving messages on the pq listener. func (p *pgPubsub) listen() { - defer close(p.listenDone) - defer p.pgListener.Close() + defer func() { + _ = p.closeListener() + close(p.listenDone) + }() var ( notif *pq.Notification diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 3d2631c49f65f..8947ba185d14d 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database @@ -42,6 +42,7 @@ type sqlcQuerier interface { // Only unused template versions will be archived, which are any versions not // referenced by the latest build of a workspace. ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) + BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error CleanTailnetCoordinators(ctx context.Context) error CleanTailnetLostPeers(ctx context.Context) error CleanTailnetTunnels(ctx context.Context) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2a1f3b316c650..4c4bfc6012e7b 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.24.0 +// sqlc v1.25.0 package database @@ -1300,7 +1300,7 @@ func (q *sqlQuerier) DeleteGroupMembersByOrgAndUser(ctx context.Context, arg Del const getGroupMembers = `-- name: GetGroupMembers :many SELECT - users.id, users.email, users.username, users.hashed_password, users.created_at, users.updated_at, users.status, users.rbac_roles, users.login_type, users.avatar_url, users.deleted, users.last_seen_at, users.quiet_hours_schedule, users.theme_preference + users.id, users.email, users.username, users.hashed_password, users.created_at, users.updated_at, users.status, users.rbac_roles, users.login_type, users.avatar_url, users.deleted, users.last_seen_at, users.quiet_hours_schedule, users.theme_preference, users.name FROM users LEFT JOIN @@ -1348,6 +1348,7 @@ func (q *sqlQuerier) GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([] &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ); err != nil { return nil, err } @@ -6075,19 +6076,21 @@ SET name = $4, icon = $5, display_name = $6, - allow_user_cancel_workspace_jobs = $7 + allow_user_cancel_workspace_jobs = $7, + group_acl = $8 WHERE id = $1 ` type UpdateTemplateMetaByIDParams struct { - ID uuid.UUID `db:"id" json:"id"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Description string `db:"description" json:"description"` - Name string `db:"name" json:"name"` - Icon string `db:"icon" json:"icon"` - DisplayName string `db:"display_name" json:"display_name"` - AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` + ID uuid.UUID `db:"id" json:"id"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Description string `db:"description" json:"description"` + Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` + DisplayName string `db:"display_name" json:"display_name"` + AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` + GroupACL TemplateACL `db:"group_acl" json:"group_acl"` } func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error { @@ -6099,6 +6102,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl arg.Icon, arg.DisplayName, arg.AllowUserCancelWorkspaceJobs, + arg.GroupACL, ) return err } @@ -7330,7 +7334,7 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid. const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one SELECT - id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference + id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name FROM users WHERE @@ -7363,13 +7367,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } const getUserByID = `-- name: GetUserByID :one SELECT - id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference + id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name FROM users WHERE @@ -7396,6 +7401,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } @@ -7418,7 +7424,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) { const getUsers = `-- name: GetUsers :many SELECT - id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, COUNT(*) OVER() AS count + id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, COUNT(*) OVER() AS count FROM users WHERE @@ -7516,6 +7522,7 @@ type GetUsersRow struct { LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"` QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"` ThemePreference string `db:"theme_preference" json:"theme_preference"` + Name string `db:"name" json:"name"` Count int64 `db:"count" json:"count"` } @@ -7553,6 +7560,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, &i.Count, ); err != nil { return nil, err @@ -7569,7 +7577,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse } const getUsersByIDs = `-- name: GetUsersByIDs :many -SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference FROM users WHERE id = ANY($1 :: uuid [ ]) +SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name FROM users WHERE id = ANY($1 :: uuid [ ]) ` // This shouldn't check for deleted, because it's frequently used @@ -7599,6 +7607,7 @@ func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ); err != nil { return nil, err } @@ -7626,7 +7635,7 @@ INSERT INTO login_type ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name ` type InsertUserParams struct { @@ -7667,6 +7676,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } @@ -7725,7 +7735,7 @@ SET updated_at = $3 WHERE id = $1 -RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference +RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name ` type UpdateUserAppearanceSettingsParams struct { @@ -7752,6 +7762,7 @@ func (q *sqlQuerier) UpdateUserAppearanceSettings(ctx context.Context, arg Updat &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } @@ -7801,7 +7812,7 @@ SET last_seen_at = $2, updated_at = $3 WHERE - id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name ` type UpdateUserLastSeenAtParams struct { @@ -7828,6 +7839,7 @@ func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLas &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } @@ -7845,7 +7857,7 @@ SET '':: bytea END WHERE - id = $2 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference + id = $2 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name ` type UpdateUserLoginTypeParams struct { @@ -7871,6 +7883,7 @@ func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLogi &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } @@ -7882,10 +7895,11 @@ SET email = $2, username = $3, avatar_url = $4, - updated_at = $5 + updated_at = $5, + name = $6 WHERE id = $1 -RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference +RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name ` type UpdateUserProfileParams struct { @@ -7894,6 +7908,7 @@ type UpdateUserProfileParams struct { Username string `db:"username" json:"username"` AvatarURL string `db:"avatar_url" json:"avatar_url"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Name string `db:"name" json:"name"` } func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) { @@ -7903,6 +7918,7 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil arg.Username, arg.AvatarURL, arg.UpdatedAt, + arg.Name, ) var i User err := row.Scan( @@ -7920,6 +7936,7 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } @@ -7931,7 +7948,7 @@ SET quiet_hours_schedule = $2 WHERE id = $1 -RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference +RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name ` type UpdateUserQuietHoursScheduleParams struct { @@ -7957,6 +7974,7 @@ func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg Updat &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } @@ -7969,7 +7987,7 @@ SET rbac_roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[])) WHERE id = $2 -RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference +RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name ` type UpdateUserRolesParams struct { @@ -7995,6 +8013,7 @@ func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesPar &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } @@ -8006,7 +8025,7 @@ SET status = $2, updated_at = $3 WHERE - id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name ` type UpdateUserStatusParams struct { @@ -8033,6 +8052,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP &i.LastSeenAt, &i.QuietHoursSchedule, &i.ThemePreference, + &i.Name, ) return i, err } @@ -10810,6 +10830,25 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } +const batchUpdateWorkspaceLastUsedAt = `-- name: BatchUpdateWorkspaceLastUsedAt :exec +UPDATE + workspaces +SET + last_used_at = $1 +WHERE + id = ANY($2 :: uuid[]) +` + +type BatchUpdateWorkspaceLastUsedAtParams struct { + LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"` + IDs []uuid.UUID `db:"ids" json:"ids"` +} + +func (q *sqlQuerier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error { + _, err := q.db.ExecContext(ctx, batchUpdateWorkspaceLastUsedAt, arg.LastUsedAt, pq.Array(arg.IDs)) + return err +} + const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index af8c3fe80f420..ca031bb0bd839 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -115,7 +115,8 @@ SET name = $4, icon = $5, display_name = $6, - allow_user_cancel_workspace_jobs = $7 + allow_user_cancel_workspace_jobs = $7, + group_acl = $8 WHERE id = $1 ; diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 4708fd4f00344..80fe137142da0 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -78,7 +78,8 @@ SET email = $2, username = $3, avatar_url = $4, - updated_at = $5 + updated_at = $5, + name = $6 WHERE id = $1 RETURNING *; diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index d9ff657fd21dc..b400a1165b292 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -357,6 +357,14 @@ SET WHERE id = $1; +-- name: BatchUpdateWorkspaceLastUsedAt :exec +UPDATE + workspaces +SET + last_used_at = @last_used_at +WHERE + id = ANY(@ids :: uuid[]); + -- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index 074949fbafb16..49140d597ae9e 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -5,85 +5,6 @@ version: "2" cloud: # This is the static ID for the coder project. project: "01HEP08N3WKWRFZT3ZZ9Q37J8X" -# Ideally renames & overrides would go under the sql section, but there is a -# bug in sqlc that only global renames & overrides are currently being applied. -overrides: - go: - overrides: - - column: "provisioner_daemons.tags" - go_type: - type: "StringMap" - - column: "provisioner_jobs.tags" - go_type: - type: "StringMap" - - column: "users.rbac_roles" - go_type: "github.com/lib/pq.StringArray" - - column: "templates.user_acl" - go_type: - type: "TemplateACL" - - column: "templates.group_acl" - go_type: - type: "TemplateACL" - - column: "template_with_users.user_acl" - go_type: - type: "TemplateACL" - - column: "template_with_users.group_acl" - go_type: - type: "TemplateACL" - rename: - template: TemplateTable - template_with_user: Template - workspace_build: WorkspaceBuildTable - workspace_build_with_user: WorkspaceBuild - template_version: TemplateVersionTable - template_version_with_user: TemplateVersion - api_key: APIKey - api_key_scope: APIKeyScope - api_key_scope_all: APIKeyScopeAll - api_key_scope_application_connect: APIKeyScopeApplicationConnect - api_version: APIVersion - avatar_url: AvatarURL - created_by_avatar_url: CreatedByAvatarURL - dbcrypt_key: DBCryptKey - session_count_vscode: SessionCountVSCode - session_count_jetbrains: SessionCountJetBrains - session_count_reconnecting_pty: SessionCountReconnectingPTY - session_count_ssh: SessionCountSSH - connection_median_latency_ms: ConnectionMedianLatencyMS - login_type_oidc: LoginTypeOIDC - oauth_access_token: OAuthAccessToken - oauth_access_token_key_id: OAuthAccessTokenKeyID - oauth_expiry: OAuthExpiry - oauth_id_token: OAuthIDToken - oauth_refresh_token: OAuthRefreshToken - oauth_refresh_token_key_id: OAuthRefreshTokenKeyID - oauth_extra: OAuthExtra - parameter_type_system_hcl: ParameterTypeSystemHCL - userstatus: UserStatus - gitsshkey: GitSSHKey - rbac_roles: RBACRoles - ip_address: IPAddress - ip_addresses: IPAddresses - ids: IDs - jwt: JWT - user_acl: UserACL - group_acl: GroupACL - troubleshooting_url: TroubleshootingURL - default_ttl: DefaultTTL - max_ttl: MaxTTL - template_max_ttl: TemplateMaxTTL - motd_file: MOTDFile - uuid: UUID - failure_ttl: FailureTTL - time_til_dormant_autodelete: TimeTilDormantAutoDelete - eof: EOF - template_ids: TemplateIDs - active_user_ids: ActiveUserIDs - display_app_ssh_helper: DisplayAppSSHHelper - oauth2_provider_app: OAuth2ProviderApp - oauth2_provider_app_secret: OAuth2ProviderAppSecret - callback_url: CallbackURL - sql: - schema: "./dump.sql" queries: "./queries" @@ -105,3 +26,77 @@ sql: emit_db_tags: true emit_enum_valid_method: true emit_all_enum_values: true + overrides: + - column: "provisioner_daemons.tags" + go_type: + type: "StringMap" + - column: "provisioner_jobs.tags" + go_type: + type: "StringMap" + - column: "users.rbac_roles" + go_type: "github.com/lib/pq.StringArray" + - column: "templates.user_acl" + go_type: + type: "TemplateACL" + - column: "templates.group_acl" + go_type: + type: "TemplateACL" + - column: "template_with_users.user_acl" + go_type: + type: "TemplateACL" + - column: "template_with_users.group_acl" + go_type: + type: "TemplateACL" + rename: + template: TemplateTable + template_with_user: Template + workspace_build: WorkspaceBuildTable + workspace_build_with_user: WorkspaceBuild + template_version: TemplateVersionTable + template_version_with_user: TemplateVersion + api_key: APIKey + api_key_scope: APIKeyScope + api_key_scope_all: APIKeyScopeAll + api_key_scope_application_connect: APIKeyScopeApplicationConnect + api_version: APIVersion + avatar_url: AvatarURL + created_by_avatar_url: CreatedByAvatarURL + dbcrypt_key: DBCryptKey + session_count_vscode: SessionCountVSCode + session_count_jetbrains: SessionCountJetBrains + session_count_reconnecting_pty: SessionCountReconnectingPTY + session_count_ssh: SessionCountSSH + connection_median_latency_ms: ConnectionMedianLatencyMS + login_type_oidc: LoginTypeOIDC + oauth_access_token: OAuthAccessToken + oauth_access_token_key_id: OAuthAccessTokenKeyID + oauth_expiry: OAuthExpiry + oauth_id_token: OAuthIDToken + oauth_refresh_token: OAuthRefreshToken + oauth_refresh_token_key_id: OAuthRefreshTokenKeyID + oauth_extra: OAuthExtra + parameter_type_system_hcl: ParameterTypeSystemHCL + userstatus: UserStatus + gitsshkey: GitSSHKey + rbac_roles: RBACRoles + ip_address: IPAddress + ip_addresses: IPAddresses + ids: IDs + jwt: JWT + user_acl: UserACL + group_acl: GroupACL + troubleshooting_url: TroubleshootingURL + default_ttl: DefaultTTL + max_ttl: MaxTTL + template_max_ttl: TemplateMaxTTL + motd_file: MOTDFile + uuid: UUID + failure_ttl: FailureTTL + time_til_dormant_autodelete: TimeTilDormantAutoDelete + eof: EOF + template_ids: TemplateIDs + active_user_ids: ActiveUserIDs + display_app_ssh_helper: DisplayAppSSHHelper + oauth2_provider_app: OAuth2ProviderApp + oauth2_provider_app_secret: OAuth2ProviderAppSecret + callback_url: CallbackURL diff --git a/coderd/database/tx_test.go b/coderd/database/tx_test.go index ff7569ef562df..d97c1bc26d57f 100644 --- a/coderd/database/tx_test.go +++ b/coderd/database/tx_test.go @@ -4,9 +4,9 @@ import ( "database/sql" "testing" - "github.com/golang/mock/gomock" "github.com/lib/pq" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database" diff --git a/coderd/externalauth.go b/coderd/externalauth.go index b9d7e665b1637..001592e04e7db 100644 --- a/coderd/externalauth.go +++ b/coderd/externalauth.go @@ -362,7 +362,6 @@ func (api *API) listUserExternalAuths(rw http.ResponseWriter, r *http.Request) { if err == nil && valid { links[i] = newLink } - break } } } diff --git a/coderd/externalauth/externalauth.go b/coderd/externalauth/externalauth.go index 9243aa29e44e4..72d02b5139076 100644 --- a/coderd/externalauth/externalauth.go +++ b/coderd/externalauth/externalauth.go @@ -22,19 +22,14 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/promoauth" "github.com/coder/coder/v2/codersdk" "github.com/coder/retry" ) -type OAuth2Config interface { - AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string - Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) - TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource -} - // Config is used for authentication for Git operations. type Config struct { - OAuth2Config + promoauth.InstrumentedOAuth2Config // ID is a unique identifier for the authenticator. ID string // Type is the type of provider. @@ -192,12 +187,8 @@ func (c *Config) ValidateToken(ctx context.Context, token string) (bool, *coders return false, nil, err } - cli := http.DefaultClient - if v, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok { - cli = v - } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - res, err := cli.Do(req) + res, err := c.InstrumentedOAuth2Config.Do(ctx, promoauth.SourceValidateToken, req) if err != nil { return false, nil, err } @@ -247,7 +238,7 @@ func (c *Config) AppInstallations(ctx context.Context, token string) ([]codersdk return nil, false, err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - res, err := http.DefaultClient.Do(req) + res, err := c.InstrumentedOAuth2Config.Do(ctx, promoauth.SourceAppInstallations, req) if err != nil { return nil, false, err } @@ -287,6 +278,8 @@ func (c *Config) AppInstallations(ctx context.Context, token string) ([]codersdk } type DeviceAuth struct { + // Config is provided for the http client method. + Config promoauth.InstrumentedOAuth2Config ClientID string TokenURL string Scopes []string @@ -308,7 +301,16 @@ func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.ExternalAut return nil, err } req.Header.Set("Accept", "application/json") - resp, err := http.DefaultClient.Do(req) + + do := http.DefaultClient.Do + if c.Config != nil { + // The cfg can be nil in unit tests. + do = func(req *http.Request) (*http.Response, error) { + return c.Config.Do(ctx, promoauth.SourceAuthorizeDevice, req) + } + } + + resp, err := do(req) if err != nil { return nil, err } @@ -319,7 +321,14 @@ func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.ExternalAut } err = json.NewDecoder(resp.Body).Decode(&r) if err != nil { - return nil, err + // Some status codes do not return json payloads, and we should + // return a better error. + switch resp.StatusCode { + case http.StatusTooManyRequests: + return nil, xerrors.New("rate limit hit, unable to authorize device. please try again later") + default: + return nil, xerrors.Errorf("status_code=%d: %w", resp.StatusCode, err) + } } if r.ErrorDescription != "" { return nil, xerrors.New(r.ErrorDescription) @@ -401,7 +410,7 @@ func (c *DeviceAuth) formatDeviceCodeURL() (string, error) { // ConvertConfig converts the SDK configuration entry format // to the parsed and ready-to-consume in coderd provider type. -func ConvertConfig(entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([]*Config, error) { +func ConvertConfig(instrument *promoauth.Factory, entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([]*Config, error) { ids := map[string]struct{}{} configs := []*Config{} for _, entry := range entries { @@ -453,7 +462,7 @@ func ConvertConfig(entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([ Scopes: entry.Scopes, } - var oauthConfig OAuth2Config = oc + var oauthConfig promoauth.OAuth2Config = oc // Azure DevOps uses JWT token authentication! if entry.Type == string(codersdk.EnhancedExternalAuthProviderAzureDevops) { oauthConfig = &jwtConfig{oc} @@ -462,18 +471,23 @@ func ConvertConfig(entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([ oauthConfig = &exchangeWithClientSecret{oc} } + instrumented := instrument.New(entry.ID, oauthConfig) + if strings.EqualFold(entry.Type, string(codersdk.EnhancedExternalAuthProviderGitHub)) { + instrumented = instrument.NewGithub(entry.ID, oauthConfig) + } + cfg := &Config{ - OAuth2Config: oauthConfig, - ID: entry.ID, - Regex: regex, - Type: entry.Type, - NoRefresh: entry.NoRefresh, - ValidateURL: entry.ValidateURL, - AppInstallationsURL: entry.AppInstallationsURL, - AppInstallURL: entry.AppInstallURL, - DisplayName: entry.DisplayName, - DisplayIcon: entry.DisplayIcon, - ExtraTokenKeys: entry.ExtraTokenKeys, + InstrumentedOAuth2Config: instrumented, + ID: entry.ID, + Regex: regex, + Type: entry.Type, + NoRefresh: entry.NoRefresh, + ValidateURL: entry.ValidateURL, + AppInstallationsURL: entry.AppInstallationsURL, + AppInstallURL: entry.AppInstallURL, + DisplayName: entry.DisplayName, + DisplayIcon: entry.DisplayIcon, + ExtraTokenKeys: entry.ExtraTokenKeys, } if entry.DeviceFlow { @@ -481,6 +495,7 @@ func ConvertConfig(entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([ return nil, xerrors.Errorf("external auth provider %q: device auth url must be provided", entry.ID) } cfg.DeviceAuth = &DeviceAuth{ + Config: cfg, ClientID: entry.ClientID, TokenURL: oc.Endpoint.TokenURL, Scopes: entry.Scopes, @@ -516,6 +531,9 @@ func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) { case codersdk.EnhancedExternalAuthProviderBitBucketServer: copyDefaultSettings(config, bitbucketServerDefaults(config)) return + case codersdk.EnhancedExternalAuthProviderJFrog: + copyDefaultSettings(config, jfrogArtifactoryDefaults(config)) + return default: // No defaults for this type. We still want to run this apply with // an empty set of defaults. @@ -608,6 +626,44 @@ func bitbucketServerDefaults(config *codersdk.ExternalAuthConfig) codersdk.Exter return defaults } +func jfrogArtifactoryDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthConfig { + defaults := codersdk.ExternalAuthConfig{ + DisplayName: "JFrog Artifactory", + Scopes: []string{"applied-permissions/user"}, + DisplayIcon: "/icon/jfrog.svg", + } + // Artifactory servers will have some base url, e.g. https://jfrog.coder.com. + // We will grab this from the Auth URL. This choice is not arbitrary. It is a + // static string for all integrations on the same artifactory. + if config.AuthURL == "" { + // No auth url, means we cannot guess the urls. + return defaults + } + + auth, err := url.Parse(config.AuthURL) + if err != nil { + // We need a valid URL to continue with. + return defaults + } + + if config.ClientID == "" { + return defaults + } + + tokenURL := auth.ResolveReference(&url.URL{Path: fmt.Sprintf("/access/api/v1/integrations/%s/token", config.ClientID)}) + defaults.TokenURL = tokenURL.String() + + // validate needs to return a 200 when logged in and a 401 when unauthenticated. + validate := auth.ResolveReference(&url.URL{Path: "/access/api/v1/system/ping"}) + defaults.ValidateURL = validate.String() + + // Some options omitted: + // - Regex: Artifactory can span pretty much all domains (git, docker, etc). + // I do not think we can intelligently guess this as a default. + + return defaults +} + var staticDefaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.ExternalAuthConfig{ codersdk.EnhancedExternalAuthProviderAzureDevops: { AuthURL: "https://app.vssps.visualstudio.com/oauth2/authorize", diff --git a/coderd/externalauth/externalauth_test.go b/coderd/externalauth/externalauth_test.go index 387bdc77382aa..84fbe4ff5de35 100644 --- a/coderd/externalauth/externalauth_test.go +++ b/coderd/externalauth/externalauth_test.go @@ -12,6 +12,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" "golang.org/x/oauth2" "golang.org/x/xerrors" @@ -22,6 +23,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbmem" "github.com/coder/coder/v2/coderd/externalauth" + "github.com/coder/coder/v2/coderd/promoauth" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" ) @@ -94,7 +96,7 @@ func TestRefreshToken(t *testing.T) { t.Run("FalseIfTokenSourceFails", func(t *testing.T) { t.Parallel() config := &externalauth.Config{ - OAuth2Config: &testutil.OAuth2Config{ + InstrumentedOAuth2Config: &testutil.OAuth2Config{ TokenSourceFunc: func() (*oauth2.Token, error) { return nil, xerrors.New("failure") }, @@ -301,9 +303,10 @@ func TestRefreshToken(t *testing.T) { func TestExchangeWithClientSecret(t *testing.T) { t.Parallel() + instrument := promoauth.NewFactory(prometheus.NewRegistry()) // This ensures a provider that requires the custom // client secret exchange works. - configs, err := externalauth.ConvertConfig([]codersdk.ExternalAuthConfig{{ + configs, err := externalauth.ConvertConfig(instrument, []codersdk.ExternalAuthConfig{{ // JFrog just happens to require this custom type. Type: codersdk.EnhancedExternalAuthProviderJFrog.String(), @@ -335,6 +338,8 @@ func TestExchangeWithClientSecret(t *testing.T) { func TestConvertYAML(t *testing.T) { t.Parallel() + + instrument := promoauth.NewFactory(prometheus.NewRegistry()) for _, tc := range []struct { Name string Input []codersdk.ExternalAuthConfig @@ -387,7 +392,7 @@ func TestConvertYAML(t *testing.T) { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - output, err := externalauth.ConvertConfig(tc.Input, &url.URL{}) + output, err := externalauth.ConvertConfig(instrument, tc.Input, &url.URL{}) if tc.Error != "" { require.Error(t, err) require.Contains(t, err.Error(), tc.Error) @@ -399,7 +404,7 @@ func TestConvertYAML(t *testing.T) { t.Run("CustomScopesAndEndpoint", func(t *testing.T) { t.Parallel() - config, err := externalauth.ConvertConfig([]codersdk.ExternalAuthConfig{{ + config, err := externalauth.ConvertConfig(instrument, []codersdk.ExternalAuthConfig{{ Type: string(codersdk.EnhancedExternalAuthProviderGitLab), ClientID: "id", ClientSecret: "secret", @@ -433,10 +438,12 @@ func setupOauth2Test(t *testing.T, settings testConfig) (*oidctest.FakeIDP, *ext append([]oidctest.FakeIDPOpt{}, settings.FakeIDPOpts...)..., ) + f := promoauth.NewFactory(prometheus.NewRegistry()) config := &externalauth.Config{ - OAuth2Config: fake.OIDCConfig(t, nil, settings.CoderOIDCConfigOpts...), - ID: providerID, - ValidateURL: fake.WellknownConfig().UserInfoURL, + InstrumentedOAuth2Config: f.New("test-oauth2", + fake.OIDCConfig(t, nil, settings.CoderOIDCConfigOpts...)), + ID: providerID, + ValidateURL: fake.WellknownConfig().UserInfoURL, } settings.ExternalAuthOpt(config) diff --git a/coderd/externalauth_test.go b/coderd/externalauth_test.go index 34c1fe7bcdc1e..17adfac69dcd7 100644 --- a/coderd/externalauth_test.go +++ b/coderd/externalauth_test.go @@ -18,6 +18,8 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest/oidctest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/httpapi" @@ -126,7 +128,7 @@ func TestExternalAuthByID(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ ExternalAuthConfigs: []*externalauth.Config{ fake.ExternalAuthConfig(t, providerID, routes, func(cfg *externalauth.Config) { - cfg.AppInstallationsURL = cfg.ValidateURL + "/installs" + cfg.AppInstallationsURL = strings.TrimSuffix(cfg.ValidateURL, "/") + "/installs" cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String() }), }, @@ -198,6 +200,66 @@ func TestExternalAuthManagement(t *testing.T) { require.Len(t, list.Providers, 2) require.Len(t, list.Links, 0) }) + t.Run("RefreshAllProviders", func(t *testing.T) { + t.Parallel() + const githubID = "fake-github" + const gitlabID = "fake-gitlab" + + githubCalled := false + githubApp := oidctest.NewFakeIDP(t, oidctest.WithServing(), oidctest.WithRefresh(func(email string) error { + githubCalled = true + return nil + })) + gitlabCalled := false + gitlab := oidctest.NewFakeIDP(t, oidctest.WithServing(), oidctest.WithRefresh(func(email string) error { + gitlabCalled = true + return nil + })) + + owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ + ExternalAuthConfigs: []*externalauth.Config{ + githubApp.ExternalAuthConfig(t, githubID, nil, func(cfg *externalauth.Config) { + cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String() + }), + gitlab.ExternalAuthConfig(t, gitlabID, nil, func(cfg *externalauth.Config) { + cfg.Type = codersdk.EnhancedExternalAuthProviderGitLab.String() + }), + }, + }) + ownerUser := coderdtest.CreateFirstUser(t, owner) + // Just a regular user + client, user := coderdtest.CreateAnotherUser(t, owner, ownerUser.OrganizationID) + ctx := testutil.Context(t, testutil.WaitLong) + + // Log into github & gitlab + githubApp.ExternalLogin(t, client) + gitlab.ExternalLogin(t, client) + + links, err := db.GetExternalAuthLinksByUserID( + dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, ownerUser.OrganizationID)), user.ID) + require.NoError(t, err) + require.Len(t, links, 2) + + // Expire the links + for _, l := range links { + _, err := db.UpdateExternalAuthLink(dbauthz.As(ctx, coderdtest.AuthzUserSubject(user, ownerUser.OrganizationID)), database.UpdateExternalAuthLinkParams{ + ProviderID: l.ProviderID, + UserID: l.UserID, + UpdatedAt: dbtime.Now(), + OAuthAccessToken: l.OAuthAccessToken, + OAuthRefreshToken: l.OAuthRefreshToken, + OAuthExpiry: time.Now().Add(time.Hour * -1), + OAuthExtra: l.OAuthExtra, + }) + require.NoErrorf(t, err, "expire key for %s", l.ProviderID) + } + + list, err := client.ListExternalAuths(ctx) + require.NoError(t, err) + require.Len(t, list.Links, 2) + require.True(t, githubCalled, "github should be refreshed") + require.True(t, gitlabCalled, "gitlab should be refreshed") + }) } func TestExternalAuthDevice(t *testing.T) { @@ -279,6 +341,28 @@ func TestExternalAuthDevice(t *testing.T) { require.NoError(t, err) require.True(t, auth.Authenticated) }) + t.Run("TooManyRequests", func(t *testing.T) { + t.Parallel() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTooManyRequests) + // Github returns an html payload for this error. + _, _ = w.Write([]byte(`Please wait a few minutes before you try again`)) + })) + defer srv.Close() + client := coderdtest.New(t, &coderdtest.Options{ + ExternalAuthConfigs: []*externalauth.Config{{ + ID: "test", + DeviceAuth: &externalauth.DeviceAuth{ + ClientID: "test", + CodeURL: srv.URL, + Scopes: []string{"repo"}, + }, + }}, + }) + coderdtest.CreateFirstUser(t, client) + _, err := client.ExternalAuthDeviceByID(context.Background(), "test") + require.ErrorContains(t, err, "rate limit hit") + }) } // nolint:bodyclose @@ -316,10 +400,10 @@ func TestExternalAuthCallback(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, ExternalAuthConfigs: []*externalauth.Config{{ - OAuth2Config: &testutil.OAuth2Config{}, - ID: "github", - Regex: regexp.MustCompile(`github\.com`), - Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, + ID: "github", + Regex: regexp.MustCompile(`github\.com`), + Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), }}, }) user := coderdtest.CreateFirstUser(t, client) @@ -347,10 +431,10 @@ func TestExternalAuthCallback(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, ExternalAuthConfigs: []*externalauth.Config{{ - OAuth2Config: &testutil.OAuth2Config{}, - ID: "github", - Regex: regexp.MustCompile(`github\.com`), - Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, + ID: "github", + Regex: regexp.MustCompile(`github\.com`), + Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), }}, }) resp := coderdtest.RequestExternalAuthCallback(t, "github", client) @@ -361,10 +445,10 @@ func TestExternalAuthCallback(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, ExternalAuthConfigs: []*externalauth.Config{{ - OAuth2Config: &testutil.OAuth2Config{}, - ID: "github", - Regex: regexp.MustCompile(`github\.com`), - Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, + ID: "github", + Regex: regexp.MustCompile(`github\.com`), + Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), }}, }) _ = coderdtest.CreateFirstUser(t, client) @@ -387,11 +471,11 @@ func TestExternalAuthCallback(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, ExternalAuthConfigs: []*externalauth.Config{{ - ValidateURL: srv.URL, - OAuth2Config: &testutil.OAuth2Config{}, - ID: "github", - Regex: regexp.MustCompile(`github\.com`), - Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), + ValidateURL: srv.URL, + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, + ID: "github", + Regex: regexp.MustCompile(`github\.com`), + Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), }}, }) user := coderdtest.CreateFirstUser(t, client) @@ -443,7 +527,7 @@ func TestExternalAuthCallback(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, ExternalAuthConfigs: []*externalauth.Config{{ - OAuth2Config: &testutil.OAuth2Config{ + InstrumentedOAuth2Config: &testutil.OAuth2Config{ Token: &oauth2.Token{ AccessToken: "token", RefreshToken: "something", @@ -497,10 +581,10 @@ func TestExternalAuthCallback(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, ExternalAuthConfigs: []*externalauth.Config{{ - OAuth2Config: &testutil.OAuth2Config{}, - ID: "github", - Regex: regexp.MustCompile(`github\.com`), - Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, + ID: "github", + Regex: regexp.MustCompile(`github\.com`), + Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), }}, }) user := coderdtest.CreateFirstUser(t, client) diff --git a/coderd/healthcheck/database_test.go b/coderd/healthcheck/database_test.go index f3f032356a413..041970206a8b7 100644 --- a/coderd/healthcheck/database_test.go +++ b/coderd/healthcheck/database_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database/dbmock" diff --git a/coderd/healthcheck/health/model.go b/coderd/healthcheck/health/model.go index 707969e404886..9eae390aa0b08 100644 --- a/coderd/healthcheck/health/model.go +++ b/coderd/healthcheck/health/model.go @@ -34,6 +34,10 @@ const ( CodeDERPNodeUsesWebsocket Code = `EDERP01` CodeDERPOneNodeUnhealthy Code = `EDERP02` + + CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01` + CodeProvisionerDaemonVersionMismatch Code = `EPD02` + CodeProvisionerDaemonAPIMajorVersionDeprecated Code = `EPD03` ) // @typescript-generate Severity diff --git a/coderd/healthcheck/healthcheck.go b/coderd/healthcheck/healthcheck.go index 7c634201234bc..1d1890ba23cbb 100644 --- a/coderd/healthcheck/healthcheck.go +++ b/coderd/healthcheck/healthcheck.go @@ -18,6 +18,7 @@ type Checker interface { Websocket(ctx context.Context, opts *WebsocketReportOptions) WebsocketReport Database(ctx context.Context, opts *DatabaseReportOptions) DatabaseReport WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyReportOptions) WorkspaceProxyReport + ProvisionerDaemons(ctx context.Context, opts *ProvisionerDaemonsReportDeps) ProvisionerDaemonsReport } // @typescript-generate Report @@ -32,49 +33,62 @@ type Report struct { // FailingSections is a list of sections that have failed their healthcheck. FailingSections []codersdk.HealthSection `json:"failing_sections"` - DERP derphealth.Report `json:"derp"` - AccessURL AccessURLReport `json:"access_url"` - Websocket WebsocketReport `json:"websocket"` - Database DatabaseReport `json:"database"` - WorkspaceProxy WorkspaceProxyReport `json:"workspace_proxy"` + DERP derphealth.Report `json:"derp"` + AccessURL AccessURLReport `json:"access_url"` + Websocket WebsocketReport `json:"websocket"` + Database DatabaseReport `json:"database"` + WorkspaceProxy WorkspaceProxyReport `json:"workspace_proxy"` + ProvisionerDaemons ProvisionerDaemonsReport `json:"provisioner_daemons"` // The Coder version of the server that the report was generated on. CoderVersion string `json:"coder_version"` } type ReportOptions struct { - AccessURL AccessURLReportOptions - Database DatabaseReportOptions - DerpHealth derphealth.ReportOptions - Websocket WebsocketReportOptions - WorkspaceProxy WorkspaceProxyReportOptions + AccessURL AccessURLReportOptions + Database DatabaseReportOptions + DerpHealth derphealth.ReportOptions + Websocket WebsocketReportOptions + WorkspaceProxy WorkspaceProxyReportOptions + ProvisionerDaemons ProvisionerDaemonsReportDeps Checker Checker } type defaultChecker struct{} -func (defaultChecker) DERP(ctx context.Context, opts *derphealth.ReportOptions) (report derphealth.Report) { +func (defaultChecker) DERP(ctx context.Context, opts *derphealth.ReportOptions) derphealth.Report { + var report derphealth.Report report.Run(ctx, opts) return report } -func (defaultChecker) AccessURL(ctx context.Context, opts *AccessURLReportOptions) (report AccessURLReport) { +func (defaultChecker) AccessURL(ctx context.Context, opts *AccessURLReportOptions) AccessURLReport { + var report AccessURLReport report.Run(ctx, opts) return report } -func (defaultChecker) Websocket(ctx context.Context, opts *WebsocketReportOptions) (report WebsocketReport) { +func (defaultChecker) Websocket(ctx context.Context, opts *WebsocketReportOptions) WebsocketReport { + var report WebsocketReport report.Run(ctx, opts) return report } -func (defaultChecker) Database(ctx context.Context, opts *DatabaseReportOptions) (report DatabaseReport) { +func (defaultChecker) Database(ctx context.Context, opts *DatabaseReportOptions) DatabaseReport { + var report DatabaseReport report.Run(ctx, opts) return report } -func (defaultChecker) WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyReportOptions) (report WorkspaceProxyReport) { +func (defaultChecker) WorkspaceProxy(ctx context.Context, opts *WorkspaceProxyReportOptions) WorkspaceProxyReport { + var report WorkspaceProxyReport + report.Run(ctx, opts) + return report +} + +func (defaultChecker) ProvisionerDaemons(ctx context.Context, opts *ProvisionerDaemonsReportDeps) ProvisionerDaemonsReport { + var report ProvisionerDaemonsReport report.Run(ctx, opts) return report } @@ -149,26 +163,41 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { report.WorkspaceProxy = opts.Checker.WorkspaceProxy(ctx, &opts.WorkspaceProxy) }() + wg.Add(1) + go func() { + defer wg.Done() + defer func() { + if err := recover(); err != nil { + report.ProvisionerDaemons.Error = health.Errorf(health.CodeUnknown, "provisioner daemon report panic: %s", err) + } + }() + + report.ProvisionerDaemons = opts.Checker.ProvisionerDaemons(ctx, &opts.ProvisionerDaemons) + }() + report.CoderVersion = buildinfo.Version() wg.Wait() report.Time = time.Now() report.FailingSections = []codersdk.HealthSection{} - if !report.DERP.Healthy { + if report.DERP.Severity.Value() > health.SeverityWarning.Value() { report.FailingSections = append(report.FailingSections, codersdk.HealthSectionDERP) } - if !report.AccessURL.Healthy { + if report.AccessURL.Severity.Value() > health.SeverityOK.Value() { report.FailingSections = append(report.FailingSections, codersdk.HealthSectionAccessURL) } - if !report.Websocket.Healthy { + if report.Websocket.Severity.Value() > health.SeverityWarning.Value() { report.FailingSections = append(report.FailingSections, codersdk.HealthSectionWebsocket) } - if !report.Database.Healthy { + if report.Database.Severity.Value() > health.SeverityWarning.Value() { report.FailingSections = append(report.FailingSections, codersdk.HealthSectionDatabase) } - if !report.WorkspaceProxy.Healthy { + if report.WorkspaceProxy.Severity.Value() > health.SeverityWarning.Value() { report.FailingSections = append(report.FailingSections, codersdk.HealthSectionWorkspaceProxy) } + if report.ProvisionerDaemons.Severity.Value() > health.SeverityWarning.Value() { + report.FailingSections = append(report.FailingSections, codersdk.HealthSectionProvisionerDaemons) + } report.Healthy = len(report.FailingSections) == 0 @@ -190,6 +219,9 @@ func Run(ctx context.Context, opts *ReportOptions) *Report { if report.WorkspaceProxy.Severity.Value() > report.Severity.Value() { report.Severity = report.WorkspaceProxy.Severity } + if report.ProvisionerDaemons.Severity.Value() > report.Severity.Value() { + report.Severity = report.ProvisionerDaemons.Severity + } return &report } diff --git a/coderd/healthcheck/healthcheck_test.go b/coderd/healthcheck/healthcheck_test.go index e8089f36eb3ea..1dc155623a2df 100644 --- a/coderd/healthcheck/healthcheck_test.go +++ b/coderd/healthcheck/healthcheck_test.go @@ -13,11 +13,12 @@ import ( ) type testChecker struct { - DERPReport derphealth.Report - AccessURLReport healthcheck.AccessURLReport - WebsocketReport healthcheck.WebsocketReport - DatabaseReport healthcheck.DatabaseReport - WorkspaceProxyReport healthcheck.WorkspaceProxyReport + DERPReport derphealth.Report + AccessURLReport healthcheck.AccessURLReport + WebsocketReport healthcheck.WebsocketReport + DatabaseReport healthcheck.DatabaseReport + WorkspaceProxyReport healthcheck.WorkspaceProxyReport + ProvisionerDaemonsReport healthcheck.ProvisionerDaemonsReport } func (c *testChecker) DERP(context.Context, *derphealth.ReportOptions) derphealth.Report { @@ -40,6 +41,10 @@ func (c *testChecker) WorkspaceProxy(context.Context, *healthcheck.WorkspaceProx return c.WorkspaceProxyReport } +func (c *testChecker) ProvisionerDaemons(context.Context, *healthcheck.ProvisionerDaemonsReportDeps) healthcheck.ProvisionerDaemonsReport { + return c.ProvisionerDaemonsReport +} + func TestHealthcheck(t *testing.T) { t.Parallel() @@ -72,6 +77,9 @@ func TestHealthcheck(t *testing.T) { Healthy: true, Severity: health.SeverityOK, }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityOK, + }, }, healthy: true, severity: health.SeverityOK, @@ -99,6 +107,9 @@ func TestHealthcheck(t *testing.T) { Healthy: true, Severity: health.SeverityOK, }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityOK, + }, }, healthy: false, severity: health.SeverityError, @@ -127,6 +138,9 @@ func TestHealthcheck(t *testing.T) { Healthy: true, Severity: health.SeverityOK, }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityOK, + }, }, healthy: true, severity: health.SeverityWarning, @@ -154,6 +168,9 @@ func TestHealthcheck(t *testing.T) { Healthy: true, Severity: health.SeverityOK, }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityOK, + }, }, healthy: false, severity: health.SeverityWarning, @@ -181,6 +198,9 @@ func TestHealthcheck(t *testing.T) { Healthy: true, Severity: health.SeverityOK, }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityOK, + }, }, healthy: false, severity: health.SeverityError, @@ -208,6 +228,9 @@ func TestHealthcheck(t *testing.T) { Healthy: true, Severity: health.SeverityOK, }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityOK, + }, }, healthy: false, severity: health.SeverityError, @@ -235,6 +258,9 @@ func TestHealthcheck(t *testing.T) { Healthy: false, Severity: health.SeverityError, }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityOK, + }, }, severity: health.SeverityError, healthy: false, @@ -263,6 +289,70 @@ func TestHealthcheck(t *testing.T) { Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, Severity: health.SeverityWarning, }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityOK, + }, + }, + severity: health.SeverityWarning, + healthy: true, + failingSections: []codersdk.HealthSection{}, + }, { + name: "ProvisionerDaemonsFail", + checker: &testChecker{ + DERPReport: derphealth.Report{ + Healthy: true, + Severity: health.SeverityOK, + }, + AccessURLReport: healthcheck.AccessURLReport{ + Healthy: true, + Severity: health.SeverityOK, + }, + WebsocketReport: healthcheck.WebsocketReport{ + Healthy: true, + Severity: health.SeverityOK, + }, + DatabaseReport: healthcheck.DatabaseReport{ + Healthy: true, + Severity: health.SeverityOK, + }, + WorkspaceProxyReport: healthcheck.WorkspaceProxyReport{ + Healthy: true, + Severity: health.SeverityOK, + }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityError, + }, + }, + severity: health.SeverityError, + healthy: false, + failingSections: []codersdk.HealthSection{codersdk.HealthSectionProvisionerDaemons}, + }, { + name: "ProvisionerDaemonsWarn", + checker: &testChecker{ + DERPReport: derphealth.Report{ + Healthy: true, + Severity: health.SeverityOK, + }, + AccessURLReport: healthcheck.AccessURLReport{ + Healthy: true, + Severity: health.SeverityOK, + }, + WebsocketReport: healthcheck.WebsocketReport{ + Healthy: true, + Severity: health.SeverityOK, + }, + DatabaseReport: healthcheck.DatabaseReport{ + Healthy: true, + Severity: health.SeverityOK, + }, + WorkspaceProxyReport: healthcheck.WorkspaceProxyReport{ + Healthy: true, + Severity: health.SeverityOK, + }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityWarning, + Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}}, + }, }, severity: health.SeverityWarning, healthy: true, @@ -291,6 +381,9 @@ func TestHealthcheck(t *testing.T) { Healthy: false, Severity: health.SeverityError, }, + ProvisionerDaemonsReport: healthcheck.ProvisionerDaemonsReport{ + Severity: health.SeverityError, + }, }, severity: health.SeverityError, failingSections: []codersdk.HealthSection{ @@ -299,6 +392,7 @@ func TestHealthcheck(t *testing.T) { codersdk.HealthSectionWebsocket, codersdk.HealthSectionDatabase, codersdk.HealthSectionWorkspaceProxy, + codersdk.HealthSectionProvisionerDaemons, }, }} { c := c diff --git a/coderd/healthcheck/provisioner.go b/coderd/healthcheck/provisioner.go new file mode 100644 index 0000000000000..4ff961454b73a --- /dev/null +++ b/coderd/healthcheck/provisioner.go @@ -0,0 +1,158 @@ +package healthcheck + +import ( + "context" + "sort" + "time" + + "golang.org/x/mod/semver" + + "github.com/coder/coder/v2/buildinfo" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/healthcheck/health" + "github.com/coder/coder/v2/coderd/provisionerdserver" + "github.com/coder/coder/v2/coderd/util/apiversion" + "github.com/coder/coder/v2/coderd/util/ptr" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/provisionersdk" +) + +// @typescript-generate ProvisionerDaemonsReport +type ProvisionerDaemonsReport struct { + Severity health.Severity `json:"severity"` + Warnings []health.Message `json:"warnings"` + Dismissed bool `json:"dismissed"` + Error *string `json:"error"` + + Items []ProvisionerDaemonsReportItem `json:"items"` +} + +// @typescript-generate ProvisionerDaemonsReportItem +type ProvisionerDaemonsReportItem struct { + codersdk.ProvisionerDaemon `json:"provisioner_daemon"` + Warnings []health.Message `json:"warnings"` +} + +type ProvisionerDaemonsReportDeps struct { + // Required + CurrentVersion string + CurrentAPIMajorVersion int + Store ProvisionerDaemonsStore + + // Optional + TimeNow func() time.Time // Defaults to dbtime.Now + StaleInterval time.Duration // Defaults to 3 heartbeats + + Dismissed bool +} + +type ProvisionerDaemonsStore interface { + GetProvisionerDaemons(ctx context.Context) ([]database.ProvisionerDaemon, error) +} + +func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDaemonsReportDeps) { + r.Items = make([]ProvisionerDaemonsReportItem, 0) + r.Severity = health.SeverityOK + r.Warnings = make([]health.Message, 0) + r.Dismissed = opts.Dismissed + + if opts.TimeNow == nil { + opts.TimeNow = dbtime.Now + } + now := opts.TimeNow() + + if opts.StaleInterval == 0 { + opts.StaleInterval = provisionerdserver.DefaultHeartbeatInterval * 3 + } + + if opts.CurrentVersion == "" { + r.Severity = health.SeverityError + r.Error = ptr.Ref("Developer error: CurrentVersion is empty!") + return + } + + if opts.CurrentAPIMajorVersion == 0 { + r.Severity = health.SeverityError + r.Error = ptr.Ref("Developer error: CurrentAPIMajorVersion must be non-zero!") + return + } + + if opts.Store == nil { + r.Severity = health.SeverityError + r.Error = ptr.Ref("Developer error: Store is nil!") + return + } + + // nolint: gocritic // need an actor to fetch provisioner daemons + daemons, err := opts.Store.GetProvisionerDaemons(dbauthz.AsSystemRestricted(ctx)) + if err != nil { + r.Severity = health.SeverityError + r.Error = ptr.Ref("error fetching provisioner daemons: " + err.Error()) + return + } + + // Ensure stable order for display and for tests + sort.Slice(daemons, func(i, j int) bool { + return daemons[i].Name < daemons[j].Name + }) + + for _, daemon := range daemons { + // Daemon never connected, skip. + if !daemon.LastSeenAt.Valid { + continue + } + // Daemon has gone away, skip. + if now.Sub(daemon.LastSeenAt.Time) > (opts.StaleInterval) { + continue + } + + it := ProvisionerDaemonsReportItem{ + ProvisionerDaemon: db2sdk.ProvisionerDaemon(daemon), + Warnings: make([]health.Message, 0), + } + + // For release versions, just check MAJOR.MINOR and ignore patch. + if !semver.IsValid(daemon.Version) { + if r.Severity.Value() < health.SeverityError.Value() { + r.Severity = health.SeverityError + } + r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Some provisioner daemons report invalid version information.")) + it.Warnings = append(it.Warnings, health.Messagef(health.CodeUnknown, "Invalid version %q", daemon.Version)) + } else if !buildinfo.VersionsMatch(opts.CurrentVersion, daemon.Version) { + if r.Severity.Value() < health.SeverityWarning.Value() { + r.Severity = health.SeverityWarning + } + r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonVersionMismatch, "Some provisioner daemons report mismatched versions.")) + it.Warnings = append(it.Warnings, health.Messagef(health.CodeProvisionerDaemonVersionMismatch, "Mismatched version %q", daemon.Version)) + } + + // Provisioner daemon API version follows different rules; we just want to check the major API version and + // warn about potential later deprecations. + // When we check API versions of connecting provisioner daemons, all active provisioner daemons + // will, by necessity, have a compatible API version. + if maj, _, err := apiversion.Parse(daemon.APIVersion); err != nil { + if r.Severity.Value() < health.SeverityError.Value() { + r.Severity = health.SeverityError + } + r.Warnings = append(r.Warnings, health.Messagef(health.CodeUnknown, "Some provisioner daemons report invalid API version information.")) + it.Warnings = append(it.Warnings, health.Messagef(health.CodeUnknown, "Invalid API version: %s", err.Error())) // contains version string + } else if maj != opts.CurrentAPIMajorVersion { + if r.Severity.Value() < health.SeverityWarning.Value() { + r.Severity = health.SeverityWarning + } + r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonAPIMajorVersionDeprecated, "Some provisioner daemons report deprecated major API versions. Consider upgrading!")) + it.Warnings = append(it.Warnings, health.Messagef(health.CodeProvisionerDaemonAPIMajorVersionDeprecated, "Deprecated major API version %d.", provisionersdk.CurrentMajor)) + } + + r.Items = append(r.Items, it) + } + + if len(r.Items) == 0 { + r.Severity = health.SeverityError + r.Warnings = append(r.Warnings, health.Messagef(health.CodeProvisionerDaemonsNoProvisionerDaemons, "No active provisioner daemons found!")) + return + } +} diff --git a/coderd/healthcheck/provisioner_test.go b/coderd/healthcheck/provisioner_test.go new file mode 100644 index 0000000000000..aba95f1f678da --- /dev/null +++ b/coderd/healthcheck/provisioner_test.go @@ -0,0 +1,377 @@ +package healthcheck_test + +import ( + "context" + "database/sql" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/healthcheck" + "github.com/coder/coder/v2/coderd/healthcheck/health" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/provisionersdk" + + gomock "go.uber.org/mock/gomock" +) + +func TestProvisionerDaemonReport(t *testing.T) { + t.Parallel() + + now := dbtime.Now() + + for _, tt := range []struct { + name string + currentVersion string + currentAPIMajorVersion int + provisionerDaemons []database.ProvisionerDaemon + provisionerDaemonsErr error + expectedSeverity health.Severity + expectedWarningCode health.Code + expectedError string + expectedItems []healthcheck.ProvisionerDaemonsReportItem + }{ + { + name: "current version empty", + currentVersion: "", + expectedSeverity: health.SeverityError, + expectedError: "Developer error: CurrentVersion is empty", + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{}, + }, + { + name: "no daemons", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityError, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{}, + expectedWarningCode: health.CodeProvisionerDaemonsNoProvisionerDaemons, + }, + { + name: "error fetching daemons", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + provisionerDaemonsErr: assert.AnError, + expectedSeverity: health.SeverityError, + expectedError: assert.AnError.Error(), + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{}, + }, + { + name: "one daemon up to date", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityOK, + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0", now)}, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{ + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-ok", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "v1.2.3", + APIVersion: "1.0", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{}, + }, + }, + }, + { + name: "one daemon out of date", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0", now)}, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{ + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-old", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "v1.1.2", + APIVersion: "1.0", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{ + { + Code: health.CodeProvisionerDaemonVersionMismatch, + Message: `Mismatched version "v1.1.2"`, + }, + }, + }, + }, + }, + { + name: "invalid daemon version", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeUnknown, + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-invalid-version", "invalid", "1.0", now)}, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{ + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-invalid-version", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "invalid", + APIVersion: "1.0", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{ + { + Code: health.CodeUnknown, + Message: `Invalid version "invalid"`, + }, + }, + }, + }, + }, + { + name: "invalid daemon api version", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeUnknown, + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-invalid-api", "v1.2.3", "invalid", now)}, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{ + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-invalid-api", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "v1.2.3", + APIVersion: "invalid", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{ + { + Code: health.CodeUnknown, + Message: `Invalid API version: invalid version string: invalid`, + }, + }, + }, + }, + }, + { + name: "api version backward compat", + currentVersion: "v2.3.4", + currentAPIMajorVersion: 2, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonAPIMajorVersionDeprecated, + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old-api", "v2.3.4", "1.0", now)}, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{ + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-old-api", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "v2.3.4", + APIVersion: "1.0", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{ + { + Code: health.CodeProvisionerDaemonAPIMajorVersionDeprecated, + Message: "Deprecated major API version 1.", + }, + }, + }, + }, + }, + { + name: "one up to date, one out of date", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0", now), fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0", now)}, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{ + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-ok", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "v1.2.3", + APIVersion: "1.0", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{}, + }, + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-old", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "v1.1.2", + APIVersion: "1.0", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{ + { + Code: health.CodeProvisionerDaemonVersionMismatch, + Message: `Mismatched version "v1.1.2"`, + }, + }, + }, + }, + }, + { + name: "one up to date, one newer", + currentVersion: "v1.2.3", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityWarning, + expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch, + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0", now), fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "1.0", now)}, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{ + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-new", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "v2.3.4", + APIVersion: "1.0", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{ + { + Code: health.CodeProvisionerDaemonVersionMismatch, + Message: `Mismatched version "v2.3.4"`, + }, + }, + }, + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-ok", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "v1.2.3", + APIVersion: "1.0", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{}, + }, + }, + }, + { + name: "one up to date, one stale older", + currentVersion: "v2.3.4", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityOK, + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemonStale(t, "pd-stale", "v1.2.3", "0.9", now.Add(-5*time.Minute), now), fakeProvisionerDaemon(t, "pd-ok", "v2.3.4", "1.0", now)}, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{ + { + ProvisionerDaemon: codersdk.ProvisionerDaemon{ + ID: uuid.Nil, + Name: "pd-ok", + CreatedAt: now, + LastSeenAt: codersdk.NewNullTime(now, true), + Version: "v2.3.4", + APIVersion: "1.0", + Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho, codersdk.ProvisionerTypeTerraform}, + Tags: map[string]string{}, + }, + Warnings: []health.Message{}, + }, + }, + }, + { + name: "one stale", + currentVersion: "v2.3.4", + currentAPIMajorVersion: provisionersdk.CurrentMajor, + expectedSeverity: health.SeverityError, + expectedWarningCode: health.CodeProvisionerDaemonsNoProvisionerDaemons, + provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemonStale(t, "pd-ok", "v1.2.3", "0.9", now.Add(-5*time.Minute), now)}, + expectedItems: []healthcheck.ProvisionerDaemonsReportItem{}, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var rpt healthcheck.ProvisionerDaemonsReport + var deps healthcheck.ProvisionerDaemonsReportDeps + deps.CurrentVersion = tt.currentVersion + deps.CurrentAPIMajorVersion = tt.currentAPIMajorVersion + if tt.currentAPIMajorVersion == 0 { + deps.CurrentAPIMajorVersion = provisionersdk.CurrentMajor + } + deps.TimeNow = func() time.Time { + return now + } + + ctrl := gomock.NewController(t) + mDB := dbmock.NewMockStore(ctrl) + mDB.EXPECT().GetProvisionerDaemons(gomock.Any()).AnyTimes().Return(tt.provisionerDaemons, tt.provisionerDaemonsErr) + deps.Store = mDB + + rpt.Run(context.Background(), &deps) + + assert.Equal(t, tt.expectedSeverity, rpt.Severity) + if tt.expectedWarningCode != "" && assert.NotEmpty(t, rpt.Warnings) { + var found bool + for _, w := range rpt.Warnings { + if w.Code == tt.expectedWarningCode { + found = true + break + } + } + assert.True(t, found, "expected warning %s not found in %v", tt.expectedWarningCode, rpt.Warnings) + } else { + assert.Empty(t, rpt.Warnings) + } + if tt.expectedError != "" && assert.NotNil(t, rpt.Error) { + assert.Contains(t, *rpt.Error, tt.expectedError) + } + if tt.expectedItems != nil { + assert.Equal(t, tt.expectedItems, rpt.Items) + } + }) + } +} + +func fakeProvisionerDaemon(t *testing.T, name, version, apiVersion string, now time.Time) database.ProvisionerDaemon { + t.Helper() + return database.ProvisionerDaemon{ + ID: uuid.Nil, + Name: name, + CreatedAt: now, + LastSeenAt: sql.NullTime{Time: now, Valid: true}, + Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform}, + ReplicaID: uuid.NullUUID{}, + Tags: map[string]string{}, + Version: version, + APIVersion: apiVersion, + } +} + +func fakeProvisionerDaemonStale(t *testing.T, name, version, apiVersion string, lastSeenAt, now time.Time) database.ProvisionerDaemon { + t.Helper() + d := fakeProvisionerDaemon(t, name, version, apiVersion, now) + d.LastSeenAt.Valid = true + d.LastSeenAt.Time = lastSeenAt + return d +} diff --git a/coderd/healthcheck/workspaceproxy.go b/coderd/healthcheck/workspaceproxy.go index 1bca7452fd9bf..509ac3318b67f 100644 --- a/coderd/healthcheck/workspaceproxy.go +++ b/coderd/healthcheck/workspaceproxy.go @@ -76,7 +76,11 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo return } - r.WorkspaceProxies = proxies + for _, proxy := range proxies.Regions { + if !proxy.Deleted { + r.WorkspaceProxies.Regions = append(r.WorkspaceProxies.Regions, proxy) + } + } if r.WorkspaceProxies.Regions == nil { r.WorkspaceProxies.Regions = make([]codersdk.WorkspaceProxy, 0) } diff --git a/coderd/healthcheck/workspaceproxy_test.go b/coderd/healthcheck/workspaceproxy_test.go index 704426836688c..fd4c127cfb2fd 100644 --- a/coderd/healthcheck/workspaceproxy_test.go +++ b/coderd/healthcheck/workspaceproxy_test.go @@ -164,6 +164,15 @@ func TestWorkspaceProxies(t *testing.T) { expectedSeverity: health.SeverityWarning, expectedWarningCode: health.CodeProxyUpdate, }, + { + name: "Enabled/OneUnhealthyAndDeleted", + fetchWorkspaceProxies: fakeFetchWorkspaceProxies(fakeWorkspaceProxy("alpha", false, currentVersion, func(wp *codersdk.WorkspaceProxy) { + wp.Deleted = true + })), + updateProxyHealth: fakeUpdateProxyHealth(nil), + expectedHealthy: true, + expectedSeverity: health.SeverityOK, + }, } { tt := tt t.Run(tt.name, func(t *testing.T) { @@ -236,7 +245,7 @@ func (u *fakeWorkspaceProxyFetchUpdater) Update(ctx context.Context) error { } //nolint:revive // yes, this is a control flag, and that is OK in a unit test. -func fakeWorkspaceProxy(name string, healthy bool, version string) codersdk.WorkspaceProxy { +func fakeWorkspaceProxy(name string, healthy bool, version string, mutators ...func(*codersdk.WorkspaceProxy)) codersdk.WorkspaceProxy { var status codersdk.WorkspaceProxyStatus if !healthy { status = codersdk.WorkspaceProxyStatus{ @@ -246,7 +255,7 @@ func fakeWorkspaceProxy(name string, healthy bool, version string) codersdk.Work }, } } - return codersdk.WorkspaceProxy{ + wsp := codersdk.WorkspaceProxy{ Region: codersdk.Region{ Name: name, Healthy: healthy, @@ -254,6 +263,10 @@ func fakeWorkspaceProxy(name string, healthy bool, version string) codersdk.Work Version: version, Status: status, } + for _, f := range mutators { + f(&wsp) + } + return wsp } func fakeFetchWorkspaceProxies(ps ...codersdk.WorkspaceProxy) func(context.Context) (codersdk.RegionsResponse[codersdk.WorkspaceProxy], error) { diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index e6c63451a0df9..fb5e4361ec32c 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -80,6 +80,20 @@ func init() { if err != nil { panic(err) } + + userRealNameValidator := func(fl validator.FieldLevel) bool { + f := fl.Field().Interface() + str, ok := f.(string) + if !ok { + return false + } + valid := UserRealNameValid(str) + return valid == nil + } + err = Validate.RegisterValidation("user_real_name", userRealNameValidator) + if err != nil { + panic(err) + } } // Is404Error returns true if the given error should return a 404 status code. diff --git a/coderd/httpapi/name.go b/coderd/httpapi/name.go index bea9c17a8b6f3..0083927c85a08 100644 --- a/coderd/httpapi/name.go +++ b/coderd/httpapi/name.go @@ -79,3 +79,15 @@ func TemplateDisplayNameValid(str string) error { } return nil } + +// UserRealNameValid returns whether the input string is a valid real user name. +func UserRealNameValid(str string) error { + if len(str) > 128 { + return xerrors.New("must be <= 128 characters") + } + + if strings.TrimSpace(str) != str { + return xerrors.New("must not have leading or trailing whitespace") + } + return nil +} diff --git a/coderd/httpapi/name_test.go b/coderd/httpapi/name_test.go index e28115eecbbd7..a6313c54034f5 100644 --- a/coderd/httpapi/name_test.go +++ b/coderd/httpapi/name_test.go @@ -209,3 +209,37 @@ func TestFrom(t *testing.T) { }) } } + +func TestUserRealNameValid(t *testing.T) { + t.Parallel() + + testCases := []struct { + Name string + Valid bool + }{ + {"1", true}, + {"A", true}, + {"A1", true}, + {".", true}, + {"Mr Bean", true}, + {"Severus Snape", true}, + {"Prof. Albus Percival Wulfric Brian Dumbledore", true}, + {"Pablo Diego José Francisco de Paula Juan Nepomuceno María de los Remedios Cipriano de la Santísima Trinidad Ruiz y Picasso", true}, + {"Hector Ó hEochagáin", true}, + {"Małgorzata Kalinowska-Iszkowska", true}, + {"成龍", true}, + {". .", true}, + + {"Lord Voldemort ", false}, + {" Bellatrix Lestrange", false}, + {" ", false}, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + valid := httpapi.UserRealNameValid(testCase.Name) + require.Equal(t, testCase.Valid, valid == nil) + }) + } +} diff --git a/coderd/httpapi/websocket.go b/coderd/httpapi/websocket.go index 60904396099a1..629dcac8131f3 100644 --- a/coderd/httpapi/websocket.go +++ b/coderd/httpapi/websocket.go @@ -5,6 +5,8 @@ import ( "time" "nhooyr.io/websocket" + + "cdr.dev/slog" ) // Heartbeat loops to ping a WebSocket to keep it alive. @@ -26,10 +28,10 @@ func Heartbeat(ctx context.Context, conn *websocket.Conn) { } } -// Heartbeat loops to ping a WebSocket to keep it alive. It kills the connection -// on ping failure. -func HeartbeatClose(ctx context.Context, exit func(), conn *websocket.Conn) { - ticker := time.NewTicker(30 * time.Second) +// Heartbeat loops to ping a WebSocket to keep it alive. It calls `exit` on ping +// failure. +func HeartbeatClose(ctx context.Context, logger slog.Logger, exit func(), conn *websocket.Conn) { + ticker := time.NewTicker(15 * time.Second) defer ticker.Stop() for { @@ -41,6 +43,7 @@ func HeartbeatClose(ctx context.Context, exit func(), conn *websocket.Conn) { err := conn.Ping(ctx) if err != nil { _ = conn.Close(websocket.StatusGoingAway, "Ping failed") + logger.Info(ctx, "failed to heartbeat ping", slog.Error(err)) exit() return } diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index dfffe9cf092df..46d8c97014bc3 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -22,6 +22,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/promoauth" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" ) @@ -74,8 +75,8 @@ func UserAuthorization(r *http.Request) Authorization { // OAuth2Configs is a collection of configurations for OAuth-based authentication. // This should be extended to support other authentication types in the future. type OAuth2Configs struct { - Github OAuth2Config - OIDC OAuth2Config + Github promoauth.OAuth2Config + OIDC promoauth.OAuth2Config } func (c *OAuth2Configs) IsZero() bool { @@ -270,7 +271,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon }) } - var oauthConfig OAuth2Config + var oauthConfig promoauth.OAuth2Config switch key.LoginType { case database.LoginTypeGithub: oauthConfig = cfg.OAuth2Configs.Github diff --git a/coderd/httpmw/cors.go b/coderd/httpmw/cors.go index b00810fbf9322..dd69c714379a4 100644 --- a/coderd/httpmw/cors.go +++ b/coderd/httpmw/cors.go @@ -7,7 +7,7 @@ import ( "github.com/go-chi/cors" - "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" ) const ( @@ -44,18 +44,18 @@ func Cors(allowAll bool, origins ...string) func(next http.Handler) http.Handler }) } -func WorkspaceAppCors(regex *regexp.Regexp, app httpapi.ApplicationURL) func(next http.Handler) http.Handler { +func WorkspaceAppCors(regex *regexp.Regexp, app appurl.ApplicationURL) func(next http.Handler) http.Handler { return cors.Handler(cors.Options{ AllowOriginFunc: func(r *http.Request, rawOrigin string) bool { origin, err := url.Parse(rawOrigin) if rawOrigin == "" || origin.Host == "" || err != nil { return false } - subdomain, ok := httpapi.ExecuteHostnamePattern(regex, origin.Host) + subdomain, ok := appurl.ExecuteHostnamePattern(regex, origin.Host) if !ok { return false } - originApp, err := httpapi.ParseSubdomainAppURL(subdomain) + originApp, err := appurl.ParseSubdomainAppURL(subdomain) if err != nil { return false } diff --git a/coderd/httpmw/cors_test.go b/coderd/httpmw/cors_test.go index ae63073b237ed..57111799ff292 100644 --- a/coderd/httpmw/cors_test.go +++ b/coderd/httpmw/cors_test.go @@ -7,14 +7,14 @@ import ( "github.com/stretchr/testify/require" - "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" ) func TestWorkspaceAppCors(t *testing.T) { t.Parallel() - regex, err := httpapi.CompileHostnamePattern("*--apps.dev.coder.com") + regex, err := appurl.CompileHostnamePattern("*--apps.dev.coder.com") require.NoError(t, err) methods := []string{ @@ -30,13 +30,13 @@ func TestWorkspaceAppCors(t *testing.T) { tests := []struct { name string origin string - app httpapi.ApplicationURL + app appurl.ApplicationURL allowed bool }{ { name: "Self", origin: "https://3000--agent--ws--user--apps.dev.coder.com", - app: httpapi.ApplicationURL{ + app: appurl.ApplicationURL{ AppSlugOrPort: "3000", AgentName: "agent", WorkspaceName: "ws", @@ -47,7 +47,7 @@ func TestWorkspaceAppCors(t *testing.T) { { name: "SameWorkspace", origin: "https://8000--agent--ws--user--apps.dev.coder.com", - app: httpapi.ApplicationURL{ + app: appurl.ApplicationURL{ AppSlugOrPort: "3000", AgentName: "agent", WorkspaceName: "ws", @@ -58,7 +58,7 @@ func TestWorkspaceAppCors(t *testing.T) { { name: "SameUser", origin: "https://8000--agent2--ws2--user--apps.dev.coder.com", - app: httpapi.ApplicationURL{ + app: appurl.ApplicationURL{ AppSlugOrPort: "3000", AgentName: "agent", WorkspaceName: "ws", @@ -69,7 +69,7 @@ func TestWorkspaceAppCors(t *testing.T) { { name: "DifferentOriginOwner", origin: "https://3000--agent--ws--user2--apps.dev.coder.com", - app: httpapi.ApplicationURL{ + app: appurl.ApplicationURL{ AppSlugOrPort: "3000", AgentName: "agent", WorkspaceName: "ws", @@ -80,7 +80,7 @@ func TestWorkspaceAppCors(t *testing.T) { { name: "DifferentHostOwner", origin: "https://3000--agent--ws--user--apps.dev.coder.com", - app: httpapi.ApplicationURL{ + app: appurl.ApplicationURL{ AppSlugOrPort: "3000", AgentName: "agent", WorkspaceName: "ws", diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go index 7888365741873..529cac3a727d7 100644 --- a/coderd/httpmw/csrf.go +++ b/coderd/httpmw/csrf.go @@ -3,6 +3,7 @@ package httpmw import ( "net/http" "regexp" + "strings" "github.com/justinas/nosurf" "golang.org/x/xerrors" @@ -12,6 +13,8 @@ import ( // CSRF is a middleware that verifies that a CSRF token is present in the request // for non-GET requests. +// If enforce is false, then CSRF enforcement is disabled. We still want +// to include the CSRF middleware because it will set the CSRF cookie. func CSRF(secureCookie bool) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { mw := nosurf.New(next) @@ -19,10 +22,16 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler { mw.SetFailureHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Something is wrong with your CSRF token. Please refresh the page. If this error persists, try clearing your cookies.", http.StatusBadRequest) })) + + mw.ExemptRegexp(regexp.MustCompile("/api/v2/users/first")) + // Exempt all requests that do not require CSRF protection. // All GET requests are exempt by default. mw.ExemptPath("/api/v2/csp/reports") + // This should not be required? + mw.ExemptRegexp(regexp.MustCompile("/api/v2/users/first")) + // Agent authenticated routes mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/me/*")) mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/*")) @@ -36,6 +45,11 @@ func CSRF(secureCookie bool) func(next http.Handler) http.Handler { mw.ExemptRegexp(regexp.MustCompile("/organizations/[^/]+/provisionerdaemons/*")) mw.ExemptFunc(func(r *http.Request) bool { + // Only enforce CSRF on API routes. + if !strings.HasPrefix(r.URL.Path, "/api") { + return true + } + // CSRF only affects requests that automatically attach credentials via a cookie. // If no cookie is present, then there is no risk of CSRF. //nolint:govet diff --git a/coderd/httpmw/csrf_test.go b/coderd/httpmw/csrf_test.go new file mode 100644 index 0000000000000..12c6afe825f75 --- /dev/null +++ b/coderd/httpmw/csrf_test.go @@ -0,0 +1,71 @@ +package httpmw_test + +import ( + "context" + "net/http" + "testing" + + "github.com/justinas/nosurf" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/codersdk" +) + +func TestCSRFExemptList(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + URL string + Exempt bool + }{ + { + Name: "Root", + URL: "https://example.com", + Exempt: true, + }, + { + Name: "WorkspacePage", + URL: "https://coder.com/workspaces", + Exempt: true, + }, + { + Name: "SubApp", + URL: "https://app--dev--coder--user--apps.coder.com/", + Exempt: true, + }, + { + Name: "PathApp", + URL: "https://coder.com/@USER/test.instance/apps/app", + Exempt: true, + }, + { + Name: "API", + URL: "https://coder.com/api/v2", + Exempt: false, + }, + { + Name: "APIMe", + URL: "https://coder.com/api/v2/me", + Exempt: false, + }, + } + + mw := httpmw.CSRF(false) + csrfmw := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})).(*nosurf.CSRFHandler) + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + r, err := http.NewRequestWithContext(context.Background(), http.MethodPost, c.URL, nil) + require.NoError(t, err) + + r.AddCookie(&http.Cookie{Name: codersdk.SessionTokenCookie, Value: "test"}) + exempt := csrfmw.IsExempt(r) + require.Equal(t, c.Exempt, exempt) + }) + } +} diff --git a/coderd/httpmw/oauth2.go b/coderd/httpmw/oauth2.go index c300576aa82c2..dbb763bc9de3e 100644 --- a/coderd/httpmw/oauth2.go +++ b/coderd/httpmw/oauth2.go @@ -10,6 +10,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/promoauth" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/cryptorand" ) @@ -22,14 +23,6 @@ type OAuth2State struct { StateString string } -// OAuth2Config exposes a subset of *oauth2.Config functions for easier testing. -// *oauth2.Config should be used instead of implementing this in production. -type OAuth2Config interface { - AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string - Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) - TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource -} - // OAuth2 returns the state from an oauth request. func OAuth2(r *http.Request) OAuth2State { oauth, ok := r.Context().Value(oauth2StateKey{}).(OAuth2State) @@ -44,7 +37,7 @@ func OAuth2(r *http.Request) OAuth2State { // a "code" URL parameter will be redirected. // AuthURLOpts are passed to the AuthCodeURL function. If this is nil, // the default option oauth2.AccessTypeOffline will be used. -func ExtractOAuth2(config OAuth2Config, client *http.Client, authURLOpts map[string]string) func(http.Handler) http.Handler { +func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOpts map[string]string) func(http.Handler) http.Handler { opts := make([]oauth2.AuthCodeOption, 0, len(authURLOpts)+1) opts = append(opts, oauth2.AccessTypeOffline) for k, v := range authURLOpts { diff --git a/coderd/oauthpki/oidcpki.go b/coderd/oauthpki/oidcpki.go index c44d130e5be9f..d761c43e446ff 100644 --- a/coderd/oauthpki/oidcpki.go +++ b/coderd/oauthpki/oidcpki.go @@ -20,7 +20,7 @@ import ( "golang.org/x/oauth2/jws" "golang.org/x/xerrors" - "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/promoauth" ) // Config uses jwt assertions over client_secret for oauth2 authentication of @@ -33,7 +33,7 @@ import ( // // https://datatracker.ietf.org/doc/html/rfc7523 type Config struct { - cfg httpmw.OAuth2Config + cfg promoauth.OAuth2Config // These values should match those provided in the oauth2.Config. // Because the inner config is an interface, we need to duplicate these @@ -57,7 +57,7 @@ type ConfigParams struct { PemEncodedKey []byte PemEncodedCert []byte - Config httpmw.OAuth2Config + Config promoauth.OAuth2Config } // NewOauth2PKIConfig creates the oauth2 config for PKI based auth. It requires the certificate and it's private key. @@ -180,6 +180,8 @@ func (src *jwtTokenSource) Token() (*oauth2.Token, error) { } cli := http.DefaultClient if v, ok := src.ctx.Value(oauth2.HTTPClient).(*http.Client); ok { + // This client should be the instrumented client already. So no need to + // handle this manually. cli = v } diff --git a/coderd/prometheusmetrics/aggregator.go b/coderd/prometheusmetrics/aggregator.go index 9eb3f08072376..aac06d63ef744 100644 --- a/coderd/prometheusmetrics/aggregator.go +++ b/coderd/prometheusmetrics/aggregator.go @@ -5,7 +5,6 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" - "golang.org/x/exp/slices" "golang.org/x/xerrors" "cdr.dev/slog" @@ -68,7 +67,12 @@ type annotatedMetric struct { var _ prometheus.Collector = new(MetricsAggregator) func (am *annotatedMetric) is(req updateRequest, m *agentproto.Stats_Metric) bool { - return am.username == req.username && am.workspaceName == req.workspaceName && am.agentName == req.agentName && am.Name == m.Name && slices.Equal(am.Labels, m.Labels) + return am.username == req.username && + am.workspaceName == req.workspaceName && + am.agentName == req.agentName && + am.templateName == req.templateName && + am.Name == m.Name && + agentproto.LabelsEqual(am.Labels, m.Labels) } func (am *annotatedMetric) asPrometheus() (prometheus.Metric, error) { diff --git a/coderd/prometheusmetrics/aggregator_internal_test.go b/coderd/prometheusmetrics/aggregator_internal_test.go new file mode 100644 index 0000000000000..8830e1b1afc30 --- /dev/null +++ b/coderd/prometheusmetrics/aggregator_internal_test.go @@ -0,0 +1,210 @@ +package prometheusmetrics + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/agent/proto" +) + +func TestAnnotatedMetric_Is(t *testing.T) { + t.Parallel() + am1 := &annotatedMetric{ + Stats_Metric: &proto.Stats_Metric{ + Name: "met", + Type: proto.Stats_Metric_COUNTER, + Value: 1, + Labels: []*proto.Stats_Metric_Label{ + {Name: "rarity", Value: "blue moon"}, + {Name: "certainty", Value: "yes"}, + }, + }, + username: "spike", + workspaceName: "work", + agentName: "janus", + templateName: "tempe", + expiryDate: time.Now(), + } + for _, tc := range []struct { + name string + req updateRequest + m *proto.Stats_Metric + is bool + }{ + { + name: "OK", + req: updateRequest{ + username: "spike", + workspaceName: "work", + agentName: "janus", + templateName: "tempe", + metrics: nil, + timestamp: time.Now().Add(-5 * time.Second), + }, + m: &proto.Stats_Metric{ + Name: "met", + Type: proto.Stats_Metric_COUNTER, + Value: 2, + Labels: []*proto.Stats_Metric_Label{ + {Name: "rarity", Value: "blue moon"}, + {Name: "certainty", Value: "yes"}, + }, + }, + is: true, + }, + { + name: "missingLabel", + req: updateRequest{ + username: "spike", + workspaceName: "work", + agentName: "janus", + templateName: "tempe", + metrics: nil, + timestamp: time.Now().Add(-5 * time.Second), + }, + m: &proto.Stats_Metric{ + Name: "met", + Type: proto.Stats_Metric_COUNTER, + Value: 2, + Labels: []*proto.Stats_Metric_Label{ + {Name: "certainty", Value: "yes"}, + }, + }, + is: false, + }, + { + name: "wrongLabelValue", + req: updateRequest{ + username: "spike", + workspaceName: "work", + agentName: "janus", + templateName: "tempe", + metrics: nil, + timestamp: time.Now().Add(-5 * time.Second), + }, + m: &proto.Stats_Metric{ + Name: "met", + Type: proto.Stats_Metric_COUNTER, + Value: 2, + Labels: []*proto.Stats_Metric_Label{ + {Name: "rarity", Value: "blue moon"}, + {Name: "certainty", Value: "inshallah"}, + }, + }, + is: false, + }, + { + name: "wrongMetricName", + req: updateRequest{ + username: "spike", + workspaceName: "work", + agentName: "janus", + templateName: "tempe", + metrics: nil, + timestamp: time.Now().Add(-5 * time.Second), + }, + m: &proto.Stats_Metric{ + Name: "cub", + Type: proto.Stats_Metric_COUNTER, + Value: 2, + Labels: []*proto.Stats_Metric_Label{ + {Name: "rarity", Value: "blue moon"}, + {Name: "certainty", Value: "yes"}, + }, + }, + is: false, + }, + { + name: "wrongUsername", + req: updateRequest{ + username: "steve", + workspaceName: "work", + agentName: "janus", + templateName: "tempe", + metrics: nil, + timestamp: time.Now().Add(-5 * time.Second), + }, + m: &proto.Stats_Metric{ + Name: "met", + Type: proto.Stats_Metric_COUNTER, + Value: 2, + Labels: []*proto.Stats_Metric_Label{ + {Name: "rarity", Value: "blue moon"}, + {Name: "certainty", Value: "yes"}, + }, + }, + is: false, + }, + { + name: "wrongWorkspaceName", + req: updateRequest{ + username: "spike", + workspaceName: "play", + agentName: "janus", + templateName: "tempe", + metrics: nil, + timestamp: time.Now().Add(-5 * time.Second), + }, + m: &proto.Stats_Metric{ + Name: "met", + Type: proto.Stats_Metric_COUNTER, + Value: 2, + Labels: []*proto.Stats_Metric_Label{ + {Name: "rarity", Value: "blue moon"}, + {Name: "certainty", Value: "yes"}, + }, + }, + is: false, + }, + { + name: "wrongAgentName", + req: updateRequest{ + username: "spike", + workspaceName: "work", + agentName: "bond", + templateName: "tempe", + metrics: nil, + timestamp: time.Now().Add(-5 * time.Second), + }, + m: &proto.Stats_Metric{ + Name: "met", + Type: proto.Stats_Metric_COUNTER, + Value: 2, + Labels: []*proto.Stats_Metric_Label{ + {Name: "rarity", Value: "blue moon"}, + {Name: "certainty", Value: "yes"}, + }, + }, + is: false, + }, + { + name: "wrongTemplateName", + req: updateRequest{ + username: "spike", + workspaceName: "work", + agentName: "janus", + templateName: "phoenix", + metrics: nil, + timestamp: time.Now().Add(-5 * time.Second), + }, + m: &proto.Stats_Metric{ + Name: "met", + Type: proto.Stats_Metric_COUNTER, + Value: 2, + Labels: []*proto.Stats_Metric_Label{ + {Name: "rarity", Value: "blue moon"}, + {Name: "certainty", Value: "yes"}, + }, + }, + is: false, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.is, am1.is(tc.req, tc.m)) + }) + } +} diff --git a/coderd/prometheusmetrics/aggregator_test.go b/coderd/prometheusmetrics/aggregator_test.go index 5f34f47962629..00d088f8b13b4 100644 --- a/coderd/prometheusmetrics/aggregator_test.go +++ b/coderd/prometheusmetrics/aggregator_test.go @@ -51,11 +51,19 @@ func TestUpdateMetrics_MetricsDoNotExpire(t *testing.T) { given1 := []*agentproto.Stats_Metric{ {Name: "a_counter_one", Type: agentproto.Stats_Metric_COUNTER, Value: 1}, {Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: 2}, + // Tests that we update labels correctly when they have extra labels + {Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: 27, Labels: []*agentproto.Stats_Metric_Label{ + {Name: "lizz", Value: "rizz"}, + }}, {Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 3}, } given2 := []*agentproto.Stats_Metric{ {Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: 4}, + // Tests that we update labels correctly when they have extra labels + {Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: -9, Labels: []*agentproto.Stats_Metric_Label{ + {Name: "lizz", Value: "rizz"}, + }}, {Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 5}, {Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 2, Labels: []*agentproto.Stats_Metric_Label{ {Name: "foobar", Value: "Foobaz"}, @@ -73,6 +81,13 @@ func TestUpdateMetrics_MetricsDoNotExpire(t *testing.T) { expected := []*agentproto.Stats_Metric{ {Name: "a_counter_one", Type: agentproto.Stats_Metric_COUNTER, Value: 1, Labels: commonLabels}, {Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: 4, Labels: commonLabels}, + {Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: -9, Labels: []*agentproto.Stats_Metric_Label{ + {Name: "agent_name", Value: testAgentName}, + {Name: "lizz", Value: "rizz"}, + {Name: "username", Value: testUsername}, + {Name: "workspace_name", Value: testWorkspaceName}, + {Name: "template_name", Value: testTemplateName}, + }}, {Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 5, Labels: commonLabels}, {Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 2, Labels: []*agentproto.Stats_Metric_Label{ {Name: "agent_name", Value: testAgentName}, @@ -111,6 +126,7 @@ func TestUpdateMetrics_MetricsDoNotExpire(t *testing.T) { func verifyCollectedMetrics(t *testing.T, expected []*agentproto.Stats_Metric, actual []prometheus.Metric) bool { if len(expected) != len(actual) { + t.Logf("expected %d metrics, got %d", len(expected), len(actual)) return false } diff --git a/coderd/promoauth/doc.go b/coderd/promoauth/doc.go new file mode 100644 index 0000000000000..72f30b48cff7a --- /dev/null +++ b/coderd/promoauth/doc.go @@ -0,0 +1,4 @@ +// Package promoauth is for instrumenting oauth2 flows with prometheus metrics. +// Specifically, it is intended to count the number of external requests made +// by the underlying oauth2 exchanges. +package promoauth diff --git a/coderd/promoauth/github.go b/coderd/promoauth/github.go new file mode 100644 index 0000000000000..3f2a97d241b7f --- /dev/null +++ b/coderd/promoauth/github.go @@ -0,0 +1,101 @@ +package promoauth + +import ( + "net/http" + "strconv" + "time" + + "golang.org/x/xerrors" +) + +type rateLimits struct { + Limit int + Remaining int + Used int + Reset time.Time + Resource string +} + +// githubRateLimits checks the returned response headers and +func githubRateLimits(resp *http.Response, err error) (rateLimits, bool) { + if err != nil || resp == nil { + return rateLimits{}, false + } + + p := headerParser{header: resp.Header} + // See + // https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#checking-the-status-of-your-rate-limit + limits := rateLimits{ + Limit: p.int("x-ratelimit-limit"), + Remaining: p.int("x-ratelimit-remaining"), + Used: p.int("x-ratelimit-used"), + Resource: p.string("x-ratelimit-resource"), + } + + if limits.Limit == 0 && + limits.Remaining == 0 && + limits.Used == 0 { + // For some requests, github has no rate limit. In which case, + // it returns all 0s. We can just omit these. + return limits, false + } + + // Reset is when the rate limit "used" will be reset to 0. + // If it's unix 0, then we do not know when it will reset. + // Change it to a zero time as that is easier to handle in golang. + unix := p.int("x-ratelimit-reset") + resetAt := time.Unix(int64(unix), 0) + if unix == 0 { + resetAt = time.Time{} + } + limits.Reset = resetAt + + // Unauthorized requests have their own rate limit, so we should + // track them separately. + if resp.StatusCode == http.StatusUnauthorized { + limits.Resource += "-unauthorized" + } + + // A 401 or 429 means too many requests. This might mess up the + // "resource" string because we could hit the unauthorized limit, + // and we do not want that to override the authorized one. + // However, in testing, it seems a 401 is always a 401, even if + // the limit is hit. + + if len(p.errors) > 0 { + // If we are missing any headers, then do not try and guess + // what the rate limits are. + return limits, false + } + return limits, true +} + +type headerParser struct { + errors map[string]error + header http.Header +} + +func (p *headerParser) string(key string) string { + if p.errors == nil { + p.errors = make(map[string]error) + } + + v := p.header.Get(key) + if v == "" { + p.errors[key] = xerrors.Errorf("missing header %q", key) + } + return v +} + +func (p *headerParser) int(key string) int { + v := p.string(key) + if v == "" { + return -1 + } + + i, err := strconv.Atoi(v) + if err != nil { + p.errors[key] = err + } + return i +} diff --git a/coderd/promoauth/oauth2.go b/coderd/promoauth/oauth2.go new file mode 100644 index 0000000000000..258694563581c --- /dev/null +++ b/coderd/promoauth/oauth2.go @@ -0,0 +1,280 @@ +package promoauth + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "golang.org/x/oauth2" +) + +type Oauth2Source string + +const ( + SourceValidateToken Oauth2Source = "ValidateToken" + SourceExchange Oauth2Source = "Exchange" + SourceTokenSource Oauth2Source = "TokenSource" + SourceAppInstallations Oauth2Source = "AppInstallations" + SourceAuthorizeDevice Oauth2Source = "AuthorizeDevice" +) + +// OAuth2Config exposes a subset of *oauth2.Config functions for easier testing. +// *oauth2.Config should be used instead of implementing this in production. +type OAuth2Config interface { + AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string + Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) + TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource +} + +// InstrumentedOAuth2Config extends OAuth2Config with a `Do` method that allows +// external oauth related calls to be instrumented. This is to support +// "ValidateToken" which is not an oauth2 specified method. +// These calls still count against the api rate limit, and should be instrumented. +type InstrumentedOAuth2Config interface { + OAuth2Config + + // Do is provided as a convenience method to make a request with the oauth2 client. + // It mirrors `http.Client.Do`. + Do(ctx context.Context, source Oauth2Source, req *http.Request) (*http.Response, error) +} + +var _ OAuth2Config = (*Config)(nil) + +// Factory allows us to have 1 set of metrics for all oauth2 providers. +// Primarily to avoid any prometheus errors registering duplicate metrics. +type Factory struct { + metrics *metrics + // optional replace now func + Now func() time.Time +} + +// metrics is the reusable metrics for all oauth2 providers. +type metrics struct { + externalRequestCount *prometheus.CounterVec + + // if the oauth supports it, rate limit metrics. + // rateLimit is the defined limit per interval + rateLimit *prometheus.GaugeVec + rateLimitRemaining *prometheus.GaugeVec + rateLimitUsed *prometheus.GaugeVec + // rateLimitReset is unix time of the next interval (when the rate limit resets). + rateLimitReset *prometheus.GaugeVec + // rateLimitResetIn is the time in seconds until the rate limit resets. + // This is included because it is sometimes more helpful to know the limit + // will reset in 600seconds, rather than at 1704000000 unix time. + rateLimitResetIn *prometheus.GaugeVec +} + +func NewFactory(registry prometheus.Registerer) *Factory { + factory := promauto.With(registry) + + return &Factory{ + metrics: &metrics{ + externalRequestCount: factory.NewCounterVec(prometheus.CounterOpts{ + Namespace: "coderd", + Subsystem: "oauth2", + Name: "external_requests_total", + Help: "The total number of api calls made to external oauth2 providers. 'status_code' will be 0 if the request failed with no response.", + }, []string{ + "name", + "source", + "status_code", + }), + rateLimit: factory.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "coderd", + Subsystem: "oauth2", + Name: "external_requests_rate_limit_total", + Help: "The total number of allowed requests per interval.", + }, []string{ + "name", + // Resource allows different rate limits for the same oauth2 provider. + // Some IDPs have different buckets for different rate limits. + "resource", + }), + rateLimitRemaining: factory.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "coderd", + Subsystem: "oauth2", + Name: "external_requests_rate_limit_remaining", + Help: "The remaining number of allowed requests in this interval.", + }, []string{ + "name", + "resource", + }), + rateLimitUsed: factory.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "coderd", + Subsystem: "oauth2", + Name: "external_requests_rate_limit_used", + Help: "The number of requests made in this interval.", + }, []string{ + "name", + "resource", + }), + rateLimitReset: factory.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "coderd", + Subsystem: "oauth2", + Name: "external_requests_rate_limit_next_reset_unix", + Help: "Unix timestamp for when the next interval starts", + }, []string{ + "name", + "resource", + }), + rateLimitResetIn: factory.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "coderd", + Subsystem: "oauth2", + Name: "external_requests_rate_limit_reset_in_seconds", + Help: "Seconds until the next interval", + }, []string{ + "name", + "resource", + }), + }, + } +} + +func (f *Factory) New(name string, under OAuth2Config) *Config { + return &Config{ + name: name, + underlying: under, + metrics: f.metrics, + } +} + +// NewGithub returns a new instrumented oauth2 config for github. It tracks +// rate limits as well as just the external request counts. +// +//nolint:bodyclose +func (f *Factory) NewGithub(name string, under OAuth2Config) *Config { + cfg := f.New(name, under) + cfg.interceptors = append(cfg.interceptors, func(resp *http.Response, err error) { + limits, ok := githubRateLimits(resp, err) + if !ok { + return + } + labels := prometheus.Labels{ + "name": cfg.name, + "resource": limits.Resource, + } + // Default to -1 for "do not know" + resetIn := float64(-1) + if !limits.Reset.IsZero() { + now := time.Now() + if f.Now != nil { + now = f.Now() + } + resetIn = limits.Reset.Sub(now).Seconds() + if resetIn < 0 { + // If it just reset, just make it 0. + resetIn = 0 + } + } + + f.metrics.rateLimit.With(labels).Set(float64(limits.Limit)) + f.metrics.rateLimitRemaining.With(labels).Set(float64(limits.Remaining)) + f.metrics.rateLimitUsed.With(labels).Set(float64(limits.Used)) + f.metrics.rateLimitReset.With(labels).Set(float64(limits.Reset.Unix())) + f.metrics.rateLimitResetIn.With(labels).Set(resetIn) + }) + return cfg +} + +type Config struct { + // Name is a human friendly name to identify the oauth2 provider. This should be + // deterministic from restart to restart, as it is going to be used as a label in + // prometheus metrics. + name string + underlying OAuth2Config + metrics *metrics + // interceptors are called after every request made by the oauth2 client. + interceptors []func(resp *http.Response, err error) +} + +func (c *Config) Do(ctx context.Context, source Oauth2Source, req *http.Request) (*http.Response, error) { + cli := c.oauthHTTPClient(ctx, source) + return cli.Do(req) +} + +func (c *Config) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { + // No external requests are made when constructing the auth code url. + return c.underlying.AuthCodeURL(state, opts...) +} + +func (c *Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + return c.underlying.Exchange(c.wrapClient(ctx, SourceExchange), code, opts...) +} + +func (c *Config) TokenSource(ctx context.Context, token *oauth2.Token) oauth2.TokenSource { + return c.underlying.TokenSource(c.wrapClient(ctx, SourceTokenSource), token) +} + +// wrapClient is the only way we can accurately instrument the oauth2 client. +// This is because method calls to the 'OAuth2Config' interface are not 1:1 with +// network requests. +// +// For example, the 'TokenSource' method will return a token +// source that will make a network request when the 'Token' method is called on +// it if the token is expired. +func (c *Config) wrapClient(ctx context.Context, source Oauth2Source) context.Context { + return context.WithValue(ctx, oauth2.HTTPClient, c.oauthHTTPClient(ctx, source)) +} + +// oauthHTTPClient returns an http client that will instrument every request made. +func (c *Config) oauthHTTPClient(ctx context.Context, source Oauth2Source) *http.Client { + cli := &http.Client{} + + // Check if the context has a http client already. + if hc, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok { + cli = hc + } + + // The new tripper will instrument every request made by the oauth2 client. + cli.Transport = newInstrumentedTripper(c, source, cli.Transport) + return cli +} + +type instrumentedTripper struct { + c *Config + source Oauth2Source + underlying http.RoundTripper +} + +// newInstrumentedTripper intercepts a http request, and increments the +// externalRequestCount metric. +func newInstrumentedTripper(c *Config, source Oauth2Source, under http.RoundTripper) *instrumentedTripper { + if under == nil { + under = http.DefaultTransport + } + + // If the underlying transport is the default, we need to clone it. + // We should also clone it if it supports cloning. + if tr, ok := under.(*http.Transport); ok { + under = tr.Clone() + } + + return &instrumentedTripper{ + c: c, + source: source, + underlying: under, + } +} + +func (i *instrumentedTripper) RoundTrip(r *http.Request) (*http.Response, error) { + resp, err := i.underlying.RoundTrip(r) + var statusCode int + if resp != nil { + statusCode = resp.StatusCode + } + i.c.metrics.externalRequestCount.With(prometheus.Labels{ + "name": i.c.name, + "source": string(i.source), + "status_code": fmt.Sprintf("%d", statusCode), + }).Inc() + + // Handle any extra interceptors. + for _, interceptor := range i.c.interceptors { + interceptor(resp, err) + } + return resp, err +} diff --git a/coderd/promoauth/oauth2_test.go b/coderd/promoauth/oauth2_test.go new file mode 100644 index 0000000000000..0ee9c6fe6a6a3 --- /dev/null +++ b/coderd/promoauth/oauth2_test.go @@ -0,0 +1,270 @@ +package promoauth_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + ptestutil "github.com/prometheus/client_golang/prometheus/testutil" + io_prometheus_client "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + "golang.org/x/oauth2" + + "github.com/coder/coder/v2/coderd/coderdtest/oidctest" + "github.com/coder/coder/v2/coderd/externalauth" + "github.com/coder/coder/v2/coderd/promoauth" + "github.com/coder/coder/v2/testutil" +) + +func TestInstrument(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + idp := oidctest.NewFakeIDP(t, oidctest.WithServing()) + reg := prometheus.NewRegistry() + t.Cleanup(func() { + if t.Failed() { + t.Log(registryDump(reg)) + } + }) + + const id = "test" + labels := prometheus.Labels{ + "name": id, + "status_code": "200", + } + const metricname = "coderd_oauth2_external_requests_total" + count := func(source string) int { + labels["source"] = source + return counterValue(t, reg, "coderd_oauth2_external_requests_total", labels) + } + + factory := promoauth.NewFactory(reg) + + cfg := externalauth.Config{ + InstrumentedOAuth2Config: factory.New(id, idp.OIDCConfig(t, []string{})), + ID: "test", + ValidateURL: must[*url.URL](t)(idp.IssuerURL().Parse("/oauth2/userinfo")).String(), + } + + // 0 Requests before we start + require.Nil(t, metricValue(t, reg, metricname, labels), "no metrics at start") + + // Exchange should trigger a request + code := idp.CreateAuthCode(t, "foo") + token, err := cfg.Exchange(ctx, code) + require.NoError(t, err) + require.Equal(t, count("Exchange"), 1) + + // Force a refresh + token.Expiry = time.Now().Add(time.Hour * -1) + src := cfg.TokenSource(ctx, token) + refreshed, err := src.Token() + require.NoError(t, err) + require.NotEqual(t, token.AccessToken, refreshed.AccessToken, "token refreshed") + require.Equal(t, count("TokenSource"), 1) + + // Try a validate + valid, _, err := cfg.ValidateToken(ctx, refreshed.AccessToken) + require.NoError(t, err) + require.True(t, valid) + require.Equal(t, count("ValidateToken"), 1) + + // Verify the default client was not broken. This check is added because we + // extend the http.DefaultTransport. If a `.Clone()` is not done, this can be + // mis-used. It is cheap to run this quick check. + snapshot := registryDump(reg) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, + must[*url.URL](t)(idp.IssuerURL().Parse("/.well-known/openid-configuration")).String(), nil) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + _ = resp.Body.Close() + + require.NoError(t, compare(reg, snapshot), "no metric changes") +} + +func TestGithubRateLimits(t *testing.T) { + t.Parallel() + + now := time.Now() + cases := []struct { + Name string + NoHeaders bool + Omit []string + ExpectNoMetrics bool + Limit int + Remaining int + Used int + Reset time.Time + + at time.Time + }{ + { + Name: "NoHeaders", + NoHeaders: true, + ExpectNoMetrics: true, + }, + { + Name: "ZeroHeaders", + ExpectNoMetrics: true, + }, + { + Name: "OverLimit", + Limit: 100, + Remaining: 0, + Used: 500, + Reset: now.Add(time.Hour), + at: now, + }, + { + Name: "UnderLimit", + Limit: 100, + Remaining: 0, + Used: 500, + Reset: now.Add(time.Hour), + at: now, + }, + { + Name: "Partial", + Omit: []string{"x-ratelimit-remaining"}, + ExpectNoMetrics: true, + Limit: 100, + Remaining: 0, + Used: 500, + Reset: now.Add(time.Hour), + at: now, + }, + } + + for _, c := range cases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + reg := prometheus.NewRegistry() + idp := oidctest.NewFakeIDP(t, oidctest.WithMiddlewares( + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if !c.NoHeaders { + rw.Header().Set("x-ratelimit-limit", fmt.Sprintf("%d", c.Limit)) + rw.Header().Set("x-ratelimit-remaining", fmt.Sprintf("%d", c.Remaining)) + rw.Header().Set("x-ratelimit-used", fmt.Sprintf("%d", c.Used)) + rw.Header().Set("x-ratelimit-resource", "core") + rw.Header().Set("x-ratelimit-reset", fmt.Sprintf("%d", c.Reset.Unix())) + for _, omit := range c.Omit { + rw.Header().Del(omit) + } + } + + next.ServeHTTP(rw, r) + }) + })) + + factory := promoauth.NewFactory(reg) + if !c.at.IsZero() { + factory.Now = func() time.Time { + return c.at + } + } + + cfg := factory.NewGithub("test", idp.OIDCConfig(t, []string{})) + + // Do a single oauth2 call + ctx := testutil.Context(t, testutil.WaitShort) + ctx = context.WithValue(ctx, oauth2.HTTPClient, idp.HTTPClient(nil)) + _, err := cfg.Exchange(ctx, idp.CreateAuthCode(t, "foo")) + require.NoError(t, err) + + // Verify + labels := prometheus.Labels{ + "name": "test", + "resource": "core", + } + pass := true + if !c.ExpectNoMetrics { + pass = pass && assert.Equal(t, gaugeValue(t, reg, "coderd_oauth2_external_requests_rate_limit_total", labels), c.Limit, "limit") + pass = pass && assert.Equal(t, gaugeValue(t, reg, "coderd_oauth2_external_requests_rate_limit_remaining", labels), c.Remaining, "remaining") + pass = pass && assert.Equal(t, gaugeValue(t, reg, "coderd_oauth2_external_requests_rate_limit_used", labels), c.Used, "used") + if !c.at.IsZero() { + until := c.Reset.Sub(c.at) + // Float accuracy is not great, so we allow a delta of 2 + pass = pass && assert.InDelta(t, gaugeValue(t, reg, "coderd_oauth2_external_requests_rate_limit_reset_in_seconds", labels), int(until.Seconds()), 2, "reset in") + } + } else { + pass = pass && assert.Nil(t, metricValue(t, reg, "coderd_oauth2_external_requests_rate_limit_total", labels), "not exists") + } + + // Helpful debugging + if !pass { + t.Log(registryDump(reg)) + } + }) + } +} + +func registryDump(reg *prometheus.Registry) string { + h := promhttp.HandlerFor(reg, promhttp.HandlerOpts{}) + rec := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil) + h.ServeHTTP(rec, req) + resp := rec.Result() + data, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + return string(data) +} + +func must[V any](t *testing.T) func(v V, err error) V { + return func(v V, err error) V { + t.Helper() + require.NoError(t, err) + return v + } +} + +func gaugeValue(t testing.TB, reg prometheus.Gatherer, metricName string, labels prometheus.Labels) int { + labeled := metricValue(t, reg, metricName, labels) + require.NotNilf(t, labeled, "metric %q with labels %v not found", metricName, labels) + return int(labeled.GetGauge().GetValue()) +} + +func counterValue(t testing.TB, reg prometheus.Gatherer, metricName string, labels prometheus.Labels) int { + labeled := metricValue(t, reg, metricName, labels) + require.NotNilf(t, labeled, "metric %q with labels %v not found", metricName, labels) + return int(labeled.GetCounter().GetValue()) +} + +func compare(reg prometheus.Gatherer, compare string) error { + return ptestutil.GatherAndCompare(reg, strings.NewReader(compare)) +} + +func metricValue(t testing.TB, reg prometheus.Gatherer, metricName string, labels prometheus.Labels) *io_prometheus_client.Metric { + metrics, err := reg.Gather() + require.NoError(t, err) + + for _, m := range metrics { + if m.GetName() == metricName { + for _, labeled := range m.GetMetric() { + mLables := make(prometheus.Labels) + for _, v := range labeled.GetLabel() { + mLables[v.GetName()] = v.GetValue() + } + if maps.Equal(mLables, labels) { + return labeled + } + } + } + } + return nil +} diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index f204cf2a728a4..0619e99f1cb76 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -32,7 +32,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" - "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/promoauth" "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/tracing" @@ -55,7 +55,7 @@ const ( ) type Options struct { - OIDCConfig httpmw.OAuth2Config + OIDCConfig promoauth.OAuth2Config ExternalAuthConfigs []*externalauth.Config // TimeNowFn is only used in tests TimeNowFn func() time.Time @@ -96,7 +96,7 @@ type server struct { UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore] DeploymentValues *codersdk.DeploymentValues - OIDCConfig httpmw.OAuth2Config + OIDCConfig promoauth.OAuth2Config TimeNowFn func() time.Time @@ -242,10 +242,8 @@ func (s *server) heartbeatLoop() { } start := s.timeNow() hbCtx, hbCancel := context.WithTimeout(s.lifecycleCtx, s.heartbeatInterval) - if err := s.heartbeat(hbCtx); err != nil { - if !xerrors.Is(err, context.DeadlineExceeded) && !xerrors.Is(err, context.Canceled) { - s.Logger.Error(hbCtx, "heartbeat failed", slog.Error(err)) - } + if err := s.heartbeat(hbCtx); err != nil && !database.IsQueryCanceledError(err) { + s.Logger.Error(hbCtx, "heartbeat failed", slog.Error(err)) } hbCancel() elapsed := s.timeNow().Sub(start) @@ -559,6 +557,7 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo WorkspaceName: workspace.Name, WorkspaceOwner: owner.Username, WorkspaceOwnerEmail: owner.Email, + WorkspaceOwnerName: owner.Name, WorkspaceOwnerOidcAccessToken: workspaceOwnerOIDCAccessToken, WorkspaceId: workspace.ID.String(), WorkspaceOwnerId: owner.ID.String(), @@ -1738,7 +1737,7 @@ func deleteSessionToken(ctx context.Context, db database.Store, workspace databa // obtainOIDCAccessToken returns a valid OpenID Connect access token // for the user if it's able to obtain one, otherwise it returns an empty string. -func obtainOIDCAccessToken(ctx context.Context, db database.Store, oidcConfig httpmw.OAuth2Config, userID uuid.UUID) (string, error) { +func obtainOIDCAccessToken(ctx context.Context, db database.Store, oidcConfig promoauth.OAuth2Config, userID uuid.UUID) (string, error) { link, err := db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{ UserID: userID, LoginType: database.LoginTypeOIDC, diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index c2e8c6a836d74..738e9da8dbd2f 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -24,6 +24,7 @@ import ( "golang.org/x/oauth2" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" @@ -186,8 +187,8 @@ func TestAcquireJob(t *testing.T) { srv, db, ps, _ := setup(t, false, &overrides{ deploymentValues: dv, externalAuthConfigs: []*externalauth.Config{{ - ID: gitAuthProvider, - OAuth2Config: &testutil.OAuth2Config{}, + ID: gitAuthProvider, + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, }}, }) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) @@ -340,6 +341,7 @@ func TestAcquireJob(t *testing.T) { WorkspaceName: workspace.Name, WorkspaceOwner: user.Username, WorkspaceOwnerEmail: user.Email, + WorkspaceOwnerName: user.Name, WorkspaceOwnerOidcAccessToken: link.OAuthAccessToken, WorkspaceId: workspace.ID.String(), WorkspaceOwnerId: user.ID.String(), @@ -1784,8 +1786,8 @@ func setup(t *testing.T, ignoreLogErrors bool, ov *overrides) (proto.DRPCProvisi Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho}, Tags: database.StringMap{}, LastSeenAt: sql.NullTime{}, - Version: "", - APIVersion: "1.0", + Version: buildinfo.Version(), + APIVersion: provisionersdk.VersionCurrent.String(), }) require.NoError(t, err) diff --git a/coderd/provisionerjobs_internal_test.go b/coderd/provisionerjobs_internal_test.go index 05fddb722b4b1..95ad2197865eb 100644 --- a/coderd/provisionerjobs_internal_test.go +++ b/coderd/provisionerjobs_internal_test.go @@ -10,10 +10,10 @@ import ( "testing" "time" - "github.com/golang/mock/gomock" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "nhooyr.io/websocket" "cdr.dev/slog/sloggers/slogtest" diff --git a/coderd/tailnet.go b/coderd/tailnet.go index b04f3dc519fec..6521d79149b48 100644 --- a/coderd/tailnet.go +++ b/coderd/tailnet.go @@ -224,6 +224,7 @@ func (s *ServerTailnet) watchAgentUpdates() { nodes, ok := conn.NextUpdate(s.ctx) if !ok { if conn.IsClosed() && s.ctx.Err() == nil { + s.logger.Warn(s.ctx, "multiagent closed, reinitializing") s.reinitCoordinator() continue } @@ -247,6 +248,7 @@ func (s *ServerTailnet) getAgentConn() tailnet.MultiAgentConn { } func (s *ServerTailnet) reinitCoordinator() { + start := time.Now() for retrier := retry.New(25*time.Millisecond, 5*time.Second); retrier.Wait(s.ctx); { s.nodesMu.Lock() agentConn, err := s.getMultiAgent(s.ctx) @@ -264,6 +266,11 @@ func (s *ServerTailnet) reinitCoordinator() { s.logger.Warn(s.ctx, "resubscribe to agent", slog.Error(err), slog.F("agent_id", agentID)) } } + + s.logger.Info(s.ctx, "successfully reinitialized multiagent", + slog.F("agents", len(s.agentConnectionTimes)), + slog.F("took", time.Since(start)), + ) s.nodesMu.Unlock() return } diff --git a/coderd/templates.go b/coderd/templates.go index 5e6d9644a782f..78f918fe18180 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -667,6 +667,11 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { name = template.Name } + groupACL := template.GroupACL + if req.DisableEveryoneGroupAccess { + delete(groupACL, template.OrganizationID.String()) + } + var err error err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{ ID: template.ID, @@ -676,6 +681,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { Description: req.Description, Icon: req.Icon, AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs, + GroupACL: groupACL, }) if err != nil { return xerrors.Errorf("update template metadata: %w", err) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index b7765f076b2f7..4423bbc4e7056 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -335,10 +335,10 @@ func TestTemplateVersionsExternalAuth(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{ IncludeProvisionerDaemon: true, ExternalAuthConfigs: []*externalauth.Config{{ - OAuth2Config: &testutil.OAuth2Config{}, - ID: "github", - Regex: regexp.MustCompile(`github\.com`), - Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, + ID: "github", + Regex: regexp.MustCompile(`github\.com`), + Type: codersdk.EnhancedExternalAuthProviderGitHub.String(), }}, }) user := coderdtest.CreateFirstUser(t, client) diff --git a/coderd/userauth.go b/coderd/userauth.go index 94fe821da7cf2..4c160c883e6e1 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -31,6 +31,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/promoauth" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/userpassword" "github.com/coder/coder/v2/codersdk" @@ -438,7 +439,7 @@ type GithubOAuth2Team struct { // GithubOAuth2Provider exposes required functions for the Github authentication flow. type GithubOAuth2Config struct { - httpmw.OAuth2Config + promoauth.OAuth2Config AuthenticatedUser func(ctx context.Context, client *http.Client) (*github.User, error) ListEmails func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) ListOrganizationMemberships func(ctx context.Context, client *http.Client) ([]*github.Membership, error) @@ -662,7 +663,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { } type OIDCConfig struct { - httpmw.OAuth2Config + promoauth.OAuth2Config Provider *oidc.Provider Verifier *oidc.IDTokenVerifier @@ -1500,6 +1501,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C user, err = tx.UpdateUserProfile(dbauthz.AsSystemRestricted(ctx), database.UpdateUserProfileParams{ ID: user.ID, Email: user.Email, + Name: user.Name, Username: user.Username, UpdatedAt: dbtime.Now(), AvatarURL: user.AvatarURL, diff --git a/coderd/users.go b/coderd/users.go index 4cfa7e7ead877..6cb8b03d37b50 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -152,7 +152,16 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { } if createUser.Trial && api.TrialGenerator != nil { - err = api.TrialGenerator(ctx, createUser.Email) + err = api.TrialGenerator(ctx, codersdk.LicensorTrialRequest{ + Email: createUser.Email, + FirstName: createUser.TrialInfo.FirstName, + LastName: createUser.TrialInfo.LastName, + PhoneNumber: createUser.TrialInfo.PhoneNumber, + JobTitle: createUser.TrialInfo.JobTitle, + CompanyName: createUser.TrialInfo.CompanyName, + Country: createUser.TrialInfo.Country, + Developers: createUser.TrialInfo.Developers, + }) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Failed to generate trial", @@ -647,6 +656,7 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) { updatedUserProfile, err := api.Database.UpdateUserProfile(ctx, database.UpdateUserProfileParams{ ID: user.ID, Email: user.Email, + Name: params.Name, AvatarURL: user.AvatarURL, Username: params.Username, UpdatedAt: dbtime.Now(), diff --git a/coderd/users_test.go b/coderd/users_test.go index 8cbd69308e61f..c73bd3014dc05 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -76,7 +76,7 @@ func TestFirstUser(t *testing.T) { t.Parallel() called := make(chan struct{}) client := coderdtest.New(t, &coderdtest.Options{ - TrialGenerator: func(ctx context.Context, s string) error { + TrialGenerator: func(context.Context, codersdk.LicensorTrialRequest) error { close(called) return nil }, @@ -677,7 +677,7 @@ func TestUpdateUserProfile(t *testing.T) { require.Equal(t, http.StatusConflict, apiErr.StatusCode()) }) - t.Run("UpdateUsername", func(t *testing.T) { + t.Run("UpdateUser", func(t *testing.T) { t.Parallel() auditor := audit.NewMock() client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor}) @@ -692,14 +692,39 @@ func TestUpdateUserProfile(t *testing.T) { _, _ = client.User(ctx, codersdk.Me) userProfile, err := client.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{ Username: "newusername", + Name: "Mr User", }) require.NoError(t, err) require.Equal(t, userProfile.Username, "newusername") + require.Equal(t, userProfile.Name, "Mr User") numLogs++ // add an audit log for user update require.Len(t, auditor.AuditLogs(), numLogs) require.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[numLogs-1].Action) }) + + t.Run("InvalidRealUserName", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "john@coder.com", + Username: "john", + Password: "SomeSecurePassword!", + OrganizationID: user.OrganizationID, + }) + require.NoError(t, err) + _, err = client.UpdateUserProfile(ctx, codersdk.Me, codersdk.UpdateUserProfileRequest{ + Name: " Mr Bean", // must not have leading space + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) } func TestUpdateUserPassword(t *testing.T) { diff --git a/coderd/util/apiversion/apiversion.go b/coderd/util/apiversion/apiversion.go new file mode 100644 index 0000000000000..7decaeab325c7 --- /dev/null +++ b/coderd/util/apiversion/apiversion.go @@ -0,0 +1,89 @@ +package apiversion + +import ( + "fmt" + "strconv" + "strings" + + "golang.org/x/xerrors" +) + +// New returns an *APIVersion with the given major.minor and +// additional supported major versions. +func New(maj, min int) *APIVersion { + v := &APIVersion{ + supportedMajor: maj, + supportedMinor: min, + additionalMajors: make([]int, 0), + } + return v +} + +type APIVersion struct { + supportedMajor int + supportedMinor int + additionalMajors []int +} + +func (v *APIVersion) WithBackwardCompat(majs ...int) *APIVersion { + v.additionalMajors = append(v.additionalMajors, majs[:]...) + return v +} + +// Validate validates the given version against the given constraints: +// A given major.minor version is valid iff: +// 1. The requested major version is contained within v.supportedMajors +// 2. If the requested major version is the 'current major', then +// the requested minor version must be less than or equal to the supported +// minor version. +// +// For example, given majors {1, 2} and minor 2, then: +// - 0.x is not supported, +// - 1.x is supported, +// - 2.0, 2.1, and 2.2 are supported, +// - 2.3+ is not supported. +func (v *APIVersion) String() string { + return fmt.Sprintf("%d.%d", v.supportedMajor, v.supportedMinor) +} + +func (v *APIVersion) Validate(version string) error { + major, minor, err := Parse(version) + if err != nil { + return err + } + if major > v.supportedMajor { + return xerrors.Errorf("server is at version %d.%d, behind requested major version %s", + v.supportedMajor, v.supportedMinor, version) + } + if major == v.supportedMajor { + if minor > v.supportedMinor { + return xerrors.Errorf("server is at version %d.%d, behind requested minor version %s", + v.supportedMajor, v.supportedMinor, version) + } + return nil + } + for _, mjr := range v.additionalMajors { + if major == mjr { + return nil + } + } + return xerrors.Errorf("version %s is no longer supported", version) +} + +// Parse parses a valid major.minor version string into (major, minor). +// Both major and minor must be valid integers separated by a period '.'. +func Parse(version string) (major int, minor int, err error) { + parts := strings.Split(version, ".") + if len(parts) != 2 { + return 0, 0, xerrors.Errorf("invalid version string: %s", version) + } + major, err = strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, xerrors.Errorf("invalid major version: %s", version) + } + minor, err = strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, xerrors.Errorf("invalid minor version: %s", version) + } + return major, minor, nil +} diff --git a/coderd/util/apiversion/apiversion_test.go b/coderd/util/apiversion/apiversion_test.go new file mode 100644 index 0000000000000..0bd6fe0f6b52f --- /dev/null +++ b/coderd/util/apiversion/apiversion_test.go @@ -0,0 +1,90 @@ +package apiversion_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/util/apiversion" +) + +func TestAPIVersionValidate(t *testing.T) { + t.Parallel() + + // Given + v := apiversion.New(2, 1).WithBackwardCompat(1) + + for _, tc := range []struct { + name string + version string + expectedError string + }{ + { + name: "OK", + version: "2.1", + }, + { + name: "MinorOK", + version: "2.0", + }, + { + name: "MajorOK", + version: "1.0", + }, + { + name: "TooNewMinor", + version: "2.2", + expectedError: "behind requested minor version", + }, + { + name: "TooNewMajor", + version: "3.1", + expectedError: "behind requested major version", + }, + { + name: "Malformed0", + version: "cats", + expectedError: "invalid version string", + }, + { + name: "Malformed1", + version: "cats.dogs", + expectedError: "invalid major version", + }, + { + name: "Malformed2", + version: "1.dogs", + expectedError: "invalid minor version", + }, + { + name: "Malformed3", + version: "1.0.1", + expectedError: "invalid version string", + }, + { + name: "Malformed4", + version: "11", + expectedError: "invalid version string", + }, + { + name: "TooOld", + version: "0.8", + expectedError: "no longer supported", + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + err := v.Validate(tc.version) + + // Then + if tc.expectedError == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tc.expectedError) + } + }) + } +} diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index dd47275a4f6ac..1e48ea0e7a088 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -12,11 +12,9 @@ import ( "net/http" "net/netip" "net/url" - "runtime/pprof" "sort" "strconv" "strings" - "sync/atomic" "time" "github.com/google/uuid" @@ -42,7 +40,6 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/prometheusmetrics" "github.com/coder/coder/v2/coderd/rbac" - "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/tailnet" @@ -215,8 +212,10 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request) httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Manifest{ AgentID: agentID, + AgentName: manifest.AgentName, OwnerName: manifest.OwnerUsername, WorkspaceID: workspaceID, + WorkspaceName: manifest.WorkspaceName, Apps: apps, Scripts: scripts, DERPMap: tailnet.DERPMapFromProto(manifest.DerpMap), @@ -1084,21 +1083,10 @@ func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request api.WebsocketWaitMutex.Unlock() defer api.WebsocketWaitGroup.Done() workspaceAgent := httpmw.WorkspaceAgent(r) - resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Failed to accept websocket.", - Detail: err.Error(), - }) - return - } - - build, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Internal error fetching workspace build job.", - Detail: err.Error(), - }) + // Ensure the resource is still valid! + // We only accept agents for resources on the latest build. + build, ok := ensureLatestBuild(ctx, api.Database, api.Logger, rw, workspaceAgent) + if !ok { return } @@ -1120,32 +1108,6 @@ func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request return } - // Ensure the resource is still valid! - // We only accept agents for resources on the latest build. - ensureLatestBuild := func() error { - latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, build.WorkspaceID) - if err != nil { - return err - } - if build.ID != latestBuild.ID { - return xerrors.New("build is outdated") - } - return nil - } - - err = ensureLatestBuild() - if err != nil { - api.Logger.Debug(ctx, "agent tried to connect from non-latest build", - slog.F("resource", resource), - slog.F("agent", workspaceAgent), - ) - httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ - Message: "Agent trying to connect from non-latest build.", - Detail: err.Error(), - }) - return - } - conn, err := websocket.Accept(rw, r, nil) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -1158,109 +1120,10 @@ func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request ctx, wsNetConn := websocketNetConn(ctx, conn, websocket.MessageBinary) defer wsNetConn.Close() - // We use a custom heartbeat routine here instead of `httpapi.Heartbeat` - // because we want to log the agent's last ping time. - var lastPing atomic.Pointer[time.Time] - lastPing.Store(ptr.Ref(time.Now())) // Since the agent initiated the request, assume it's alive. - - go pprof.Do(ctx, pprof.Labels("agent", workspaceAgent.ID.String()), func(ctx context.Context) { - // TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout? - t := time.NewTicker(api.AgentConnectionUpdateFrequency) - defer t.Stop() - - for { - select { - case <-t.C: - case <-ctx.Done(): - return - } - - // We don't need a context that times out here because the ping will - // eventually go through. If the context times out, then other - // websocket read operations will receive an error, obfuscating the - // actual problem. - err := conn.Ping(ctx) - if err != nil { - return - } - lastPing.Store(ptr.Ref(time.Now())) - } - }) - - firstConnectedAt := workspaceAgent.FirstConnectedAt - if !firstConnectedAt.Valid { - firstConnectedAt = sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - } - } - lastConnectedAt := sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - } - disconnectedAt := workspaceAgent.DisconnectedAt - updateConnectionTimes := func(ctx context.Context) error { - //nolint:gocritic // We only update ourself. - err = api.Database.UpdateWorkspaceAgentConnectionByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAgentConnectionByIDParams{ - ID: workspaceAgent.ID, - FirstConnectedAt: firstConnectedAt, - LastConnectedAt: lastConnectedAt, - DisconnectedAt: disconnectedAt, - UpdatedAt: dbtime.Now(), - LastConnectedReplicaID: uuid.NullUUID{ - UUID: api.ID, - Valid: true, - }, - }) - if err != nil { - return err - } - return nil - } - - defer func() { - // If connection closed then context will be canceled, try to - // ensure our final update is sent. By waiting at most the agent - // inactive disconnect timeout we ensure that we don't block but - // also guarantee that the agent will be considered disconnected - // by normal status check. - // - // Use a system context as the agent has disconnected and that token - // may no longer be valid. - //nolint:gocritic - ctx, cancel := context.WithTimeout(dbauthz.AsSystemRestricted(api.ctx), api.AgentInactiveDisconnectTimeout) - defer cancel() - - // Only update timestamp if the disconnect is new. - if !disconnectedAt.Valid { - disconnectedAt = sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - } - } - err := updateConnectionTimes(ctx) - if err != nil { - // This is a bug with unit tests that cancel the app context and - // cause this error log to be generated. We should fix the unit tests - // as this is a valid log. - // - // The pq error occurs when the server is shutting down. - if !xerrors.Is(err, context.Canceled) && !database.IsQueryCanceledError(err) { - api.Logger.Error(ctx, "failed to update agent disconnect time", - slog.Error(err), - slog.F("workspace_id", build.WorkspaceID), - ) - } - } - api.publishWorkspaceUpdate(ctx, build.WorkspaceID) - }() - - err = updateConnectionTimes(ctx) - if err != nil { - _ = conn.Close(websocket.StatusGoingAway, err.Error()) - return - } - api.publishWorkspaceUpdate(ctx, build.WorkspaceID) + closeCtx, closeCtxCancel := context.WithCancel(ctx) + defer closeCtxCancel() + monitor := api.startAgentWebsocketMonitor(closeCtx, workspaceAgent, build, conn) + defer monitor.close() api.Logger.Debug(ctx, "accepting agent", slog.F("owner", owner.Username), @@ -1271,61 +1134,13 @@ func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request defer conn.Close(websocket.StatusNormalClosure, "") - closeChan := make(chan struct{}) - go func() { - defer close(closeChan) - err := (*api.TailnetCoordinator.Load()).ServeAgent(wsNetConn, workspaceAgent.ID, - fmt.Sprintf("%s-%s-%s", owner.Username, workspace.Name, workspaceAgent.Name), - ) - if err != nil { - api.Logger.Warn(ctx, "tailnet coordinator agent error", slog.Error(err)) - _ = conn.Close(websocket.StatusInternalError, err.Error()) - return - } - }() - ticker := time.NewTicker(api.AgentConnectionUpdateFrequency) - defer ticker.Stop() - for { - select { - case <-closeChan: - return - case <-ticker.C: - } - - lastPing := *lastPing.Load() - - var connectionStatusChanged bool - if time.Since(lastPing) > api.AgentInactiveDisconnectTimeout { - if !disconnectedAt.Valid { - connectionStatusChanged = true - disconnectedAt = sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - } - } - } else { - connectionStatusChanged = disconnectedAt.Valid - // TODO(mafredri): Should we update it here or allow lastConnectedAt to shadow it? - disconnectedAt = sql.NullTime{} - lastConnectedAt = sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - } - } - err = updateConnectionTimes(ctx) - if err != nil { - _ = conn.Close(websocket.StatusGoingAway, err.Error()) - return - } - if connectionStatusChanged { - api.publishWorkspaceUpdate(ctx, build.WorkspaceID) - } - err := ensureLatestBuild() - if err != nil { - // Disconnect agents that are no longer valid. - _ = conn.Close(websocket.StatusGoingAway, "") - return - } + err = (*api.TailnetCoordinator.Load()).ServeAgent(wsNetConn, workspaceAgent.ID, + fmt.Sprintf("%s-%s-%s", owner.Username, workspace.Name, workspaceAgent.Name), + ) + if err != nil { + api.Logger.Warn(ctx, "tailnet coordinator agent error", slog.Error(err)) + _ = conn.Close(websocket.StatusInternalError, err.Error()) + return } } @@ -1365,7 +1180,7 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R if qv != "" { version = qv } - if err := tailnet.ValidateVersion(version); err != nil { + if err := tailnet.CurrentVersion.Validate(version); err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Unknown or unsupported API version", Validations: []codersdk.ValidationError{ @@ -2236,13 +2051,14 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ if listen { // Since we're ticking frequently and this sign-in operation is rare, // we are OK with polling to avoid the complexity of pubsub. - ticker := time.NewTicker(time.Second) - defer ticker.Stop() + ticker, done := api.NewTicker(time.Second) + defer done() + var previousToken database.ExternalAuthLink for { select { case <-ctx.Done(): return - case <-ticker.C: + case <-ticker: } externalAuthLink, err := api.Database.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{ ProviderID: externalAuthConfig.ID, @@ -2266,6 +2082,15 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ if externalAuthLink.OAuthExpiry.Before(dbtime.Now()) && !externalAuthLink.OAuthExpiry.IsZero() { continue } + + // Only attempt to revalidate an oauth token if it has actually changed. + // No point in trying to validate the same token over and over again. + if previousToken.OAuthAccessToken == externalAuthLink.OAuthAccessToken && + previousToken.OAuthRefreshToken == externalAuthLink.OAuthRefreshToken && + previousToken.OAuthExpiry == externalAuthLink.OAuthExpiry { + continue + } + valid, _, err := externalAuthConfig.ValidateToken(ctx, externalAuthLink.OAuthAccessToken) if err != nil { api.Logger.Warn(ctx, "failed to validate external auth token", @@ -2274,6 +2099,7 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ slog.Error(err), ) } + previousToken = externalAuthLink if !valid { continue } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 5232b71113ea9..0d620c991e6dd 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -25,12 +25,15 @@ import ( "github.com/coder/coder/v2/agent/agenttest" "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/coderdtest/oidctest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbfake" + "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbmem" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" @@ -1536,3 +1539,94 @@ func TestWorkspaceAgent_UpdatedDERP(t *testing.T) { require.True(t, ok) require.Equal(t, []int{2}, conn2.DERPMap().RegionIDs()) } + +func TestWorkspaceAgentExternalAuthListen(t *testing.T) { + t.Parallel() + + // ValidateURLSpam acts as a workspace calling GIT_ASK_PASS which + // will wait until the external auth token is valid. The issue is we spam + // the validate endpoint with requests until the token is valid. We do this + // even if the token has not changed. We are calling validate with the + // same inputs expecting a different result (insanity?). To reduce our + // api rate limit usage, we should do nothing if the inputs have not + // changed. + // + // Note that an expired oauth token is already skipped, so this really + // only covers the case of a revoked token. + t.Run("ValidateURLSpam", func(t *testing.T) { + t.Parallel() + + const providerID = "fake-idp" + + // Count all the times we call validate + validateCalls := 0 + fake := oidctest.NewFakeIDP(t, oidctest.WithServing(), oidctest.WithMiddlewares(func(handler http.Handler) http.Handler { + return http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Count all the validate calls + if strings.Contains(r.URL.Path, "/external-auth-validate/") { + validateCalls++ + } + handler.ServeHTTP(w, r) + })) + })) + + ticks := make(chan time.Time) + // setup + ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ + NewTicker: func(duration time.Duration) (<-chan time.Time, func()) { + return ticks, func() {} + }, + ExternalAuthConfigs: []*externalauth.Config{ + fake.ExternalAuthConfig(t, providerID, nil, func(cfg *externalauth.Config) { + cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String() + }), + }, + }) + first := coderdtest.CreateFirstUser(t, ownerClient) + tmpDir := t.TempDir() + client, user := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID) + + r := dbfake.WorkspaceBuild(t, db, database.Workspace{ + OrganizationID: first.OrganizationID, + OwnerID: user.ID, + }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { + agents[0].Directory = tmpDir + return agents + }).Do() + + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(r.AgentToken) + + // We need to include an invalid oauth token that is not expired. + dbgen.ExternalAuthLink(t, db, database.ExternalAuthLink{ + ProviderID: providerID, + UserID: user.ID, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + OAuthAccessToken: "invalid", + OAuthRefreshToken: "bad", + OAuthExpiry: dbtime.Now().Add(time.Hour), + }) + + ctx, cancel := context.WithCancel(testutil.Context(t, testutil.WaitShort)) + go func() { + // The request that will block and fire off validate calls. + _, err := agentClient.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{ + ID: providerID, + Match: "", + Listen: true, + }) + assert.Error(t, err, "this should fail") + }() + + // Send off 10 ticks to cause 10 validate calls + for i := 0; i < 10; i++ { + ticks <- time.Now() + } + cancel() + // We expect only 1 + // In a failed test, you will likely see 9, as the last one + // gets canceled. + require.Equal(t, 1, validateCalls, "validate calls duplicated on same token") + }) +} diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index 9b4987867e40a..6b9438a8b8c9f 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -3,8 +3,10 @@ package coderd import ( "context" "database/sql" + "fmt" "net/http" "runtime/pprof" + "sync" "sync/atomic" "time" @@ -22,6 +24,7 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/tailnet" ) // @Summary Workspace agent RPC API @@ -40,7 +43,7 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { defer api.WebsocketWaitGroup.Done() workspaceAgent := httpmw.WorkspaceAgent(r) - ensureLatestBuildFn, build, ok := ensureLatestBuild(ctx, api.Database, api.Logger, rw, workspaceAgent) + build, ok := ensureLatestBuild(ctx, api.Database, api.Logger, rw, workspaceAgent) if !ok { return } @@ -94,10 +97,10 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { defer conn.Close(websocket.StatusNormalClosure, "") - pingFn, ok := api.agentConnectionUpdate(ctx, workspaceAgent, build.WorkspaceID, conn) - if !ok { - return - } + closeCtx, closeCtxCancel := context.WithCancel(ctx) + defer closeCtxCancel() + monitor := api.startAgentWebsocketMonitor(closeCtx, workspaceAgent, build, conn) + defer monitor.close() agentAPI := agentapi.New(agentapi.Options{ AgentID: workspaceAgent.ID, @@ -128,28 +131,28 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { UpdateAgentMetricsFn: api.UpdateAgentMetrics, }) - closeCtx, closeCtxCancel := context.WithCancel(ctx) - go func() { - defer closeCtxCancel() - err := agentAPI.Serve(ctx, mux) - if err != nil { - api.Logger.Warn(ctx, "workspace agent RPC listen error", slog.Error(err)) - _ = conn.Close(websocket.StatusInternalError, err.Error()) - return - } - }() - - pingFn(closeCtx, ensureLatestBuildFn) + streamID := tailnet.StreamID{ + Name: fmt.Sprintf("%s-%s-%s", owner.Username, workspace.Name, workspaceAgent.Name), + ID: workspaceAgent.ID, + Auth: tailnet.AgentTunnelAuth{}, + } + ctx = tailnet.WithStreamID(ctx, streamID) + err = agentAPI.Serve(ctx, mux) + if err != nil { + api.Logger.Warn(ctx, "workspace agent RPC listen error", slog.Error(err)) + _ = conn.Close(websocket.StatusInternalError, err.Error()) + return + } } -func ensureLatestBuild(ctx context.Context, db database.Store, logger slog.Logger, rw http.ResponseWriter, workspaceAgent database.WorkspaceAgent) (func() error, database.WorkspaceBuild, bool) { +func ensureLatestBuild(ctx context.Context, db database.Store, logger slog.Logger, rw http.ResponseWriter, workspaceAgent database.WorkspaceAgent) (database.WorkspaceBuild, bool) { resource, err := db.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Internal error fetching workspace agent resource.", Detail: err.Error(), }) - return nil, database.WorkspaceBuild{}, false + return database.WorkspaceBuild{}, false } build, err := db.GetWorkspaceBuildByJobID(ctx, resource.JobID) @@ -158,23 +161,12 @@ func ensureLatestBuild(ctx context.Context, db database.Store, logger slog.Logge Message: "Internal error fetching workspace build job.", Detail: err.Error(), }) - return nil, database.WorkspaceBuild{}, false + return database.WorkspaceBuild{}, false } // Ensure the resource is still valid! // We only accept agents for resources on the latest build. - ensureLatestBuild := func() error { - latestBuild, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, build.WorkspaceID) - if err != nil { - return err - } - if build.ID != latestBuild.ID { - return xerrors.New("build is outdated") - } - return nil - } - - err = ensureLatestBuild() + err = checkBuildIsLatest(ctx, db, build) if err != nil { logger.Debug(ctx, "agent tried to connect from non-latest build", slog.F("resource", resource), @@ -184,73 +176,159 @@ func ensureLatestBuild(ctx context.Context, db database.Store, logger slog.Logge Message: "Agent trying to connect from non-latest build.", Detail: err.Error(), }) - return nil, database.WorkspaceBuild{}, false + return database.WorkspaceBuild{}, false } - return ensureLatestBuild, build, true + return build, true } -func (api *API) agentConnectionUpdate(ctx context.Context, workspaceAgent database.WorkspaceAgent, workspaceID uuid.UUID, conn *websocket.Conn) (func(closeCtx context.Context, ensureLatestBuildFn func() error), bool) { - // We use a custom heartbeat routine here instead of `httpapi.Heartbeat` - // because we want to log the agent's last ping time. - var lastPing atomic.Pointer[time.Time] - lastPing.Store(ptr.Ref(time.Now())) // Since the agent initiated the request, assume it's alive. - - go pprof.Do(ctx, pprof.Labels("agent", workspaceAgent.ID.String()), func(ctx context.Context) { - // TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout? - t := time.NewTicker(api.AgentConnectionUpdateFrequency) - defer t.Stop() - - for { - select { - case <-t.C: - case <-ctx.Done(): - return - } +func checkBuildIsLatest(ctx context.Context, db database.Store, build database.WorkspaceBuild) error { + latestBuild, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, build.WorkspaceID) + if err != nil { + return err + } + if build.ID != latestBuild.ID { + return xerrors.New("build is outdated") + } + return nil +} - // We don't need a context that times out here because the ping will - // eventually go through. If the context times out, then other - // websocket read operations will receive an error, obfuscating the - // actual problem. - err := conn.Ping(ctx) - if err != nil { - return - } - lastPing.Store(ptr.Ref(time.Now())) +func (api *API) startAgentWebsocketMonitor(ctx context.Context, + workspaceAgent database.WorkspaceAgent, workspaceBuild database.WorkspaceBuild, + conn *websocket.Conn, +) *agentWebsocketMonitor { + monitor := &agentWebsocketMonitor{ + apiCtx: api.ctx, + workspaceAgent: workspaceAgent, + workspaceBuild: workspaceBuild, + conn: conn, + pingPeriod: api.AgentConnectionUpdateFrequency, + db: api.Database, + replicaID: api.ID, + updater: api, + disconnectTimeout: api.AgentInactiveDisconnectTimeout, + logger: api.Logger.With( + slog.F("workspace_id", workspaceBuild.WorkspaceID), + slog.F("agent_id", workspaceAgent.ID), + ), + } + monitor.init() + monitor.start(ctx) + + return monitor +} + +type workspaceUpdater interface { + publishWorkspaceUpdate(ctx context.Context, workspaceID uuid.UUID) +} + +type pingerCloser interface { + Ping(ctx context.Context) error + Close(code websocket.StatusCode, reason string) error +} + +type agentWebsocketMonitor struct { + apiCtx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + workspaceAgent database.WorkspaceAgent + workspaceBuild database.WorkspaceBuild + conn pingerCloser + db database.Store + replicaID uuid.UUID + updater workspaceUpdater + logger slog.Logger + pingPeriod time.Duration + + // state manipulated by both sendPings() and monitor() goroutines: needs to be threadsafe + lastPing atomic.Pointer[time.Time] + + // state manipulated only by monitor() goroutine: does not need to be threadsafe + firstConnectedAt sql.NullTime + lastConnectedAt sql.NullTime + disconnectedAt sql.NullTime + disconnectTimeout time.Duration +} + +// sendPings sends websocket pings. +// +// We use a custom heartbeat routine here instead of `httpapi.Heartbeat` +// because we want to log the agent's last ping time. +func (m *agentWebsocketMonitor) sendPings(ctx context.Context) { + t := time.NewTicker(m.pingPeriod) + defer t.Stop() + + for { + select { + case <-t.C: + case <-ctx.Done(): + return } + + // We don't need a context that times out here because the ping will + // eventually go through. If the context times out, then other + // websocket read operations will receive an error, obfuscating the + // actual problem. + err := m.conn.Ping(ctx) + if err != nil { + return + } + m.lastPing.Store(ptr.Ref(time.Now())) + } +} + +func (m *agentWebsocketMonitor) updateConnectionTimes(ctx context.Context) error { + //nolint:gocritic // We only update the agent we are minding. + err := m.db.UpdateWorkspaceAgentConnectionByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAgentConnectionByIDParams{ + ID: m.workspaceAgent.ID, + FirstConnectedAt: m.firstConnectedAt, + LastConnectedAt: m.lastConnectedAt, + DisconnectedAt: m.disconnectedAt, + UpdatedAt: dbtime.Now(), + LastConnectedReplicaID: uuid.NullUUID{ + UUID: m.replicaID, + Valid: true, + }, }) + if err != nil { + return xerrors.Errorf("failed to update workspace agent connection times: %w", err) + } + return nil +} - firstConnectedAt := workspaceAgent.FirstConnectedAt - if !firstConnectedAt.Valid { - firstConnectedAt = sql.NullTime{ - Time: dbtime.Now(), +func (m *agentWebsocketMonitor) init() { + now := dbtime.Now() + m.firstConnectedAt = m.workspaceAgent.FirstConnectedAt + if !m.firstConnectedAt.Valid { + m.firstConnectedAt = sql.NullTime{ + Time: now, Valid: true, } } - lastConnectedAt := sql.NullTime{ - Time: dbtime.Now(), + m.lastConnectedAt = sql.NullTime{ + Time: now, Valid: true, } - disconnectedAt := workspaceAgent.DisconnectedAt - updateConnectionTimes := func(ctx context.Context) error { - //nolint:gocritic // We only update ourself. - err := api.Database.UpdateWorkspaceAgentConnectionByID(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAgentConnectionByIDParams{ - ID: workspaceAgent.ID, - FirstConnectedAt: firstConnectedAt, - LastConnectedAt: lastConnectedAt, - DisconnectedAt: disconnectedAt, - UpdatedAt: dbtime.Now(), - LastConnectedReplicaID: uuid.NullUUID{ - UUID: api.ID, - Valid: true, - }, + m.disconnectedAt = m.workspaceAgent.DisconnectedAt + m.lastPing.Store(ptr.Ref(time.Now())) // Since the agent initiated the request, assume it's alive. +} + +func (m *agentWebsocketMonitor) start(ctx context.Context) { + ctx, m.cancel = context.WithCancel(ctx) + m.wg.Add(2) + go pprof.Do(ctx, pprof.Labels("agent", m.workspaceAgent.ID.String()), + func(ctx context.Context) { + defer m.wg.Done() + m.sendPings(ctx) }) - if err != nil { - return err - } - return nil - } + go pprof.Do(ctx, pprof.Labels("agent", m.workspaceAgent.ID.String()), + func(ctx context.Context) { + defer m.wg.Done() + m.monitor(ctx) + }) +} +func (m *agentWebsocketMonitor) monitor(ctx context.Context) { defer func() { // If connection closed then context will be canceled, try to // ensure our final update is sent. By waiting at most the agent @@ -261,17 +339,17 @@ func (api *API) agentConnectionUpdate(ctx context.Context, workspaceAgent databa // Use a system context as the agent has disconnected and that token // may no longer be valid. //nolint:gocritic - ctx, cancel := context.WithTimeout(dbauthz.AsSystemRestricted(api.ctx), api.AgentInactiveDisconnectTimeout) + finalCtx, cancel := context.WithTimeout(dbauthz.AsSystemRestricted(m.apiCtx), m.disconnectTimeout) defer cancel() // Only update timestamp if the disconnect is new. - if !disconnectedAt.Valid { - disconnectedAt = sql.NullTime{ + if !m.disconnectedAt.Valid { + m.disconnectedAt = sql.NullTime{ Time: dbtime.Now(), Valid: true, } } - err := updateConnectionTimes(ctx) + err := m.updateConnectionTimes(finalCtx) if err != nil { // This is a bug with unit tests that cancel the app context and // cause this error log to be generated. We should fix the unit tests @@ -279,66 +357,66 @@ func (api *API) agentConnectionUpdate(ctx context.Context, workspaceAgent databa // // The pq error occurs when the server is shutting down. if !xerrors.Is(err, context.Canceled) && !database.IsQueryCanceledError(err) { - api.Logger.Error(ctx, "failed to update agent disconnect time", + m.logger.Error(finalCtx, "failed to update agent disconnect time", slog.Error(err), - slog.F("workspace_id", workspaceID), ) } } - api.publishWorkspaceUpdate(ctx, workspaceID) + m.updater.publishWorkspaceUpdate(finalCtx, m.workspaceBuild.WorkspaceID) + }() + reason := "disconnect" + defer func() { + m.logger.Debug(ctx, "agent websocket monitor is closing connection", + slog.F("reason", reason)) + _ = m.conn.Close(websocket.StatusGoingAway, reason) }() - err := updateConnectionTimes(ctx) + err := m.updateConnectionTimes(ctx) if err != nil { - _ = conn.Close(websocket.StatusGoingAway, err.Error()) - return nil, false + reason = err.Error() + return } - api.publishWorkspaceUpdate(ctx, workspaceID) - - return func(closeCtx context.Context, ensureLatestBuildFn func() error) { - ticker := time.NewTicker(api.AgentConnectionUpdateFrequency) - defer ticker.Stop() - for { - select { - case <-closeCtx.Done(): - return - case <-ticker.C: - } + m.updater.publishWorkspaceUpdate(ctx, m.workspaceBuild.WorkspaceID) + + ticker := time.NewTicker(m.pingPeriod) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + reason = "canceled" + return + case <-ticker.C: + } - lastPing := *lastPing.Load() - - var connectionStatusChanged bool - if time.Since(lastPing) > api.AgentInactiveDisconnectTimeout { - if !disconnectedAt.Valid { - connectionStatusChanged = true - disconnectedAt = sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - } - } - } else { - connectionStatusChanged = disconnectedAt.Valid - // TODO(mafredri): Should we update it here or allow lastConnectedAt to shadow it? - disconnectedAt = sql.NullTime{} - lastConnectedAt = sql.NullTime{ - Time: dbtime.Now(), - Valid: true, - } - } - err = updateConnectionTimes(ctx) - if err != nil { - _ = conn.Close(websocket.StatusGoingAway, err.Error()) - return - } - if connectionStatusChanged { - api.publishWorkspaceUpdate(ctx, workspaceID) - } - err := ensureLatestBuildFn() - if err != nil { - // Disconnect agents that are no longer valid. - _ = conn.Close(websocket.StatusGoingAway, "") - return - } + lastPing := *m.lastPing.Load() + if time.Since(lastPing) > m.disconnectTimeout { + reason = "ping timeout" + return + } + connectionStatusChanged := m.disconnectedAt.Valid + m.disconnectedAt = sql.NullTime{} + m.lastConnectedAt = sql.NullTime{ + Time: dbtime.Now(), + Valid: true, + } + + err = m.updateConnectionTimes(ctx) + if err != nil { + reason = err.Error() + return + } + if connectionStatusChanged { + m.updater.publishWorkspaceUpdate(ctx, m.workspaceBuild.WorkspaceID) + } + err = checkBuildIsLatest(ctx, m.db, m.workspaceBuild) + if err != nil { + reason = err.Error() + return } - }, true + } +} + +func (m *agentWebsocketMonitor) close() { + m.cancel() + m.wg.Wait() } diff --git a/coderd/workspaceagentsrpc_internal_test.go b/coderd/workspaceagentsrpc_internal_test.go new file mode 100644 index 0000000000000..834de4807d9be --- /dev/null +++ b/coderd/workspaceagentsrpc_internal_test.go @@ -0,0 +1,443 @@ +package coderd + +import ( + "context" + "database/sql" + "fmt" + "sync" + "testing" + "time" + + "github.com/coder/coder/v2/coderd/util/ptr" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "nhooyr.io/websocket" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/testutil" +) + +func TestAgentWebsocketMonitor_ContextCancel(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + now := dbtime.Now() + fConn := &fakePingerCloser{} + ctrl := gomock.NewController(t) + mDB := dbmock.NewMockStore(ctrl) + fUpdater := &fakeUpdater{} + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + agent := database.WorkspaceAgent{ + ID: uuid.New(), + FirstConnectedAt: sql.NullTime{ + Time: now.Add(-time.Minute), + Valid: true, + }, + } + build := database.WorkspaceBuild{ + ID: uuid.New(), + WorkspaceID: uuid.New(), + } + replicaID := uuid.New() + + uut := &agentWebsocketMonitor{ + apiCtx: ctx, + workspaceAgent: agent, + workspaceBuild: build, + conn: fConn, + db: mDB, + replicaID: replicaID, + updater: fUpdater, + logger: logger, + pingPeriod: testutil.IntervalFast, + disconnectTimeout: testutil.WaitShort, + } + uut.init() + + connected := mDB.EXPECT().UpdateWorkspaceAgentConnectionByID( + gomock.Any(), + connectionUpdate(agent.ID, replicaID), + ). + AnyTimes(). + Return(nil) + mDB.EXPECT().UpdateWorkspaceAgentConnectionByID( + gomock.Any(), + connectionUpdate(agent.ID, replicaID, withDisconnected()), + ). + After(connected). + Times(1). + Return(nil) + mDB.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), build.WorkspaceID). + AnyTimes(). + Return(database.WorkspaceBuild{ID: build.ID}, nil) + + closeCtx, cancel := context.WithCancel(ctx) + defer cancel() + done := make(chan struct{}) + go func() { + uut.monitor(closeCtx) + close(done) + }() + // wait a couple intervals, but not long enough for a disconnect + time.Sleep(3 * testutil.IntervalFast) + fConn.requireNotClosed(t) + fUpdater.requireEventuallySomeUpdates(t, build.WorkspaceID) + n := fUpdater.getUpdates() + cancel() + fConn.requireEventuallyClosed(t, websocket.StatusGoingAway, "canceled") + + // make sure we got at least one additional update on close + _ = testutil.RequireRecvCtx(ctx, t, done) + m := fUpdater.getUpdates() + require.Greater(t, m, n) +} + +func TestAgentWebsocketMonitor_PingTimeout(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + now := dbtime.Now() + fConn := &fakePingerCloser{} + ctrl := gomock.NewController(t) + mDB := dbmock.NewMockStore(ctrl) + fUpdater := &fakeUpdater{} + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + agent := database.WorkspaceAgent{ + ID: uuid.New(), + FirstConnectedAt: sql.NullTime{ + Time: now.Add(-time.Minute), + Valid: true, + }, + } + build := database.WorkspaceBuild{ + ID: uuid.New(), + WorkspaceID: uuid.New(), + } + replicaID := uuid.New() + + uut := &agentWebsocketMonitor{ + apiCtx: ctx, + workspaceAgent: agent, + workspaceBuild: build, + conn: fConn, + db: mDB, + replicaID: replicaID, + updater: fUpdater, + logger: logger, + pingPeriod: testutil.IntervalFast, + disconnectTimeout: testutil.WaitShort, + } + uut.init() + // set the last ping to the past, so we go thru the timeout + uut.lastPing.Store(ptr.Ref(now.Add(-time.Hour))) + + connected := mDB.EXPECT().UpdateWorkspaceAgentConnectionByID( + gomock.Any(), + connectionUpdate(agent.ID, replicaID), + ). + AnyTimes(). + Return(nil) + mDB.EXPECT().UpdateWorkspaceAgentConnectionByID( + gomock.Any(), + connectionUpdate(agent.ID, replicaID, withDisconnected()), + ). + After(connected). + Times(1). + Return(nil) + mDB.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), build.WorkspaceID). + AnyTimes(). + Return(database.WorkspaceBuild{ID: build.ID}, nil) + + go uut.monitor(ctx) + fConn.requireEventuallyClosed(t, websocket.StatusGoingAway, "ping timeout") + fUpdater.requireEventuallySomeUpdates(t, build.WorkspaceID) +} + +func TestAgentWebsocketMonitor_BuildOutdated(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + now := dbtime.Now() + fConn := &fakePingerCloser{} + ctrl := gomock.NewController(t) + mDB := dbmock.NewMockStore(ctrl) + fUpdater := &fakeUpdater{} + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + agent := database.WorkspaceAgent{ + ID: uuid.New(), + FirstConnectedAt: sql.NullTime{ + Time: now.Add(-time.Minute), + Valid: true, + }, + } + build := database.WorkspaceBuild{ + ID: uuid.New(), + WorkspaceID: uuid.New(), + } + replicaID := uuid.New() + + uut := &agentWebsocketMonitor{ + apiCtx: ctx, + workspaceAgent: agent, + workspaceBuild: build, + conn: fConn, + db: mDB, + replicaID: replicaID, + updater: fUpdater, + logger: logger, + pingPeriod: testutil.IntervalFast, + disconnectTimeout: testutil.WaitShort, + } + uut.init() + + connected := mDB.EXPECT().UpdateWorkspaceAgentConnectionByID( + gomock.Any(), + connectionUpdate(agent.ID, replicaID), + ). + AnyTimes(). + Return(nil) + mDB.EXPECT().UpdateWorkspaceAgentConnectionByID( + gomock.Any(), + connectionUpdate(agent.ID, replicaID, withDisconnected()), + ). + After(connected). + Times(1). + Return(nil) + + // return a new buildID each time, meaning the connection is outdated + mDB.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), build.WorkspaceID). + AnyTimes(). + Return(database.WorkspaceBuild{ID: uuid.New()}, nil) + + go uut.monitor(ctx) + fConn.requireEventuallyClosed(t, websocket.StatusGoingAway, "build is outdated") + fUpdater.requireEventuallySomeUpdates(t, build.WorkspaceID) +} + +func TestAgentWebsocketMonitor_SendPings(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + t.Cleanup(cancel) + fConn := &fakePingerCloser{} + uut := &agentWebsocketMonitor{ + pingPeriod: testutil.IntervalFast, + conn: fConn, + } + done := make(chan struct{}) + go func() { + uut.sendPings(ctx) + close(done) + }() + fConn.requireEventuallyHasPing(t) + cancel() + <-done + lastPing := uut.lastPing.Load() + require.NotNil(t, lastPing) +} + +func TestAgentWebsocketMonitor_StartClose(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + fConn := &fakePingerCloser{} + now := dbtime.Now() + ctrl := gomock.NewController(t) + mDB := dbmock.NewMockStore(ctrl) + fUpdater := &fakeUpdater{} + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + agent := database.WorkspaceAgent{ + ID: uuid.New(), + FirstConnectedAt: sql.NullTime{ + Time: now.Add(-time.Minute), + Valid: true, + }, + } + build := database.WorkspaceBuild{ + ID: uuid.New(), + WorkspaceID: uuid.New(), + } + replicaID := uuid.New() + uut := &agentWebsocketMonitor{ + apiCtx: ctx, + workspaceAgent: agent, + workspaceBuild: build, + conn: fConn, + db: mDB, + replicaID: replicaID, + updater: fUpdater, + logger: logger, + pingPeriod: testutil.IntervalFast, + disconnectTimeout: testutil.WaitShort, + } + + connected := mDB.EXPECT().UpdateWorkspaceAgentConnectionByID( + gomock.Any(), + connectionUpdate(agent.ID, replicaID), + ). + AnyTimes(). + Return(nil) + mDB.EXPECT().UpdateWorkspaceAgentConnectionByID( + gomock.Any(), + connectionUpdate(agent.ID, replicaID, withDisconnected()), + ). + After(connected). + Times(1). + Return(nil) + mDB.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), build.WorkspaceID). + AnyTimes(). + Return(database.WorkspaceBuild{ID: build.ID}, nil) + + uut.start(ctx) + closed := make(chan struct{}) + go func() { + uut.close() + close(closed) + }() + _ = testutil.RequireRecvCtx(ctx, t, closed) +} + +type fakePingerCloser struct { + sync.Mutex + pings []time.Time + code websocket.StatusCode + reason string + closed bool +} + +func (f *fakePingerCloser) Ping(context.Context) error { + f.Lock() + defer f.Unlock() + f.pings = append(f.pings, time.Now()) + return nil +} + +func (f *fakePingerCloser) Close(code websocket.StatusCode, reason string) error { + f.Lock() + defer f.Unlock() + if f.closed { + return nil + } + f.closed = true + f.code = code + f.reason = reason + return nil +} + +func (f *fakePingerCloser) requireNotClosed(t *testing.T) { + f.Lock() + defer f.Unlock() + require.False(t, f.closed) +} + +func (f *fakePingerCloser) requireEventuallyClosed(t *testing.T, code websocket.StatusCode, reason string) { + require.Eventually(t, func() bool { + f.Lock() + defer f.Unlock() + return f.closed + }, testutil.WaitShort, testutil.IntervalFast) + f.Lock() + defer f.Unlock() + require.Equal(t, code, f.code) + require.Equal(t, reason, f.reason) +} + +func (f *fakePingerCloser) requireEventuallyHasPing(t *testing.T) { + require.Eventually(t, func() bool { + f.Lock() + defer f.Unlock() + return len(f.pings) > 0 + }, testutil.WaitShort, testutil.IntervalFast) +} + +type fakeUpdater struct { + sync.Mutex + updates []uuid.UUID +} + +func (f *fakeUpdater) publishWorkspaceUpdate(_ context.Context, workspaceID uuid.UUID) { + f.Lock() + defer f.Unlock() + f.updates = append(f.updates, workspaceID) +} + +func (f *fakeUpdater) requireEventuallySomeUpdates(t *testing.T, workspaceID uuid.UUID) { + require.Eventually(t, func() bool { + f.Lock() + defer f.Unlock() + return len(f.updates) >= 1 + }, testutil.WaitShort, testutil.IntervalFast) + + f.Lock() + defer f.Unlock() + for _, u := range f.updates { + require.Equal(t, workspaceID, u) + } +} + +func (f *fakeUpdater) getUpdates() int { + f.Lock() + defer f.Unlock() + return len(f.updates) +} + +type connectionUpdateMatcher struct { + agentID uuid.UUID + replicaID uuid.UUID + disconnected bool +} + +type connectionUpdateMatcherOption func(m connectionUpdateMatcher) connectionUpdateMatcher + +func connectionUpdate(id, replica uuid.UUID, opts ...connectionUpdateMatcherOption) connectionUpdateMatcher { + m := connectionUpdateMatcher{ + agentID: id, + replicaID: replica, + } + for _, opt := range opts { + m = opt(m) + } + return m +} + +func withDisconnected() connectionUpdateMatcherOption { + return func(m connectionUpdateMatcher) connectionUpdateMatcher { + m.disconnected = true + return m + } +} + +func (m connectionUpdateMatcher) Matches(x interface{}) bool { + args, ok := x.(database.UpdateWorkspaceAgentConnectionByIDParams) + if !ok { + return false + } + if args.ID != m.agentID { + return false + } + if !args.LastConnectedReplicaID.Valid { + return false + } + if args.LastConnectedReplicaID.UUID != m.replicaID { + return false + } + if args.DisconnectedAt.Valid != m.disconnected { + return false + } + return true +} + +func (m connectionUpdateMatcher) String() string { + return fmt.Sprintf("{agent=%s, replica=%s, disconnected=%t}", + m.agentID.String(), m.replicaID.String(), m.disconnected) +} + +func (connectionUpdateMatcher) Got(x interface{}) string { + args, ok := x.(database.UpdateWorkspaceAgentConnectionByIDParams) + if !ok { + return fmt.Sprintf("type=%T", x) + } + return fmt.Sprintf("{agent=%s, replica=%s, disconnected=%t}", + args.ID, args.LastConnectedReplicaID.UUID, args.DisconnectedAt.Valid) +} diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index a523c586faa4c..b519bc2a29028 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -3,7 +3,6 @@ package coderd import ( "context" "database/sql" - "fmt" "net/http" "net/url" "strings" @@ -19,6 +18,7 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/workspaceapps" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" "github.com/coder/coder/v2/codersdk" ) @@ -31,13 +31,8 @@ import ( // @Router /applications/host [get] // @Deprecated use api/v2/regions and see the primary proxy. func (api *API) appHost(rw http.ResponseWriter, r *http.Request) { - host := api.AppHostname - if host != "" && api.AccessURL.Port() != "" { - host += fmt.Sprintf(":%s", api.AccessURL.Port()) - } - httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AppHostResponse{ - Host: host, + Host: appurl.SubdomainAppHost(api.AppHostname, api.AccessURL), }) } @@ -169,7 +164,7 @@ func (api *API) ValidWorkspaceAppHostname(ctx context.Context, host string, opts } if opts.AllowPrimaryWildcard && api.AppHostnameRegex != nil { - _, ok := httpapi.ExecuteHostnamePattern(api.AppHostnameRegex, host) + _, ok := appurl.ExecuteHostnamePattern(api.AppHostnameRegex, host) if ok { // Force the redirect URI to have the same scheme as the access URL // for security purposes. diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index 166f3ba137fe3..2c4963060b360 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -26,6 +26,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/codersdk" @@ -64,6 +65,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // reconnecting-pty proxy server we want to test is mounted. client := appDetails.AppClient(t) testReconnectingPTY(ctx, t, client, appDetails.Agent.ID, "") + assertWorkspaceLastUsedAtUpdated(t, appDetails) }) t.Run("SignedTokenQueryParameter", func(t *testing.T) { @@ -92,6 +94,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // Make an unauthenticated client. unauthedAppClient := codersdk.New(appDetails.AppClient(t).URL) testReconnectingPTY(ctx, t, unauthedAppClient, appDetails.Agent.ID, issueRes.SignedToken) + assertWorkspaceLastUsedAtUpdated(t, appDetails) }) }) @@ -117,6 +120,9 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Contains(t, string(body), "Path-based applications are disabled") + // Even though path-based apps are disabled, the request should indicate + // that the workspace was used. + assertWorkspaceLastUsedAtNotUpdated(t, appDetails) }) t.Run("LoginWithoutAuthOnPrimary", func(t *testing.T) { @@ -142,6 +148,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.NoError(t, err) require.True(t, loc.Query().Has("message")) require.True(t, loc.Query().Has("redirect")) + assertWorkspaceLastUsedAtUpdated(t, appDetails) }) t.Run("LoginWithoutAuthOnProxy", func(t *testing.T) { @@ -179,6 +186,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // request is getting stripped. require.Equal(t, u.Path, redirectURI.Path+"/") require.Equal(t, u.RawQuery, redirectURI.RawQuery) + assertWorkspaceLastUsedAtUpdated(t, appDetails) }) t.Run("NoAccessShould404", func(t *testing.T) { @@ -195,6 +203,8 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusNotFound, resp.StatusCode) + // TODO(cian): A blocked request should not count as workspace usage. + // assertWorkspaceLastUsedAtNotUpdated(t, appDetails.AppClient(t), appDetails) }) t.Run("RedirectsWithSlash", func(t *testing.T) { @@ -209,6 +219,8 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + // TODO(cian): The initial redirect should not count as workspace usage. + // assertWorkspaceLastUsedAtNotUpdated(t, appDetails.AppClient(t), appDetails) }) t.Run("RedirectsWithQuery", func(t *testing.T) { @@ -226,6 +238,8 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { loc, err := resp.Location() require.NoError(t, err) require.Equal(t, proxyTestAppQuery, loc.RawQuery) + // TODO(cian): The initial redirect should not count as workspace usage. + // assertWorkspaceLastUsedAtNotUpdated(t, appDetails.AppClient(t), appDetails) }) t.Run("Proxies", func(t *testing.T) { @@ -267,6 +281,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.NoError(t, err) require.Equal(t, proxyTestAppBody, string(body)) require.Equal(t, http.StatusOK, resp.StatusCode) + assertWorkspaceLastUsedAtUpdated(t, appDetails) }) t.Run("ProxiesHTTPS", func(t *testing.T) { @@ -312,6 +327,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.NoError(t, err) require.Equal(t, proxyTestAppBody, string(body)) require.Equal(t, http.StatusOK, resp.StatusCode) + assertWorkspaceLastUsedAtUpdated(t, appDetails) }) t.Run("BlocksMe", func(t *testing.T) { @@ -331,6 +347,8 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Contains(t, string(body), "must be accessed with the full username, not @me") + // TODO(cian): A blocked request should not count as workspace usage. + // assertWorkspaceLastUsedAtNotUpdated(t, appDetails.AppClient(t), appDetails) }) t.Run("ForwardsIP", func(t *testing.T) { @@ -349,6 +367,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.Equal(t, proxyTestAppBody, string(body)) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, "1.1.1.1,127.0.0.1", resp.Header.Get("X-Forwarded-For")) + assertWorkspaceLastUsedAtUpdated(t, appDetails) }) t.Run("ProxyError", func(t *testing.T) { @@ -361,6 +380,9 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusBadGateway, resp.StatusCode) + // An valid authenticated attempt to access a workspace app + // should count as usage regardless of success. + assertWorkspaceLastUsedAtUpdated(t, appDetails) }) t.Run("NoProxyPort", func(t *testing.T) { @@ -375,6 +397,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { // TODO(@deansheather): This should be 400. There's a todo in the // resolve request code to fix this. require.Equal(t, http.StatusInternalServerError, resp.StatusCode) + assertWorkspaceLastUsedAtUpdated(t, appDetails) }) }) @@ -940,6 +963,38 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { require.Equal(t, http.StatusOK, resp.StatusCode) }) + t.Run("WildcardPortOK", func(t *testing.T) { + t.Parallel() + + // Manually specifying a port should override the access url port on + // the app host. + appDetails := setupProxyTest(t, &DeploymentOptions{ + // Just throw both the wsproxy and primary to same url. + AppHost: "*.test.coder.com:4444", + PrimaryAppHost: "*.test.coder.com:4444", + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + u := appDetails.SubdomainAppURL(appDetails.Apps.Owner) + t.Logf("url: %s", u) + require.Equal(t, "4444", u.Port(), "port should be 4444") + + // Assert the api response the UI uses has the port. + apphost, err := appDetails.SDKClient.AppHost(ctx) + require.NoError(t, err) + require.Equal(t, "*.test.coder.com:4444", apphost.Host, "apphost has port") + + resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil) + require.NoError(t, err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, proxyTestAppBody, string(body)) + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + t.Run("SuffixWildcardNotMatch", func(t *testing.T) { t.Parallel() @@ -1430,16 +1485,12 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { t.Run("ReportStats", func(t *testing.T) { t.Parallel() - flush := make(chan chan<- struct{}, 1) - reporter := &fakeStatsReporter{} appDetails := setupProxyTest(t, &DeploymentOptions{ StatsCollectorOptions: workspaceapps.StatsCollectorOptions{ Reporter: reporter, ReportInterval: time.Hour, RollupWindow: time.Minute, - - Flush: flush, }, }) @@ -1457,10 +1508,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { var stats []workspaceapps.StatsReport require.Eventually(t, func() bool { // Keep flushing until we get a non-empty stats report. - flushDone := make(chan struct{}, 1) - flush <- flushDone - <-flushDone - + appDetails.FlushStats() stats = reporter.stats() return len(stats) > 0 }, testutil.WaitLong, testutil.IntervalFast, "stats not reported") @@ -1469,6 +1517,24 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { assert.Equal(t, "test-app-owner", stats[0].SlugOrPort) assert.Equal(t, 1, stats[0].Requests) }) + + t.Run("WorkspaceOffline", func(t *testing.T) { + t.Parallel() + + appDetails := setupProxyTest(t, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _ = coderdtest.MustTransitionWorkspace(t, appDetails.SDKClient, appDetails.Workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + u := appDetails.PathAppURL(appDetails.Apps.Owner) + resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, u.String(), nil) + require.NoError(t, err) + _ = resp.Body.Close() + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) + }) } type fakeStatsReporter struct { @@ -1549,3 +1615,28 @@ func testReconnectingPTY(ctx context.Context, t *testing.T, client *codersdk.Cli // Ensure the connection closes. require.ErrorIs(t, tr.ReadUntil(ctx, nil), io.EOF) } + +// Accessing an app should update the workspace's LastUsedAt. +// NOTE: Despite our efforts with the flush channel, this is inherently racy. +func assertWorkspaceLastUsedAtUpdated(t testing.TB, details *Details) { + t.Helper() + + // Wait for stats to fully flush. + require.Eventually(t, func() bool { + details.FlushStats() + ws, err := details.SDKClient.Workspace(context.Background(), details.Workspace.ID) + assert.NoError(t, err) + return ws.LastUsedAt.After(details.Workspace.LastUsedAt) + }, testutil.WaitShort, testutil.IntervalMedium, "workspace LastUsedAt not updated when it should have been") +} + +// Except when it sometimes shouldn't (e.g. no access) +// NOTE: Despite our efforts with the flush channel, this is inherently racy. +func assertWorkspaceLastUsedAtNotUpdated(t testing.TB, details *Details) { + t.Helper() + + details.FlushStats() + ws, err := details.SDKClient.Workspace(context.Background(), details.Workspace.ID) + require.NoError(t, err) + require.Equal(t, ws.LastUsedAt, details.Workspace.LastUsedAt, "workspace LastUsedAt updated when it should not have been") +} diff --git a/coderd/workspaceapps/apptest/setup.go b/coderd/workspaceapps/apptest/setup.go index 534a35398f653..99d91c2e20614 100644 --- a/coderd/workspaceapps/apptest/setup.go +++ b/coderd/workspaceapps/apptest/setup.go @@ -21,8 +21,8 @@ import ( "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/agent" "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/workspaceapps" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/cryptorand" @@ -47,6 +47,7 @@ const ( // DeploymentOptions are the options for creating a *Deployment with a // DeploymentFactory. type DeploymentOptions struct { + PrimaryAppHost string AppHost string DisablePathApps bool DisableSubdomainApps bool @@ -71,6 +72,7 @@ type Deployment struct { SDKClient *codersdk.Client FirstUser codersdk.CreateFirstUserResponse PathAppBaseURL *url.URL + FlushStats func() } // DeploymentFactory generates a deployment with an API client, a path base URL, @@ -145,7 +147,7 @@ func (d *Details) PathAppURL(app App) *url.URL { // SubdomainAppURL returns the URL for the given subdomain app. func (d *Details) SubdomainAppURL(app App) *url.URL { - appHost := httpapi.ApplicationURL{ + appHost := appurl.ApplicationURL{ Prefix: app.Prefix, AppSlugOrPort: app.AppSlugOrPort, AgentName: app.AgentName, @@ -369,7 +371,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U for _, app := range workspaceBuild.Resources[0].Agents[0].Apps { require.True(t, app.Subdomain) - appURL := httpapi.ApplicationURL{ + appURL := appurl.ApplicationURL{ Prefix: "", // findProtoApp is needed as the order of apps returned from PG database // is not guaranteed. @@ -398,7 +400,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U manifest, err := agentClient.Manifest(appHostCtx) require.NoError(t, err) - appHost := httpapi.ApplicationURL{ + appHost := appurl.ApplicationURL{ Prefix: "", AppSlugOrPort: "{{port}}", AgentName: proxyTestAgentName, @@ -406,7 +408,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U Username: me.Username, } proxyURL := "http://" + appHost.String() + strings.ReplaceAll(primaryAppHost.Host, "*", "") - require.Equal(t, proxyURL, manifest.VSCodePortProxyURI) + require.Equal(t, manifest.VSCodePortProxyURI, proxyURL) } agentCloser := agent.New(agent.Options{ Client: agentClient, diff --git a/coderd/httpapi/url.go b/coderd/workspaceapps/appurl/appurl.go similarity index 77% rename from coderd/httpapi/url.go rename to coderd/workspaceapps/appurl/appurl.go index bbdb9af1802d8..4daa05a7e3664 100644 --- a/coderd/httpapi/url.go +++ b/coderd/workspaceapps/appurl/appurl.go @@ -1,8 +1,9 @@ -package httpapi +package appurl import ( "fmt" "net" + "net/url" "regexp" "strings" @@ -10,8 +11,8 @@ import ( ) var ( - // Remove the "starts with" and "ends with" regex components. - nameRegex = strings.Trim(UsernameValidRegex.String(), "^$") + // nameRegex is the same as our UsernameRegex without the ^ and $. + nameRegex = "[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*" appURL = regexp.MustCompile(fmt.Sprintf( // {PORT/APP_SLUG}--{AGENT_NAME}--{WORKSPACE_NAME}--{USERNAME} `^(?P
Field | Tracked |
---|---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
Field | Tracked |
---|---|
active_version_id | true |
allow_user_autostart | true |
allow_user_autostop | true |
allow_user_cancel_workspace_jobs | true |
autostart_block_days_of_week | true |
autostop_requirement_days_of_week | true |
autostop_requirement_weeks | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
default_ttl | true |
deleted | false |
deprecated | true |
description | true |
display_name | true |
failure_ttl | true |
group_acl | true |
icon | true |
id | true |
max_ttl | true |
name | true |
organization_id | false |
provisioner | true |
require_active_version | true |
time_til_dormant | true |
time_til_dormant_autodelete | true |
updated_at | false |
use_max_ttl | true |
user_acl | true |
Field | Tracked |
---|---|
archived | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
external_auth_providers | false |
id | true |
job_id | false |
message | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
Field | Tracked |
---|---|
avatar_url | false |
created_at | false |
deleted | true |
true | |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
theme_preference | false |
updated_at | false |
username | true |
Field | Tracked |
---|---|
avatar_url | false |
created_at | false |
deleted | true |
true | |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | true |
name | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
theme_preference | false |
updated_at | false |
username | true |
Field | Tracked |
---|---|
automatic_updates | true |
autostart_schedule | true |
created_at | false |
deleted | false |
deleting_at | true |
dormant_at | true |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
Field | Tracked |
---|---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_by_avatar_url | false |
initiator_by_username | false |
initiator_id | false |
job_id | false |
max_deadline | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
Field | Tracked |
---|---|
created_at | true |
deleted | false |
derp_enabled | true |
derp_only | true |
display_name | true |
icon | true |
id | true |
name | true |
region_id | true |
token_hashed_secret | true |
updated_at | false |
url | true |
version | true |
wildcard_hostname | true |
login
](./cli/login.md) | Authenticate with Coder deployment |
| [logout
](./cli/logout.md) | Unauthenticate your local session |
| [netcheck
](./cli/netcheck.md) | Print network debug information for DERP and STUN |
+| [open
](./cli/open.md) | Open a workspace |
| [ping
](./cli/ping.md) | Ping a workspace |
| [port-forward
](./cli/port-forward.md) | Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R". |
| [provisionerd
](./cli/provisionerd.md) | Manage provisioner daemons |
diff --git a/docs/cli/config-ssh.md b/docs/cli/config-ssh.md
index 6fece81c58693..5f1261ff676d4 100644
--- a/docs/cli/config-ssh.md
+++ b/docs/cli/config-ssh.md
@@ -93,8 +93,8 @@ Specifies whether or not to keep options from previous run of config-ssh.
### --wait
| | |
-| ----------- | ---------------------------------- | --- | ------------ |
-| Type | enum[yes | no | auto]
|
+| ----------- | ---------------------------------- |
+| Type | enum[yes\|no\|auto]
|
| Environment | $CODER_CONFIGSSH_WAIT
|
| Default | auto
|
diff --git a/docs/cli/list.md b/docs/cli/list.md
index ef8ef2fcaad16..9681d32c1a5a4 100644
--- a/docs/cli/list.md
+++ b/docs/cli/list.md
@@ -26,12 +26,12 @@ Specifies whether all workspaces will be listed or not.
### -c, --column
-| | |
-| ------- | ---------------------------------------------------------------------------------------- |
-| Type | string-array
|
-| Default | workspace,template,status,healthy,last built,outdated,starts at,stops after
|
+| | |
+| ------- | -------------------------------------------------------------------------------------------------------- |
+| Type | string-array
|
+| Default | workspace,template,status,healthy,last built,current version,outdated,starts at,stops after
|
-Columns to display in table output. Available columns: workspace, template, status, healthy, last built, outdated, starts at, starts next, stops after, stops next, daily cost.
+Columns to display in table output. Available columns: workspace, template, status, healthy, last built, current version, outdated, starts at, starts next, stops after, stops next, daily cost.
### -o, --output
diff --git a/docs/cli/open.md b/docs/cli/open.md
new file mode 100644
index 0000000000000..8b5f5beef4c03
--- /dev/null
+++ b/docs/cli/open.md
@@ -0,0 +1,17 @@
+
+
+# open
+
+Open a workspace
+
+## Usage
+
+```console
+coder open
+```
+
+## Subcommands
+
+| Name | Purpose |
+| --------------------------------------- | ----------------------------------- |
+| [vscode
](./open_vscode.md) | Open a workspace in VS Code Desktop |
diff --git a/docs/cli/open_vscode.md b/docs/cli/open_vscode.md
new file mode 100644
index 0000000000000..23e4d85d604b6
--- /dev/null
+++ b/docs/cli/open_vscode.md
@@ -0,0 +1,22 @@
+
+
+# open vscode
+
+Open a workspace in VS Code Desktop
+
+## Usage
+
+```console
+coder open vscode [flags] bool
|
+| Environment | $CODER_OPEN_VSCODE_GENERATE_TOKEN
|
+
+Generate an auth token and include it in the vscode:// URI. This is for automagical configuration of VS Code Desktop and not needed if already configured. This flag does not need to be specified when running this command on a local machine unless automatic open fails.
diff --git a/docs/cli/server.md b/docs/cli/server.md
index a0c4aad6e97ba..77f6d600e372c 100644
--- a/docs/cli/server.md
+++ b/docs/cli/server.md
@@ -918,6 +918,16 @@ Controls if the 'Strict-Transport-Security' header is set on all static file res
Two optional fields can be set in the Strict-Transport-Security header; 'includeSubDomains' and 'preload'. The 'strict-transport-security' flag must be set to a non-zero value for these options to be used.
+### --support-links
+
+| | |
+| ----------- | ------------------------------------------ |
+| Type | struct[[]codersdk.LinkConfig]
|
+| Environment | $CODER_SUPPORT_LINKS
|
+| YAML | supportLinks
|
+
+Support links to display in the top right drop down menu.
+
### --tls-address
| | |
@@ -1088,7 +1098,7 @@ The renderer to use when opening a web terminal. Valid values are 'canvas', 'web
| | |
| ----------- | ----------------------------------------- |
-| Type | url
|
+| Type | string
|
| Environment | $CODER_WILDCARD_ACCESS_URL
|
| YAML | networking.wildcardAccessURL
|
diff --git a/docs/cli/speedtest.md b/docs/cli/speedtest.md
index d06cdd77367cd..0a351fde5d9df 100644
--- a/docs/cli/speedtest.md
+++ b/docs/cli/speedtest.md
@@ -22,10 +22,10 @@ Specifies whether to wait for a direct connection before testing speed.
### --direction
-| | |
-| ------- | ----------------- | ------------ |
-| Type | enum[up | down]
|
-| Default | down
|
+| | |
+| ------- | --------------------------- |
+| Type | enum[up\|down]
|
+| Default | down
|
Specifies whether to run in reverse mode where the client receives and the server sends.
diff --git a/docs/cli/ssh.md b/docs/cli/ssh.md
index 264b36a89583d..34762d5b2bd59 100644
--- a/docs/cli/ssh.md
+++ b/docs/cli/ssh.md
@@ -71,7 +71,7 @@ Enter workspace immediately after the agent has connected. This is the default i
| | |
| ----------- | -------------------------------------- |
-| Type | string
|
+| Type | string-array
|
| Environment | $CODER_SSH_REMOTE_FORWARD
|
Enable remote port forwarding (remote_port:local_address:local_port).
@@ -87,11 +87,11 @@ Specifies whether to emit SSH output over stdin/stdout.
### --wait
-| | |
-| ----------- | ---------------------------- | --- | ------------ |
-| Type | enum[yes | no | auto]
|
-| Environment | $CODER_SSH_WAIT
|
-| Default | auto
|
+| | |
+| ----------- | -------------------------------- |
+| Type | enum[yes\|no\|auto]
|
+| Environment | $CODER_SSH_WAIT
|
+| Default | auto
|
Specifies whether or not to wait for the startup script to finish executing. Auto means that the agent startup script behavior configured in the workspace template is used.
diff --git a/docs/cli/stat_disk.md b/docs/cli/stat_disk.md
index be4e8a429e6b2..8585f6faa5a3e 100644
--- a/docs/cli/stat_disk.md
+++ b/docs/cli/stat_disk.md
@@ -32,9 +32,9 @@ Path for which to check disk usage.
### --prefix
-| | |
-| ------- | --------------- | --- | --- | ---------- |
-| Type | enum[Ki | Mi | Gi | Ti]
|
-| Default | Gi
|
+| | |
+| ------- | --------------------------------- |
+| Type | enum[Ki\|Mi\|Gi\|Ti]
|
+| Default | Gi
|
SI Prefix for disk measurement.
diff --git a/docs/cli/stat_mem.md b/docs/cli/stat_mem.md
index f76e2901f9d13..6594316753c30 100644
--- a/docs/cli/stat_mem.md
+++ b/docs/cli/stat_mem.md
@@ -31,9 +31,9 @@ Output format. Available formats: text, json.
### --prefix
-| | |
-| ------- | --------------- | --- | --- | ---------- |
-| Type | enum[Ki | Mi | Gi | Ti]
|
-| Default | Gi
|
+| | |
+| ------- | --------------------------------- |
+| Type | enum[Ki\|Mi\|Gi\|Ti]
|
+| Default | Gi
|
SI Prefix for memory measurement.
diff --git a/docs/cli/templates.md b/docs/cli/templates.md
index 4a5b60161114f..0226bd5a60d92 100644
--- a/docs/cli/templates.md
+++ b/docs/cli/templates.md
@@ -18,29 +18,26 @@ coder templates
```console
Templates are written in standard Terraform and describe the infrastructure for workspaces
- - Create a template for developers to create workspaces:
-
- $ coder templates create
-
- Make changes to your template, and plan the changes:
$ coder templates plan my-template
- - Push an update to the template. Your developers can update their workspaces:
+ - Create or push an update to the template. Your developers can update their
+workspaces:
$ coder templates push my-template
```
## Subcommands
-| Name | Purpose |
-| ------------------------------------------------ | ------------------------------------------------------------------------------ |
-| [archive
](./templates_archive.md) | Archive unused or failed template versions from a given template(s) |
-| [create
](./templates_create.md) | Create a template from the current directory or as specified by flag |
-| [delete
](./templates_delete.md) | Delete templates |
-| [edit
](./templates_edit.md) | Edit the metadata of a template by name. |
-| [init
](./templates_init.md) | Get started with a templated template. |
-| [list
](./templates_list.md) | List all the templates available for the organization |
-| [pull
](./templates_pull.md) | Download the active, latest, or specified version of a template to a path. |
-| [push
](./templates_push.md) | Push a new template version from the current directory or as specified by flag |
-| [versions
](./templates_versions.md) | Manage different versions of the specified template |
+| Name | Purpose |
+| ------------------------------------------------ | -------------------------------------------------------------------------------- |
+| [archive
](./templates_archive.md) | Archive unused or failed template versions from a given template(s) |
+| [create
](./templates_create.md) | DEPRECATED: Create a template from the current directory or as specified by flag |
+| [delete
](./templates_delete.md) | Delete templates |
+| [edit
](./templates_edit.md) | Edit the metadata of a template by name. |
+| [init
](./templates_init.md) | Get started with a templated template. |
+| [list
](./templates_list.md) | List all the templates available for the organization |
+| [pull
](./templates_pull.md) | Download the active, latest, or specified version of a template to a path. |
+| [push
](./templates_push.md) | Create or update a template from the current directory or as specified by flag |
+| [versions
](./templates_versions.md) | Manage different versions of the specified template |
diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md
index 9535e2f12e6da..eacac108501db 100644
--- a/docs/cli/templates_create.md
+++ b/docs/cli/templates_create.md
@@ -2,7 +2,7 @@
# templates create
-Create a template from the current directory or as specified by flag
+DEPRECATED: Create a template from the current directory or as specified by flag
## Usage
diff --git a/docs/cli/templates_edit.md b/docs/cli/templates_edit.md
index 12577cbcaba23..ff73c2828eb83 100644
--- a/docs/cli/templates_edit.md
+++ b/docs/cli/templates_edit.md
@@ -130,6 +130,15 @@ Edit the template maximum time before shutdown - workspaces created from this te
Edit the template name.
+### --private
+
+| | |
+| ------- | ------------------ |
+| Type | bool
|
+| Default | false
|
+
+Disable the default behavior of granting template access to the 'everyone' group. The template permissions must be updated to allow non-admin users to use this template.
+
### --require-active-version
| | |
diff --git a/docs/cli/templates_init.md b/docs/cli/templates_init.md
index d26a8cb857f81..06b4c849f4698 100644
--- a/docs/cli/templates_init.md
+++ b/docs/cli/templates_init.md
@@ -14,8 +14,8 @@ coder templates init [flags] [directory]
### --id
-| | |
-| ---- | --------------------------- | --------- | ----------- | ----------- | -------- | ------ | ---------------- | --------- | ---------------- | ----------- | ---------- | -------------------- |
-| Type | enum[aws-devcontainer | aws-linux | aws-windows | azure-linux | do-linux | docker | gcp-devcontainer | gcp-linux | gcp-vm-container | gcp-windows | kubernetes | nomad-docker]
|
+| | |
+| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Type | enum[aws-devcontainer\|aws-linux\|aws-windows\|azure-linux\|do-linux\|docker\|gcp-devcontainer\|gcp-linux\|gcp-vm-container\|gcp-windows\|kubernetes\|nomad-docker]
|
Specify a given example template by ID.
diff --git a/docs/cli/templates_push.md b/docs/cli/templates_push.md
index bfa73fdad1151..d7a6cb7043989 100644
--- a/docs/cli/templates_push.md
+++ b/docs/cli/templates_push.md
@@ -2,7 +2,7 @@
# templates push
-Push a new template version from the current directory or as specified by flag
+Create or update a template from the current directory or as specified by flag
## Usage
@@ -29,15 +29,6 @@ Whether the new template will be marked active.
Always prompt all parameters. Does not pull parameter values from active template version.
-### --create
-
-| | |
-| ------- | ------------------ |
-| Type | bool
|
-| Default | false
|
-
-Create the template if it does not exist.
-
### -d, --directory
| | |
diff --git a/docs/contributing/frontend.md b/docs/contributing/frontend.md
index 965d80e842d6c..e24fadf8f53f5 100644
--- a/docs/contributing/frontend.md
+++ b/docs/contributing/frontend.md
@@ -99,11 +99,45 @@ the api/queries folder when it is possible.
### Where to fetch data
-Finding the right place to fetch data in React apps is the million-dollar
-question, but we decided to make it only in the page components and pass the
-props down to the views. This makes it easier to find where data is being loaded
-and easy to test using Storybook. So you will see components like `UsersPage`
-and `UsersPageView`.
+In the past, our approach involved creating separate components for page and
+view, where the page component served as a container responsible for fetching
+data and passing it down to the view.
+
+For instance, when developing a page to display users, we would have a
+`UsersPage` component with a corresponding `UsersPageView`. The `UsersPage`
+would handle API calls, while the `UsersPageView` managed the presentational
+logic.
+
+Over time, however, we encountered challenges with this approach, particularly
+in terms of excessive props drilling. To address this, we opted to fetch data in
+proximity to its usage. Taking the example of displaying users, in the past, if
+we were creating a header component for that page, we would have needed to fetch
+the data in the page component and pass it down through the hierarchy
+(`UsersPage -> UsersPageView -> UsersHeader`). Now, with libraries such as
+`react-query`, data fetching can be performed directly in the `UsersHeader`
+component, allowing UI elements to declare and consume their data-fetching
+dependencies directly, while preventing duplicate server requests
+([more info](https://github.com/TanStack/query/discussions/608#discussioncomment-29735)).
+
+To simplify visual testing of scenarios where components are responsible for
+fetching data, you can easily set the queries' value using `parameters.queries`
+within the component's story.
+
+```tsx
+export const WithQuota: Story = {
+ parameters: {
+ queries: [
+ {
+ key: getWorkspaceQuotaQueryKey(MockUser.username),
+ data: {
+ credits_consumed: 2,
+ budget: 40,
+ },
+ },
+ ],
+ },
+};
+```
### API
@@ -237,13 +271,16 @@ another page, you should probably consider using the **E2E** approach.
### Visual testing
-Test components without user interaction like testing if a page view is rendered
-correctly depending on some parameters, if the button is showing a spinner if
-the `loading` props are passing, etc. This should always be your first option
-since it is way easier to maintain. For this, we use
+We use visual tests to test components without user interaction like testing if
+a page/component is rendered correctly depending on some parameters, if a button
+is showing a spinner, if `loading` props are passed correctly, etc. This should
+always be your first option since it is way easier to maintain. For this, we use
[Storybook](https://storybook.js.org/) and
[Chromatic](https://www.chromatic.com/).
+> ℹ️ To learn more about testing components that fetch API data, refer to the
+> [**Where to fetch data**](#where-to-fetch-data) section.
+
### What should I test?
Choosing what to test is not always easy since there are a lot of flows and a
diff --git a/docs/faqs.md b/docs/faqs.md
index 000109ecf06a1..7a599ca7a9d3e 100644
--- a/docs/faqs.md
+++ b/docs/faqs.md
@@ -4,7 +4,8 @@ Frequently asked questions on Coder OSS and Enterprise deployments. These FAQs
come from our community and enterprise customers, feel free to
[contribute to this page](https://github.com/coder/coder/edit/main/docs/faqs.md).
-## How do I add an enterprise license?
+-**Before you install** -If you would like your workspaces to be able to run Docker, we recommend that you install Sysbox before proceeding. - -As part of the Sysbox installation you will be required to remove all existing -Docker containers including containers used by Coder workspaces. Installing -Sysbox ahead of time will reduce disruption to your Coder instance. - -- ## Requirements Docker is required. See the @@ -24,7 +14,7 @@ Docker is required. See the For proof-of-concept deployments, you can run a complete Coder instance with the following command. -```console +```shell export CODER_DATA=$HOME/.config/coderv2-docker export DOCKER_GROUP=$(getent group docker | cut -d: -f3) mkdir -p $CODER_DATA @@ -43,13 +33,15 @@ systems `/var/run/docker.sock` is not group writeable or does not belong to the Coder configuration is defined via environment variables. Learn more about Coder's [configuration options](../admin/configure.md). -## Run Coder with access URL and external PostgreSQL (recommended) +
+Before you install +If you would like your workspaces to be able to run Docker, we recommend that you install Sysbox before proceeding. + +As part of the Sysbox installation you will be required to remove all existing +Docker containers including containers used by Coder workspaces. Installing +Sysbox ahead of time will reduce disruption to your Coder instance. + ++ ## Instructions 1. Run Coder with Docker. - ```console + ```shell export CODER_DATA=$HOME/.config/coderv2-docker export DOCKER_GROUP=$(getent group docker | cut -d: -f3) mkdir -p $CODER_DATA @@ -37,7 +46,7 @@ Coder with Docker has the following advantages: 1. In new terminal, [install Coder](../install/) in order to connect to your deployment through the CLI. - ```console + ```shell curl -L https://coder.com/install.sh | sh ``` @@ -47,12 +56,12 @@ Coder with Docker has the following advantages: 1. Pull the "Docker" example template using the interactive `coder templates init`: - ```console + ```shell coder templates init cd docker ``` -1. Push up the template with `coder templates create` +1. Push up the template with `coder templates push` 1. Open the dashboard in your browser to create your first workspace: diff --git a/docs/platforms/jfrog.md b/docs/platforms/jfrog.md index 5862bd915d844..5ed569632c962 100644 --- a/docs/platforms/jfrog.md +++ b/docs/platforms/jfrog.md @@ -25,7 +25,7 @@ developers or stored in workspaces.
-You can skip the whole page and use [JFrog module](https://registry.coder.com/modules/jfrog) for easy JFrog Artifactory integration. +You can skip the whole page and use [JFrog module](https://registry.coder.com/modules/jfrog-token) for easy JFrog Artifactory integration.## Provisioner Authentication @@ -41,40 +41,48 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "~> 0.11.1" } docker = { source = "kreuzwerker/docker" - version = "~> 3.0.1" } artifactory = { source = "registry.terraform.io/jfrog/artifactory" - version = "~> 8.4.0" } } } -variable "jfrog_host" { +variable "jfrog_url" { type = string - description = "JFrog instance hostname. e.g. YYY.jfrog.io" + description = "JFrog instance URL. e.g. https://jfrog.example.com" + # validate the URL to ensure it starts with https:// or http:// + validation { + condition = can(regex("^https?://", var.jfrog_url)) + error_message = "JFrog URL must start with https:// or http://" + } } -variable "artifactory_access_token" { +variable "artifactory_admin_access_token" { type = string - description = "The admin-level access token to use for JFrog." + description = "The admin-level access token to use for JFrog with scope applied-permissions/admin" } # Configure the Artifactory provider provider "artifactory" { - url = "https://${var.jfrog_host}/artifactory" - access_token = "${var.artifactory_access_token}" + url = "${var.jfrog_url}/artifactory" + access_token = "${var.artifactory_admin_access_token}" +} + +resource "artifactory_scoped_token" "me" { + # This is hacky, but on terraform plan the data source gives empty strings, + # which fails validation. + username = length(local.artifactory_username) > 0 ? local.artifactory_username : "plan" } ``` When pushing the template, you can pass in the variables using the `--var` flag: ```shell -coder templates push --var 'jfrog_host=YYY.jfrog.io' --var 'artifactory_access_token=XXX' +coder templates push --var 'jfrog_url=https://YYY.jfrog.io' --var 'artifactory_admin_access_token=XXX' ``` ## Installing JFrog CLI @@ -112,6 +120,10 @@ locals { python = "pypi" go = "go" } + # Make sure to use the same field as the username field in the Artifactory + # It can be either the username or the email address. + artifactory_username = data.coder_workspace.me.owner_email + jfrog_host = replace(var.jfrog_url, "^https://", "") } ``` @@ -127,7 +139,7 @@ resource "coder_agent" "main" { set -e # install and start code-server - curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.11.0 + curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 & # The jf CLI checks $CI when determining whether to use interactive @@ -136,12 +148,12 @@ resource "coder_agent" "main" { jf c rm 0 || true echo ${artifactory_scoped_token.me.access_token} | \ - jf c add --access-token-stdin --url https://${var.jfrog_host} 0 + jf c add --access-token-stdin --url ${var.jfrog_url} 0 # Configure the `npm` CLI to use the Artifactory "npm" repository. cat << EOF > ~/.npmrc email = ${data.coder_workspace.me.owner_email} - registry = https://${var.jfrog_host}/artifactory/api/npm/${local.artifactory_repository_keys["npm"]} + registry = ${var.jfrog_url}/artifactory/api/npm/${local.artifactory_repository_keys["npm"]} EOF jf rt curl /api/npm/auth >> .npmrc @@ -149,13 +161,13 @@ resource "coder_agent" "main" { mkdir -p ~/.pip cat << EOF > ~/.pip/pip.conf [global] - index-url = https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/pypi/${local.artifactory_repository_keys["python"]}/simple + index-url = https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${local.jfrog_host}/artifactory/api/pypi/${local.artifactory_repository_keys["python"]}/simple EOF EOT # Set GOPROXY to use the Artifactory "go" repository. env = { - GOPROXY : "https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/go/${local.artifactory_repository_keys["go"]}" + GOPROXY : "https://${local.artifactory_username}:${artifactory_scoped_token.me.access_token}@${local..jfrog_host}/artifactory/api/go/${local.artifactory_repository_keys["go"]}" } } ``` @@ -186,9 +198,7 @@ following lines into your `startup_script`: # Install the JFrog VS Code extension. # Find the latest version number at # https://open-vsx.org/extension/JFrog/jfrog-vscode-extension. -JFROG_EXT_VERSION=2.4.1 -curl -o /tmp/jfrog.vsix -L "https://open-vsx.org/api/JFrog/jfrog-vscode-extension/$JFROG_EXT_VERSION/file/JFrog.jfrog-vscode-extension-$JFROG_EXT_VERSION.vsix" -/tmp/code-server/bin/code-server --install-extension /tmp/jfrog.vsix +/tmp/code-server/bin/code-server --install-extension jfrog.jfrog-vscode-extension ``` Note that this method will only work if your developers use code-server. @@ -202,7 +212,7 @@ Artifactory: # Configure the `npm` CLI to use the Artifactory "npm" registry. cat << EOF > ~/.npmrc email = ${data.coder_workspace.me.owner_email} - registry = https://${var.jfrog_host}/artifactory/api/npm/npm/ + registry = ${var.jfrog_url}/artifactory/api/npm/npm/ EOF jf rt curl /api/npm/auth >> .npmrc ``` @@ -221,7 +231,7 @@ Artifactory: mkdir -p ~/.pip cat << EOF > ~/.pip/pip.conf [global] - index-url = https://${data.coder_workspace.me.owner}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/pypi/pypi/simple + index-url = https://${data.coder_workspace.me.owner}:${artifactory_scoped_token.me.access_token}@${local.jfrog_host}/artifactory/api/pypi/pypi/simple EOF ``` @@ -237,7 +247,7 @@ Add the following environment variable to your `coder_agent` block to configure ```hcl env = { - GOPROXY : "https://${data.coder_workspace.me.owner}:${artifactory_scoped_token.me.access_token}@${var.jfrog_host}/artifactory/api/go/go" + GOPROXY : "https://${data.coder_workspace.me.owner}:${artifactory_scoped_token.me.access_token}@${local.jfrog_host}/artifactory/api/go/go" } ``` diff --git a/docs/platforms/kubernetes/additional-clusters.md b/docs/platforms/kubernetes/additional-clusters.md index 0a27ecb061b35..c3bcd42d18cfe 100644 --- a/docs/platforms/kubernetes/additional-clusters.md +++ b/docs/platforms/kubernetes/additional-clusters.md @@ -211,7 +211,7 @@ export CLUSTER_SERVICEACCOUNT_TOKEN=$(kubectl get secrets coder-v2 -n coder-work Create the template with these values: ```shell -coder templates create \ +coder templates push \ --variable host=$CLUSTER_ADDRESS \ --variable cluster_ca_certificate=$CLUSTER_CA_CERTIFICATE \ --variable token=$CLUSTER_SERVICEACCOUNT_TOKEN \ @@ -228,7 +228,7 @@ kubectl cluster-info # Get cluster CA and token (base64 encoded) kubectl get secrets coder-service-account-token -n coder-workspaces -o jsonpath="{.data}" -coder templates create \ +coder templates push \ --variable host=API_ADDRESS \ --variable cluster_ca_certificate=CLUSTER_CA_CERTIFICATE \ --variable token=CLUSTER_SERVICEACCOUNT_TOKEN \ diff --git a/docs/platforms/other.md b/docs/platforms/other.md index a01654cec04e4..d2f08ebd2d357 100644 --- a/docs/platforms/other.md +++ b/docs/platforms/other.md @@ -8,7 +8,6 @@ workspaces can include any Terraform resource. See our The following resources may help as you're deploying Coder. - [Coder packages: one-click install on cloud providers](https://github.com/coder/packages) -- [Run Coder as a system service](../install/packages.md) - [Deploy Coder offline](../install/offline.md) - [Supported resources (Terraform registry)](https://registry.terraform.io) - [Writing custom templates](../templates/index.md) diff --git a/docs/templates/general-settings.md b/docs/templates/general-settings.md index 7db4f01e44c29..592d63934cdb4 100644 --- a/docs/templates/general-settings.md +++ b/docs/templates/general-settings.md @@ -17,14 +17,7 @@ While this can be helpful for cases where a build is unlikely to finish, it also carries the risk of potentially corrupting your workspace. The setting is disabled by default. -### Require automatic updates - -> Requiring automatic updates is in an -> [experimental state](../contributing/feature-stages.md#experimental-features) -> and the behavior is subject to change. Use -> [GitHub issues](https://github.com/coder/coder) to leave feedback. This -> experiment must be specifically enabled with the -> `--experiments="template_update_policies"` option on your coderd deployment. +### Require automatic updates (enterprise) Admins can require all workspaces update to the latest active template version when they're started. This can be used to enforce security patches or other diff --git a/docs/templates/icons.md b/docs/templates/icons.md index a9072c3f14a01..0dc129c90a738 100644 --- a/docs/templates/icons.md +++ b/docs/templates/icons.md @@ -36,7 +36,20 @@ come bundled with your Coder deployment. - [**Authentication Providers**](https://coder.com/docs/v2/latest/admin/external-auth): - - Use icons for external authentication providers to make them recognizable + - Use icons for external authentication providers to make them recognizable. + You can set an icon for each provider by setting the + `CODER_EXTERNAL_AUTH_X_ICON` environment variable, where `X` is the number + of the provider. + + ```env + CODER_EXTERNAL_AUTH_0_ICON=/icon/github.svg + CODER_EXTERNAL_AUTH_1_ICON=/icon/google.svg + ``` + +- [**Support Links**](../admin/appearance#support-links): + + - Use icons for support links to make them recognizable. You can set the + `icon` field for each link in `CODER_SUPPORT_LINKS` array. ## Bundled icons diff --git a/docs/templates/schedule.md b/docs/templates/schedule.md new file mode 100644 index 0000000000000..e355d4ca27e9e --- /dev/null +++ b/docs/templates/schedule.md @@ -0,0 +1,44 @@ +# Workspace Scheduling + +You can configure a template to control how workspaces are started and stopped. +You can also manage the lifecycle of failed or inactive workspaces. + + + +## Schedule + +Template [admins](../admin/users.md) may define these default values: + +- **Default autostop**: How long a workspace runs without user activity before + Coder automatically stops it. +- **Max lifetime**: The maximum duration a workspace stays in a started state + before Coder forcibly stops it. + +## Allow users scheduling + +For templates where a uniform autostop duration is not appropriate, admins may +allow users to define their own autostart and autostop schedules. Admins can +restrict the days of the week a workspace should automatically start to help +manage infrastructure costs. + +## Failure cleanup + +Failure cleanup defines how long a workspace is permitted to remain in the +failed state prior to being automatically stopped. Failure cleanup is an +enterprise-only feature. + +## Dormancy threshold + +Dormancy Threshold defines how long Coder allows a workspace to remain inactive +before being moved into a dormant state. A workspace's inactivity is determined +by the time elapsed since a user last accessed the workspace. A workspace in the +dormant state is not eligible for autostart and must be manually activated by +the user before being accessible. Coder stops workspaces during their transition +to the dormant state if they are detected to be running. Dormancy Threshold is +an enterprise-only feature. + +## Dormancy auto-deletion + +Dormancy Auto-Deletion allows a template admin to dictate how long a workspace +is permitted to remain dormant before it is automatically deleted. Dormancy +Auto-Deletion is an enterprise-only feature. diff --git a/docs/workspaces.md b/docs/workspaces.md index 56db573a1431a..a56c6b414cec8 100644 --- a/docs/workspaces.md +++ b/docs/workspaces.md @@ -74,21 +74,72 @@ coder_app.  -### Max lifetime (Enterprise) +### Max lifetime (Deprecated, Enterprise) Max lifetime is a template setting that determines the number of hours a workspace will run before Coder automatically stops it, regardless of any active connections. Use this setting to ensure that workspaces do not run in perpetuity when connections are left open inadvertently. -### Automatic updates +Max lifetime is deprecated in favor of template autostop requirements. Templates +can choose to use a max lifetime or an autostop requirement during the +deprecation period, but only one can be used at a time. Coder recommends using +autostop requirements instead as they avoid restarts during work hours. + +### Autostop requirement (enterprise) + +Autostop requirement is a template setting that determines how often workspaces +using the template must automatically stop. Autostop requirement ignores any +active connections, and ensures that workspaces do not run in perpetuity when +connections are left open inadvertently. + +Workspaces will apply the template autostop requirement on the given day in the +user's timezone and specified quiet hours (see below). This ensures that +workspaces will not be stopped during work hours. + +The available options are "Days", which can be set to "Daily", "Saturday" or +"Sunday", and "Weeks", which can be set to any number from 1 to 16. + +"Days" governs which days of the week workspaces must stop. If you select +"daily", workspaces must be automatically stopped every day at the start of the +user's defined quiet hours. When using "Saturday" or "Sunday", workspaces will +be automatically stopped on Saturday or Sunday in the user's timezone and quiet +hours. + +"Weeks" determines how many weeks between required stops. It cannot be changed +from the default of 1 if you have selected "Daily" for "Days". When using a +value greater than 1, workspaces will be automatically stopped every N weeks on +the day specified by "Days" and the user's quiet hours. The autostop week is +synchronized for all workspaces on the same template. -> Automatic updates is part of an -> [experimental feature](../contributing/feature-stages.md#experimental-features) -> and the behavior is subject to change. Use -> [GitHub issues](https://github.com/coder/coder) to leave feedback. This -> experiment must be specifically enabled with the -> `--experiments="template_update_policies"` option on your coderd deployment. +Autostop requirement is disabled when the template is using the deprecated max +lifetime feature. Templates can choose to use a max lifetime or an autostop +requirement during the deprecation period, but only one can be used at a time. + +#### User quiet hours (enterprise) + +User quiet hours can be configured in the user's schedule settings page. +Workspaces on templates with an autostop requirement will only be forcibly +stopped due to the policy at the start of the user's quiet hours. + + + +Admins can define the default quiet hours for all users with the +`--default-quiet-hours-schedule` flag or `CODER_DEFAULT_QUIET_HOURS_SCHEDULE` +environment variable. The value should be a cron expression such as +`CRON_TZ=America/Chicago 30 2 * * *` which would set the default quiet hours to +2:30 AM in the America/Chicago timezone. The cron schedule can only have a +minute and hour component. The default schedule is UTC 00:00. It is recommended +to set the default quiet hours to a time when most users are not expected to be +using Coder. + +Admins can force users to use the default quiet hours with the +[CODER_ALLOW_CUSTOM_QUIET_HOURS](./cli/server.md#allow-custom-quiet-hours) +environment variable. Users will still be able to see the page, but will be +unable to set a custom time or timezone. If users have already set a custom +quiet hours schedule, it will be ignored and the default will be used instead. + +### Automatic updates It can be tedious to manually update a workspace everytime an update is pushed to a template. Users can choose to opt-in to automatic updates to update to the diff --git a/dogfood/Dockerfile b/dogfood/Dockerfile index 7cd9e5e637b8c..2b2bc8897d32f 100644 --- a/dogfood/Dockerfile +++ b/dogfood/Dockerfile @@ -53,7 +53,7 @@ RUN mkdir --parents "$GOPATH" && \ # charts and values files go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.5.0 && \ # sqlc for Go code generation - (CGO_ENABLED=1 go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.24.0) && \ + (CGO_ENABLED=1 go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.25.0) && \ # gcr-cleaner-cli used by CI to prune unused images go install github.com/sethvargo/gcr-cleaner/cmd/gcr-cleaner-cli@v0.5.1 && \ # ruleguard for checking custom rules, without needing to run all of @@ -68,12 +68,12 @@ RUN mkdir --parents "$GOPATH" && \ go install github.com/goreleaser/goreleaser@v1.6.1 && \ go install mvdan.cc/sh/v3/cmd/shfmt@latest && \ # nfpm is used with `make build` to make release packages - go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.16.0 && \ + go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.35.1 && \ # yq v4 is used to process yaml files in coder v2. Conflicts with # yq v3 used in v1. go install github.com/mikefarah/yq/v4@v4.30.6 && \ mv /tmp/bin/yq /tmp/bin/yq4 && \ - go install github.com/golang/mock/mockgen@v1.6.0 + go install go.uber.org/mock/mockgen@v0.4.0 FROM gcr.io/coder-dev-1/alpine:3.18 as proto WORKDIR /tmp @@ -199,14 +199,13 @@ RUN LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygi # Install frontend utilities RUN apt-get update && \ # Node.js (from nodesource) and Yarn (from yarnpkg) - curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - &&\ apt-get install --yes --quiet \ nodejs yarn \ # Install browsers for e2e testing google-chrome-stable microsoft-edge-beta && \ # Pre-install system dependencies that Playwright needs. npx doesn't work here # for some reason. See https://github.com/microsoft/playwright-cli/issues/136 - npm i -g playwright@1.36.2 pnpm@^8 && playwright install-deps && \ + npm i -g playwright@1.36.2 pnpm@^8 corepack && playwright install-deps && \ npm cache clean --force # Ensure PostgreSQL binaries are in the users $PATH. diff --git a/dogfood/files/etc/apt/sources.list.d/nodesource.list b/dogfood/files/etc/apt/sources.list.d/nodesource.list index a328c2c3c47dc..6612fe36684b9 100644 --- a/dogfood/files/etc/apt/sources.list.d/nodesource.list +++ b/dogfood/files/etc/apt/sources.list.d/nodesource.list @@ -1 +1 @@ -deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x jammy main +deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main diff --git a/dogfood/files/usr/share/keyrings/nodesource.gpg b/dogfood/files/usr/share/keyrings/nodesource.gpg index 4f3ec4ed793b3..a8c38d432dbd8 100644 Binary files a/dogfood/files/usr/share/keyrings/nodesource.gpg and b/dogfood/files/usr/share/keyrings/nodesource.gpg differ diff --git a/dogfood/main.tf b/dogfood/main.tf index 034be9006b697..4154070329e58 100644 --- a/dogfood/main.tf +++ b/dogfood/main.tf @@ -10,6 +10,16 @@ terraform { } } +variable "jfrog_url" { + type = string + description = "Artifactory URL. e.g. https://myartifactory.example.com" + # ensue the URL is HTTPS or HTTP + validation { + condition = can(regex("^(https|http)://", var.jfrog_url)) + error_message = "jfrog_url must be a valid URL starting with either 'https://' or 'http://'" + } +} + locals { // These are cluster service addresses mapped to Tailscale nodes. Ask Dean or // Kyle for help. @@ -21,7 +31,10 @@ locals { "sa-saopaulo" = "tcp://oberstein-sao-cdr-dev.tailscale.svc.cluster.local:2375" } - repo_dir = replace(data.coder_parameter.repo_dir.value, "/^~\\//", "/home/coder/") + repo_dir = replace(data.coder_parameter.repo_dir.value, "/^~\\//", "/home/coder/") + container_name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + registry_name = "codercom/oss-dogfood" + jfrog_host = replace(var.jfrog_url, "https://", "") } data "coder_parameter" "repo_dir" { @@ -125,6 +138,20 @@ module "coder-login" { agent_id = coder_agent.dev.id } +module "jfrog" { + source = "https://registry.coder.com/modules/jfrog-oauth" + agent_id = coder_agent.dev.id + jfrog_url = var.jfrog_url + configure_code_server = true + username_field = "username" + package_managers = { + "npm" : "npm", + "go" : "go", + "pypi" : "pypi", + "docker" : "docker" + } +} + resource "coder_agent" "dev" { arch = "amd64" os = "linux" @@ -219,8 +246,9 @@ resource "coder_agent" "dev" { startup_script_timeout = 60 startup_script = <<-EOT set -eux -o pipefail + # Start Docker service sudo service docker start - EOT +EOT } resource "docker_volume" "home_volume" { @@ -250,10 +278,6 @@ resource "docker_volume" "home_volume" { } } -locals { - container_name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" - registry_name = "codercom/oss-dogfood" -} data "docker_registry_image" "dogfood" { name = "${local.registry_name}:latest" } diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 0f5e5eef01dc5..c7e9272adfe40 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -120,6 +120,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "deleted": ActionTrack, "quiet_hours_schedule": ActionTrack, "theme_preference": ActionIgnore, + "name": ActionTrack, }, &database.Workspace{}: { "id": ActionTrack, diff --git a/enterprise/cli/provisionerdaemons_test.go b/enterprise/cli/provisionerdaemons_test.go index 2b4d0ab117dae..3651971e8f9c5 100644 --- a/enterprise/cli/provisionerdaemons_test.go +++ b/enterprise/cli/provisionerdaemons_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/rbac" @@ -49,6 +50,8 @@ func TestProvisionerDaemon_PSK(t *testing.T) { }, testutil.WaitShort, testutil.IntervalSlow) require.Equal(t, "matt-daemon", daemons[0].Name) require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope]) + require.Equal(t, buildinfo.Version(), daemons[0].Version) + require.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion) } func TestProvisionerDaemon_SessionToken(t *testing.T) { @@ -84,6 +87,8 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { assert.Equal(t, "my-daemon", daemons[0].Name) assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope]) assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner]) + assert.Equal(t, buildinfo.Version(), daemons[0].Version) + assert.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion) }) t.Run("ScopeAnotherUser", func(t *testing.T) { @@ -118,6 +123,8 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope]) // This should get clobbered to the user who started the daemon. assert.Equal(t, anotherUser.ID.String(), daemons[0].Tags[provisionersdk.TagOwner]) + assert.Equal(t, buildinfo.Version(), daemons[0].Version) + assert.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion) }) t.Run("ScopeOrg", func(t *testing.T) { @@ -150,5 +157,7 @@ func TestProvisionerDaemon_SessionToken(t *testing.T) { }, testutil.WaitShort, testutil.IntervalSlow) assert.Equal(t, "org-daemon", daemons[0].Name) assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope]) + assert.Equal(t, buildinfo.Version(), daemons[0].Version) + assert.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion) }) } diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go index 4e37077b3c90f..9ac59735b120d 100644 --- a/enterprise/cli/proxyserver.go +++ b/enterprise/cli/proxyserver.go @@ -26,8 +26,8 @@ import ( "github.com/coder/coder/v2/cli/clilog" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/coderd" - "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/wsproxy" ) @@ -208,7 +208,7 @@ func (r *RootCmd) proxyServer() *clibase.Cmd { var appHostnameRegex *regexp.Regexp appHostname := cfg.WildcardAccessURL.String() if appHostname != "" { - appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname) + appHostnameRegex, err = appurl.CompileHostnamePattern(appHostname) if err != nil { return xerrors.Errorf("parse wildcard access URL %q: %w", appHostname, err) } diff --git a/enterprise/cli/server_dbcrypt_test.go b/enterprise/cli/server_dbcrypt_test.go index 8b1bbffa52b9f..e9e88c49d28e1 100644 --- a/enterprise/cli/server_dbcrypt_test.go +++ b/enterprise/cli/server_dbcrypt_test.go @@ -15,9 +15,9 @@ import ( "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/postgres" - "github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/enterprise/dbcrypt" "github.com/coder/coder/v2/pty/ptytest" + "github.com/coder/coder/v2/testutil" ) // TestServerDBCrypt tests end-to-end encryption, decryption, and deletion @@ -50,7 +50,7 @@ func TestServerDBCrypt(t *testing.T) { users := genData(t, db) // Setup an initial cipher A - keyA := mustString(t, 32) + keyA := testutil.MustRandString(t, 32) cipherA, err := dbcrypt.NewCiphers([]byte(keyA)) require.NoError(t, err) @@ -87,7 +87,7 @@ func TestServerDBCrypt(t *testing.T) { } // Re-encrypt all existing data with a new cipher. - keyB := mustString(t, 32) + keyB := testutil.MustRandString(t, 32) cipherBA, err := dbcrypt.NewCiphers([]byte(keyB), []byte(keyA)) require.NoError(t, err) @@ -160,7 +160,7 @@ func TestServerDBCrypt(t *testing.T) { } // Re-encrypt all existing data with a new cipher. - keyC := mustString(t, 32) + keyC := testutil.MustRandString(t, 32) cipherC, err := dbcrypt.NewCiphers([]byte(keyC)) require.NoError(t, err) @@ -222,7 +222,7 @@ func genData(t *testing.T, db database.Store) []database.User { for _, status := range database.AllUserStatusValues() { for _, loginType := range database.AllLoginTypeValues() { for _, deleted := range []bool{false, true} { - randName := mustString(t, 32) + randName := testutil.MustRandString(t, 32) usr := dbgen.User(t, db, database.User{ Username: randName, Email: randName + "@notcoder.com", @@ -252,13 +252,6 @@ func genData(t *testing.T, db database.Store) []database.User { return users } -func mustString(t *testing.T, n int) string { - t.Helper() - s, err := cryptorand.String(n) - require.NoError(t, err) - return s -} - func requireEncryptedEquals(t *testing.T, c dbcrypt.Cipher, expected, actual string) { t.Helper() var decodedVal []byte diff --git a/enterprise/cli/start_test.go b/enterprise/cli/start_test.go index 8f3903dd6357c..1972ada2072bb 100644 --- a/enterprise/cli/start_test.go +++ b/enterprise/cli/start_test.go @@ -167,4 +167,46 @@ func TestStart(t *testing.T) { }) } }) + + t.Run("StartActivatesDormant", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + }) + + version := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, version.ID) + template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, version.ID) + + memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) + workspace := coderdtest.CreateWorkspace(t, memberClient, owner.OrganizationID, template.ID) + _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace.LatestBuild.ID) + _ = coderdtest.MustTransitionWorkspace(t, memberClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + err := memberClient.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ + Dormant: true, + }) + require.NoError(t, err) + + inv, root := newCLI(t, "start", workspace.Name) + clitest.SetupConfig(t, memberClient, root) + + var buf bytes.Buffer + inv.Stdout = &buf + + err = inv.Run() + require.NoError(t, err) + require.Contains(t, buf.String(), "Activating dormant workspace...") + + workspace = coderdtest.MustWorkspace(t, memberClient, workspace.ID) + require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition) + }) } diff --git a/enterprise/cli/templatecreate_test.go b/enterprise/cli/templatecreate_test.go index 9499810b7df3a..6803ad394033e 100644 --- a/enterprise/cli/templatecreate_test.go +++ b/enterprise/cli/templatecreate_test.go @@ -62,11 +62,6 @@ func TestTemplateCreate(t *testing.T) { t.Run("WorkspaceCleanup", func(t *testing.T) { t.Parallel() - dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{ - string(codersdk.ExperimentWorkspaceActions), - } - client, user := coderdenttest.New(t, &coderdenttest.Options{ LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ @@ -74,7 +69,6 @@ func TestTemplateCreate(t *testing.T) { }, }, Options: &coderdtest.Options{ - DeploymentValues: dv, IncludeProvisionerDaemon: true, }, }) diff --git a/enterprise/cli/templateedit_test.go b/enterprise/cli/templateedit_test.go index 36b17e23d2119..29575e5ab5046 100644 --- a/enterprise/cli/templateedit_test.go +++ b/enterprise/cli/templateedit_test.go @@ -4,11 +4,15 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "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/dbfake" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" "github.com/coder/coder/v2/enterprise/coderd/license" @@ -89,11 +93,6 @@ func TestTemplateEdit(t *testing.T) { t.Run("WorkspaceCleanup", func(t *testing.T) { t.Parallel() - dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{ - string(codersdk.ExperimentWorkspaceActions), - } - ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{ LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ @@ -101,7 +100,6 @@ func TestTemplateEdit(t *testing.T) { }, }, Options: &coderdtest.Options{ - DeploymentValues: dv, IncludeProvisionerDaemon: true, }, }) @@ -111,7 +109,6 @@ func TestTemplateEdit(t *testing.T) { _ = coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID) template := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) require.False(t, template.RequireActiveVersion) - const ( expectedFailureTTL = time.Hour * 3 expectedDormancyThreshold = time.Hour * 4 @@ -156,4 +153,168 @@ func TestTemplateEdit(t *testing.T) { require.Equal(t, expectedDormancyThreshold.Milliseconds(), template.TimeTilDormantMillis) require.Equal(t, expectedDormancyAutoDeletion.Milliseconds(), template.TimeTilDormantAutoDeleteMillis) }) + + // Test that omitting a flag does not update a template with the + // default for a flag. + t.Run("DefaultsDontOverride", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + codersdk.FeatureAccessControl: 1, + codersdk.FeatureTemplateRBAC: 1, + }, + }, + }) + + dbtemplate := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{ + CreatedBy: owner.UserID, + OrganizationID: owner.OrganizationID, + }).Do().Template + + var ( + expectedName = "template" + expectedDisplayName = "template_display" + expectedDescription = "My description" + expectedIcon = "icon.pjg" + expectedDefaultTTLMillis = time.Hour.Milliseconds() + expectedMaxTTLMillis = (time.Hour * 24).Milliseconds() + expectedAllowAutostart = false + expectedAllowAutostop = false + expectedFailureTTLMillis = time.Minute.Milliseconds() + expectedDormancyMillis = 2 * time.Minute.Milliseconds() + expectedAutoDeleteMillis = 3 * time.Minute.Milliseconds() + expectedRequireActiveVersion = true + expectedAllowCancelJobs = false + deprecationMessage = "Deprecate me" + expectedDisableEveryone = true + expectedAutostartDaysOfWeek = []string{} + expectedAutoStopDaysOfWeek = []string{} + expectedAutoStopWeeks = 1 + ) + + assertFieldsFn := func(t *testing.T, tpl codersdk.Template, acl codersdk.TemplateACL) { + t.Helper() + + assert.Equal(t, expectedName, tpl.Name) + assert.Equal(t, expectedDisplayName, tpl.DisplayName) + assert.Equal(t, expectedDescription, tpl.Description) + assert.Equal(t, expectedIcon, tpl.Icon) + assert.Equal(t, expectedDefaultTTLMillis, tpl.DefaultTTLMillis) + assert.Equal(t, expectedMaxTTLMillis, tpl.MaxTTLMillis) + assert.Equal(t, expectedAllowAutostart, tpl.AllowUserAutostart) + assert.Equal(t, expectedAllowAutostop, tpl.AllowUserAutostop) + assert.Equal(t, expectedFailureTTLMillis, tpl.FailureTTLMillis) + assert.Equal(t, expectedDormancyMillis, tpl.TimeTilDormantMillis) + assert.Equal(t, expectedAutoDeleteMillis, tpl.TimeTilDormantAutoDeleteMillis) + assert.Equal(t, expectedRequireActiveVersion, tpl.RequireActiveVersion) + assert.Equal(t, deprecationMessage, tpl.DeprecationMessage) + assert.Equal(t, expectedAllowCancelJobs, tpl.AllowUserCancelWorkspaceJobs) + assert.Equal(t, len(acl.Groups) == 0, expectedDisableEveryone) + assert.Equal(t, expectedAutostartDaysOfWeek, tpl.AutostartRequirement.DaysOfWeek) + assert.Equal(t, expectedAutoStopDaysOfWeek, tpl.AutostopRequirement.DaysOfWeek) + assert.Equal(t, int64(expectedAutoStopWeeks), tpl.AutostopRequirement.Weeks) + } + + template, err := ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{ + Name: expectedName, + DisplayName: expectedDisplayName, + Description: expectedDescription, + Icon: expectedIcon, + DefaultTTLMillis: expectedDefaultTTLMillis, + MaxTTLMillis: expectedMaxTTLMillis, + AllowUserAutostop: expectedAllowAutostop, + AllowUserAutostart: expectedAllowAutostart, + FailureTTLMillis: expectedFailureTTLMillis, + TimeTilDormantMillis: expectedDormancyMillis, + TimeTilDormantAutoDeleteMillis: expectedAutoDeleteMillis, + RequireActiveVersion: expectedRequireActiveVersion, + DeprecationMessage: ptr.Ref(deprecationMessage), + DisableEveryoneGroupAccess: expectedDisableEveryone, + AllowUserCancelWorkspaceJobs: expectedAllowCancelJobs, + AutostartRequirement: &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: expectedAutostartDaysOfWeek, + }, + }) + require.NoError(t, err) + + templateACL, err := ownerClient.TemplateACL(ctx, template.ID) + require.NoError(t, err) + + assertFieldsFn(t, template, templateACL) + + expectedName = "newName" + inv, conf := newCLI(t, "templates", + "edit", template.Name, + "--name=newName", + "-y", + ) + + clitest.SetupConfig(t, ownerClient, conf) + + err = inv.Run() + require.NoError(t, err) + + template, err = ownerClient.Template(ctx, template.ID) + require.NoError(t, err) + templateACL, err = ownerClient.TemplateACL(ctx, template.ID) + require.NoError(t, err) + assertFieldsFn(t, template, templateACL) + + expectedAutostartDaysOfWeek = []string{"monday", "wednesday", "friday"} + expectedAutoStopDaysOfWeek = []string{"tuesday", "thursday"} + expectedAutoStopWeeks = 2 + expectedMaxTTLMillis = 0 + + template, err = ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{ + Name: expectedName, + DisplayName: expectedDisplayName, + Description: expectedDescription, + Icon: expectedIcon, + DefaultTTLMillis: expectedDefaultTTLMillis, + AllowUserAutostop: expectedAllowAutostop, + AllowUserAutostart: expectedAllowAutostart, + FailureTTLMillis: expectedFailureTTLMillis, + TimeTilDormantMillis: expectedDormancyMillis, + TimeTilDormantAutoDeleteMillis: expectedAutoDeleteMillis, + RequireActiveVersion: expectedRequireActiveVersion, + DeprecationMessage: ptr.Ref(deprecationMessage), + DisableEveryoneGroupAccess: expectedDisableEveryone, + AllowUserCancelWorkspaceJobs: expectedAllowCancelJobs, + AutostartRequirement: &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: expectedAutostartDaysOfWeek, + }, + + AutostopRequirement: &codersdk.TemplateAutostopRequirement{ + DaysOfWeek: expectedAutoStopDaysOfWeek, + Weeks: int64(expectedAutoStopWeeks), + }, + }) + require.NoError(t, err) + assertFieldsFn(t, template, templateACL) + + // Rerun the update so we can assert that autostop days aren't + // mucked with. + expectedName = "newName2" + inv, conf = newCLI(t, "templates", + "edit", template.Name, + "--name=newName2", + "-y", + ) + + clitest.SetupConfig(t, ownerClient, conf) + + err = inv.Run() + require.NoError(t, err) + + template, err = ownerClient.Template(ctx, template.ID) + require.NoError(t, err) + + templateACL, err = ownerClient.TemplateACL(ctx, template.ID) + require.NoError(t, err) + assertFieldsFn(t, template, templateACL) + }) } diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 0df1bec5bb35d..e2b27dc6d9234 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -55,6 +55,9 @@ OPTIONS: The algorithm to use for generating ssh keys. Accepted values are "ed25519", "ecdsa", or "rsa4096". + --support-links struct[[]codersdk.LinkConfig], $CODER_SUPPORT_LINKS + Support links to display in the top right drop down menu. + --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. @@ -168,7 +171,7 @@ NETWORKING OPTIONS: --secure-auth-cookie bool, $CODER_SECURE_AUTH_COOKIE Controls if the 'Secure' property is set on browser session cookies. - --wildcard-access-url url, $CODER_WILDCARD_ACCESS_URL + --wildcard-access-url string, $CODER_WILDCARD_ACCESS_URL Specifies the wildcard hostname to use for workspace applications in the form "*.example.com". diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index bd6997506a32e..af56626a8db68 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -128,6 +128,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { } return api.fetchRegions(ctx) } + api.tailnetService, err = tailnet.NewClientService( + api.Logger.Named("tailnetclient"), + &api.AGPL.TailnetCoordinator, + api.Options.DERPMapUpdateFrequency, + api.AGPL.DERPMap, + ) + if err != nil { + api.Logger.Fatal(api.ctx, "failed to initialize tailnet client service", slog.Error(err)) + } oauthConfigs := &httpmw.OAuth2Configs{ Github: options.GithubOAuth2Config, @@ -483,6 +492,7 @@ type API struct { provisionerDaemonAuth *provisionerDaemonAuth licenseMetricsCollector license.MetricsCollector + tailnetService *tailnet.ClientService } func (api *API) Close() error { @@ -613,12 +623,7 @@ func (api *API) updateEntitlements(ctx context.Context) error { if initial, changed, enabled := featureChanged(codersdk.FeatureHighAvailability); shouldUpdate(initial, changed, enabled) { var coordinator agpltailnet.Coordinator if enabled { - var haCoordinator agpltailnet.Coordinator - if api.AGPL.Experiments.Enabled(codersdk.ExperimentTailnetPGCoordinator) { - haCoordinator, err = tailnet.NewPGCoord(api.ctx, api.Logger, api.Pubsub, api.Database) - } else { - haCoordinator, err = tailnet.NewCoordinator(api.Logger, api.Pubsub) - } + haCoordinator, err := tailnet.NewPGCoord(api.ctx, api.Logger, api.Pubsub, api.Database) if err != nil { api.Logger.Error(ctx, "unable to set up high availability coordinator", slog.Error(err)) // If we try to setup the HA coordinator and it fails, nothing diff --git a/enterprise/coderd/coderd_test.go b/enterprise/coderd/coderd_test.go index 59fbe1818c781..f69fbff8d49cd 100644 --- a/enterprise/coderd/coderd_test.go +++ b/enterprise/coderd/coderd_test.go @@ -48,6 +48,10 @@ func TestEntitlements(t *testing.T) { require.Empty(t, res.Warnings) }) t.Run("FullLicense", func(t *testing.T) { + // PGCoordinator requires a real postgres + if !dbtestutil.WillUsePostgres() { + t.Skip("test only with postgres") + } t.Parallel() adminClient, _ := coderdenttest.New(t, &coderdenttest.Options{ AuditLogging: true, diff --git a/enterprise/coderd/coderdenttest/proxytest.go b/enterprise/coderd/coderdenttest/proxytest.go index 8a28b077c16f4..9b43cbe6c316d 100644 --- a/enterprise/coderd/coderdenttest/proxytest.go +++ b/enterprise/coderd/coderdenttest/proxytest.go @@ -19,7 +19,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" - "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd" "github.com/coder/coder/v2/enterprise/wsproxy" @@ -37,6 +37,9 @@ type ProxyOptions struct { // ProxyURL is optional ProxyURL *url.URL + + // FlushStats is optional + FlushStats chan chan<- struct{} } // NewWorkspaceProxy will configure a wsproxy.Server with the given options. @@ -96,7 +99,7 @@ func NewWorkspaceProxy(t *testing.T, coderdAPI *coderd.API, owner *codersdk.Clie var appHostnameRegex *regexp.Regexp if options.AppHostname != "" { var err error - appHostnameRegex, err = httpapi.CompileHostnamePattern(options.AppHostname) + appHostnameRegex, err = appurl.CompileHostnamePattern(options.AppHostname) require.NoError(t, err) } @@ -113,6 +116,9 @@ func NewWorkspaceProxy(t *testing.T, coderdAPI *coderd.API, owner *codersdk.Clie // Inherit collector options from coderd, but keep the wsproxy reporter. statsCollectorOptions := coderdAPI.Options.WorkspaceAppsStatsCollectorOptions statsCollectorOptions.Reporter = nil + if options.FlushStats != nil { + statsCollectorOptions.Flush = options.FlushStats + } wssrv, err := wsproxy.New(ctx, &wsproxy.Options{ Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug), diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index a681d27859514..b1330993b1add 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -48,7 +48,8 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request) if req.Name == database.EveryoneGroup { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("%q is a reserved keyword and cannot be used for a group name.", database.EveryoneGroup), + Message: "Invalid group name.", + Validations: []codersdk.ValidationError{{Field: "name", Detail: fmt.Sprintf("%q is a reserved group name", req.Name)}}, }) return } @@ -63,7 +64,8 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request) }) if database.IsUniqueViolation(err) { httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ - Message: fmt.Sprintf("Group with name %q already exists.", req.Name), + Message: fmt.Sprintf("A group named %q already exists.", req.Name), + Validations: []codersdk.ValidationError{{Field: "name", Detail: "Group names must be unique"}}, }) return } diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 874c8cb501105..92f034e35202c 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -26,6 +26,7 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" @@ -89,7 +90,7 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) { } apiDaemons := make([]codersdk.ProvisionerDaemon, 0) for _, daemon := range daemons { - apiDaemons = append(apiDaemons, convertProvisionerDaemon(daemon)) + apiDaemons = append(apiDaemons, db2sdk.ProvisionerDaemon(daemon)) } httpapi.Write(ctx, rw, http.StatusOK, apiDaemons) } @@ -233,6 +234,13 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) authCtx = dbauthz.AsSystemRestricted(ctx) } + versionHdrVal := r.Header.Get(codersdk.BuildVersionHeader) + + apiVersion := "1.0" + if qv := r.URL.Query().Get("version"); qv != "" { + apiVersion = qv + } + // Create the daemon in the database. now := dbtime.Now() daemon, err := api.Database.UpsertProvisionerDaemon(authCtx, database.UpsertProvisionerDaemonParams{ @@ -241,8 +249,8 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) Tags: tags, CreatedAt: now, LastSeenAt: sql.NullTime{Time: now, Valid: true}, - Version: "", // TODO: provisionerd needs to send version - APIVersion: "1.0", + Version: versionHdrVal, + APIVersion: apiVersion, }) if err != nil { if !xerrors.Is(err, context.Canceled) { @@ -353,21 +361,6 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) _ = conn.Close(websocket.StatusGoingAway, "") } -func convertProvisionerDaemon(daemon database.ProvisionerDaemon) codersdk.ProvisionerDaemon { - result := codersdk.ProvisionerDaemon{ - ID: daemon.ID, - CreatedAt: daemon.CreatedAt, - LastSeenAt: codersdk.NullTime{NullTime: daemon.LastSeenAt}, - Name: daemon.Name, - Tags: daemon.Tags, - Version: daemon.Version, - } - for _, provisionerType := range daemon.Provisioners { - result.Provisioners = append(result.Provisioners, codersdk.ProvisionerType(provisionerType)) - } - return result -} - // wsNetConn wraps net.Conn created by websocket.NetConn(). Cancel func // is called if a read or write error is encountered. type wsNetConn struct { diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go index 2e19aa31688f8..ac48e21cdd14f 100644 --- a/enterprise/coderd/provisionerdaemons_test.go +++ b/enterprise/coderd/provisionerdaemons_test.go @@ -12,6 +12,7 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" @@ -40,9 +41,10 @@ func TestProvisionerDaemonServe(t *testing.T) { templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() + daemonName := testutil.MustRandString(t, 63) srv, err := templateAdminClient.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), - Name: t.Name(), + Name: daemonName, Organization: user.OrganizationID, Provisioners: []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, @@ -54,7 +56,11 @@ func TestProvisionerDaemonServe(t *testing.T) { daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion. require.NoError(t, err) - require.Len(t, daemons, 1) + if assert.Len(t, daemons, 1) { + assert.Equal(t, daemonName, daemons[0].Name) + assert.Equal(t, buildinfo.Version(), daemons[0].Version) + assert.Equal(t, provisionersdk.VersionCurrent.String(), daemons[0].APIVersion) + } }) t.Run("NoLicense", func(t *testing.T) { @@ -63,9 +69,10 @@ func TestProvisionerDaemonServe(t *testing.T) { templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() + daemonName := testutil.MustRandString(t, 63) _, err := templateAdminClient.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), - Name: t.Name(), + Name: daemonName, Organization: user.OrganizationID, Provisioners: []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, @@ -90,7 +97,7 @@ func TestProvisionerDaemonServe(t *testing.T) { defer cancel() _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), - Name: t.Name(), + Name: testutil.MustRandString(t, 63), Organization: user.OrganizationID, Provisioners: []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, @@ -117,7 +124,7 @@ func TestProvisionerDaemonServe(t *testing.T) { defer cancel() _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), - Name: t.Name(), + Name: testutil.MustRandString(t, 63), Organization: user.OrganizationID, Provisioners: []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, @@ -212,7 +219,9 @@ func TestProvisionerDaemonServe(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() another := codersdk.New(client.URL) + daemonName := testutil.MustRandString(t, 63) srv, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ + Name: daemonName, Organization: user.OrganizationID, Provisioners: []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, @@ -229,6 +238,7 @@ func TestProvisionerDaemonServe(t *testing.T) { daemons, err := client.ProvisionerDaemons(ctx) //nolint:gocritic // Test assertion. require.NoError(t, err) if assert.Len(t, daemons, 1) { + assert.Equal(t, daemonName, daemons[0].Name) assert.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope]) } }) @@ -274,7 +284,7 @@ func TestProvisionerDaemonServe(t *testing.T) { pd := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { return another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), - Name: t.Name(), + Name: testutil.MustRandString(t, 63), Organization: user.OrganizationID, Provisioners: []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, @@ -352,7 +362,7 @@ func TestProvisionerDaemonServe(t *testing.T) { defer cancel() _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), - Name: t.Name(), + Name: testutil.MustRandString(t, 32), Organization: user.OrganizationID, Provisioners: []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, @@ -387,7 +397,7 @@ func TestProvisionerDaemonServe(t *testing.T) { another := codersdk.New(client.URL) _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), - Name: t.Name(), + Name: testutil.MustRandString(t, 63), Organization: user.OrganizationID, Provisioners: []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, @@ -420,7 +430,7 @@ func TestProvisionerDaemonServe(t *testing.T) { another := codersdk.New(client.URL) _, err := another.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), - Name: t.Name(), + Name: testutil.MustRandString(t, 63), Organization: user.OrganizationID, Provisioners: []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, diff --git a/enterprise/coderd/proxyhealth/proxyhealth.go b/enterprise/coderd/proxyhealth/proxyhealth.go index f4014e398135b..33a5da7d269a8 100644 --- a/enterprise/coderd/proxyhealth/proxyhealth.go +++ b/enterprise/coderd/proxyhealth/proxyhealth.go @@ -3,6 +3,7 @@ package proxyhealth import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -275,8 +276,33 @@ func (p *ProxyHealth) runOnce(ctx context.Context, now time.Time) (map[uuid.UUID case err == nil && resp.StatusCode == http.StatusOK: err := json.NewDecoder(resp.Body).Decode(&status.Report) if err != nil { + isCoderErr := xerrors.Errorf("proxy url %q is not a coder proxy instance, verify the url is correct", reqURL) + if resp.Header.Get(codersdk.BuildVersionHeader) != "" { + isCoderErr = xerrors.Errorf("proxy url %q is a coder instance, but unable to decode the response payload. Could this be a primary coderd and not a proxy?", reqURL) + } + + // If the response is not json, then the user likely input a bad url that returns status code 200. + // This is very common, since most webpages do return a 200. So let's improve the error message. + if notJSONErr := codersdk.ExpectJSONMime(resp); notJSONErr != nil { + err = errors.Join( + isCoderErr, + xerrors.Errorf("attempted to query health at %q but got back the incorrect content type: %w", reqURL, notJSONErr), + ) + + status.Report.Errors = []string{ + err.Error(), + } + status.Status = Unhealthy + break + } + // If we cannot read the report, mark the proxy as unhealthy. - status.Report.Errors = []string{fmt.Sprintf("failed to decode health report: %s", err.Error())} + status.Report.Errors = []string{ + errors.Join( + isCoderErr, + xerrors.Errorf("received a status code 200, but failed to decode health report body: %w", err), + ).Error(), + } status.Status = Unhealthy break } @@ -295,19 +321,17 @@ func (p *ProxyHealth) runOnce(ctx context.Context, now time.Time) (map[uuid.UUID // readable. builder.WriteString(fmt.Sprintf("unexpected status code %d. ", resp.StatusCode)) builder.WriteString(fmt.Sprintf("\nEncountered error, send a request to %q from the Coderd environment to debug this issue.", reqURL)) + // err will always be non-nil err := codersdk.ReadBodyAsError(resp) - if err != nil { - var apiErr *codersdk.Error - if xerrors.As(err, &apiErr) { - builder.WriteString(fmt.Sprintf("\nError Message: %s\nError Detail: %s", apiErr.Message, apiErr.Detail)) - for _, v := range apiErr.Validations { - // Pretty sure this is not possible from the called endpoint, but just in case. - builder.WriteString(fmt.Sprintf("\n\tValidation: %s=%s", v.Field, v.Detail)) - } - } else { - builder.WriteString(fmt.Sprintf("\nError: %s", err.Error())) + var apiErr *codersdk.Error + if xerrors.As(err, &apiErr) { + builder.WriteString(fmt.Sprintf("\nError Message: %s\nError Detail: %s", apiErr.Message, apiErr.Detail)) + for _, v := range apiErr.Validations { + // Pretty sure this is not possible from the called endpoint, but just in case. + builder.WriteString(fmt.Sprintf("\n\tValidation: %s=%s", v.Field, v.Detail)) } } + builder.WriteString(fmt.Sprintf("\nError: %s", err.Error())) status.Report.Errors = []string{builder.String()} case err != nil: diff --git a/enterprise/coderd/schedule/user_test.go b/enterprise/coderd/schedule/user_test.go index 5e1685a42e2c2..30227840587a6 100644 --- a/enterprise/coderd/schedule/user_test.go +++ b/enterprise/coderd/schedule/user_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - "github.com/golang/mock/gomock" "github.com/google/uuid" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbmock" diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 3c141542fdfc7..ca70113744cff 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -687,6 +687,44 @@ func TestTemplates(t *testing.T) { require.Empty(t, template.DeprecationMessage) require.False(t, template.Deprecated) }) + + // Create a template, remove the group, see if an owner can + // still fetch the template. + t.Run("GetOnEveryoneRemove", func(t *testing.T) { + t.Parallel() + owner, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAccessControl: 1, + codersdk.FeatureTemplateRBAC: 1, + }, + }, + }) + + client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleTemplateAdmin()) + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) + + ctx := testutil.Context(t, testutil.WaitMedium) + err := client.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{ + UserPerms: nil, + GroupPerms: map[string]codersdk.TemplateRole{ + // OrgID is the everyone ID + first.OrganizationID.String(): codersdk.TemplateRoleDeleted, + }, + }) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err = owner.Template(ctx, template.ID) + require.NoError(t, err) + }) } func TestTemplateACL(t *testing.T) { @@ -808,6 +846,39 @@ func TestTemplateACL(t *testing.T) { require.Equal(t, http.StatusNotFound, cerr.StatusCode()) }) + t.Run("DisableEveryoneGroupAccess", func(t *testing.T) { + t.Parallel() + + client, admin := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, + }}) + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + //nolint:gocritic // non-template-admin cannot get template acl + acl, err := client.TemplateACL(ctx, template.ID) + require.NoError(t, err) + require.Equal(t, 1, len(acl.Groups)) + _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + DisableEveryoneGroupAccess: true, + }) + require.NoError(t, err) + + acl, err = client.TemplateACL(ctx, template.ID) + require.NoError(t, err) + require.Equal(t, 0, len(acl.Groups), acl.Groups) + }) + // Test that we do not return deleted users. t.Run("FilterDeletedUsers", func(t *testing.T) { t.Parallel() diff --git a/enterprise/coderd/workspaceproxy.go b/enterprise/coderd/workspaceproxy.go index 92be790b348e1..c229903adaca4 100644 --- a/enterprise/coderd/workspaceproxy.go +++ b/enterprise/coderd/workspaceproxy.go @@ -26,6 +26,7 @@ import ( "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/telemetry" "github.com/coder/coder/v2/coderd/workspaceapps" + "github.com/coder/coder/v2/coderd/workspaceapps/appurl" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/enterprise/coderd/proxyhealth" @@ -591,7 +592,7 @@ func (api *API) workspaceProxyRegister(rw http.ResponseWriter, r *http.Request) } if req.WildcardHostname != "" { - if _, err := httpapi.CompileHostnamePattern(req.WildcardHostname); err != nil { + if _, err := appurl.CompileHostnamePattern(req.WildcardHostname); err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Wildcard URL is invalid.", Detail: err.Error(), @@ -809,7 +810,7 @@ func (api *API) workspaceProxyDeregister(rw http.ResponseWriter, r *http.Request // @Summary Issue signed app token for reconnecting PTY // @ID issue-signed-app-token-for-reconnecting-pty // @Security CoderSessionToken -// @Tags Applications Enterprise +// @Tags Enterprise // @Accept json // @Produce json // @Param request body codersdk.IssueReconnectingPTYSignedTokenRequest true "Issue reconnecting PTY signed token request" @@ -930,6 +931,7 @@ func convertRegion(proxy database.WorkspaceProxy, status proxyhealth.ProxyStatus } func convertProxy(p database.WorkspaceProxy, status proxyhealth.ProxyStatus) codersdk.WorkspaceProxy { + now := dbtime.Now() if p.IsPrimary() { // Primary is always healthy since the primary serves the api that this // is returned from. @@ -939,8 +941,11 @@ func convertProxy(p database.WorkspaceProxy, status proxyhealth.ProxyStatus) cod ProxyHost: u.Host, Status: proxyhealth.Healthy, Report: codersdk.ProxyHealthReport{}, - CheckedAt: time.Now(), + CheckedAt: now, } + // For primary, created at / updated at are always 'now' + p.CreatedAt = now + p.UpdatedAt = now } if status.Status == "" { status.Status = proxyhealth.Unknown diff --git a/enterprise/coderd/workspaceproxy_test.go b/enterprise/coderd/workspaceproxy_test.go index 310e8bef96dec..17e17240dcace 100644 --- a/enterprise/coderd/workspaceproxy_test.go +++ b/enterprise/coderd/workspaceproxy_test.go @@ -19,6 +19,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" @@ -61,7 +62,7 @@ func TestRegions(t *testing.T) { require.NotEmpty(t, regions[0].IconURL) require.True(t, regions[0].Healthy) require.Equal(t, client.URL.String(), regions[0].PathAppURL) - require.Equal(t, appHostname, regions[0].WildcardHostname) + require.Equal(t, fmt.Sprintf("%s:%s", appHostname, client.URL.Port()), regions[0].WildcardHostname) // Ensure the primary region ID is constant. regions2, err := client.Regions(ctx) @@ -93,11 +94,16 @@ func TestRegions(t *testing.T) { deploymentID, err := db.GetDeploymentID(ctx) require.NoError(t, err, "get deployment ID") + // The default proxy is always called "primary". + primary, err := client.WorkspaceProxyByName(ctx, "primary") + require.NoError(t, err) + const proxyName = "hello" _ = coderdenttest.NewWorkspaceProxy(t, api, client, &coderdenttest.ProxyOptions{ Name: proxyName, AppHostname: appHostname + ".proxy", }) + approxCreateTime := dbtime.Now() proxy, err := db.GetWorkspaceProxyByName(ctx, proxyName) require.NoError(t, err) @@ -135,7 +141,7 @@ func TestRegions(t *testing.T) { require.NoError(t, err) require.Len(t, regions, 2) - // Region 0 is the primary require.Len(t, regions, 1) + // Region 0 is the primary require.NotEqual(t, uuid.Nil, regions[0].ID) require.Equal(t, regions[0].ID.String(), deploymentID) require.Equal(t, "primary", regions[0].Name) @@ -143,7 +149,12 @@ func TestRegions(t *testing.T) { require.NotEmpty(t, regions[0].IconURL) require.True(t, regions[0].Healthy) require.Equal(t, client.URL.String(), regions[0].PathAppURL) - require.Equal(t, appHostname, regions[0].WildcardHostname) + require.Equal(t, fmt.Sprintf("%s:%s", appHostname, client.URL.Port()), regions[0].WildcardHostname) + + // Ensure non-zero fields of the default proxy + require.NotZero(t, primary.Name) + require.NotZero(t, primary.CreatedAt) + require.NotZero(t, primary.UpdatedAt) // Region 1 is the proxy. require.NotEqual(t, uuid.Nil, regions[1].ID) @@ -154,6 +165,11 @@ func TestRegions(t *testing.T) { require.True(t, regions[1].Healthy) require.Equal(t, proxy.Url, regions[1].PathAppURL) require.Equal(t, proxy.WildcardHostname, regions[1].WildcardHostname) + + // Unfortunately need to wait to assert createdAt/updatedAt + <-time.After(testutil.WaitShort / 10) + require.WithinDuration(t, approxCreateTime, proxy.CreatedAt, testutil.WaitShort/10) + require.WithinDuration(t, approxCreateTime, proxy.UpdatedAt, testutil.WaitShort/10) }) t.Run("RequireAuth", func(t *testing.T) { diff --git a/enterprise/coderd/workspaceproxycoordinate.go b/enterprise/coderd/workspaceproxycoordinate.go index 501095d44477e..bf291e45cecfb 100644 --- a/enterprise/coderd/workspaceproxycoordinate.go +++ b/enterprise/coderd/workspaceproxycoordinate.go @@ -9,8 +9,8 @@ import ( "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/enterprise/tailnet" "github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk" + agpl "github.com/coder/coder/v2/tailnet" ) // @Summary Agent is legacy @@ -52,6 +52,21 @@ func (api *API) agentIsLegacy(rw http.ResponseWriter, r *http.Request) { func (api *API) workspaceProxyCoordinate(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() + version := "1.0" + qv := r.URL.Query().Get("version") + if qv != "" { + version = qv + } + if err := agpl.CurrentVersion.Validate(version); err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Unknown or unsupported API version", + Validations: []codersdk.ValidationError{ + {Field: "version", Detail: err.Error()}, + }, + }) + return + } + api.AGPL.WebsocketWaitMutex.Lock() api.AGPL.WebsocketWaitGroup.Add(1) api.AGPL.WebsocketWaitMutex.Unlock() @@ -66,14 +81,14 @@ func (api *API) workspaceProxyCoordinate(rw http.ResponseWriter, r *http.Request return } - id := uuid.New() - sub := (*api.AGPL.TailnetCoordinator.Load()).ServeMultiAgent(id) - ctx, nc := websocketNetConn(ctx, conn, websocket.MessageText) defer nc.Close() - err = tailnet.ServeWorkspaceProxy(ctx, nc, sub) + id := uuid.New() + err = api.tailnetService.ServeMultiAgentClient(ctx, version, nc, id) if err != nil { _ = conn.Close(websocket.StatusInternalError, err.Error()) + } else { + _ = conn.Close(websocket.StatusGoingAway, "") } } diff --git a/enterprise/dbcrypt/dbcrypt_internal_test.go b/enterprise/dbcrypt/dbcrypt_internal_test.go index cbe12e61f0c03..37fcc8cae55a3 100644 --- a/enterprise/dbcrypt/dbcrypt_internal_test.go +++ b/enterprise/dbcrypt/dbcrypt_internal_test.go @@ -9,9 +9,9 @@ import ( "io" "testing" - "github.com/golang/mock/gomock" "github.com/lib/pq" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbgen" diff --git a/enterprise/provisionerd/remoteprovisioners_test.go b/enterprise/provisionerd/remoteprovisioners_test.go index 1e1ca3d788b02..38c8bc1605fef 100644 --- a/enterprise/provisionerd/remoteprovisioners_test.go +++ b/enterprise/provisionerd/remoteprovisioners_test.go @@ -206,7 +206,6 @@ func TestRemoteConnector_Fuzz(t *testing.T) { case <-exec.done: // Connector hung up on the fuzzer } - require.Less(t, exec.bytesFuzzed, 2<<20, "should not allow more than 1 MiB") connectCtxCancel() var resp agpl.ConnectResponse select { diff --git a/enterprise/tailnet/coordinator.go b/enterprise/tailnet/coordinator.go deleted file mode 100644 index 687ec236b4a44..0000000000000 --- a/enterprise/tailnet/coordinator.go +++ /dev/null @@ -1,951 +0,0 @@ -package tailnet - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "html/template" - "io" - "net" - "net/http" - "sync" - "time" - - "github.com/google/uuid" - lru "github.com/hashicorp/golang-lru/v2" - "golang.org/x/exp/slices" - "golang.org/x/xerrors" - - "cdr.dev/slog" - "github.com/coder/coder/v2/coderd/database/pubsub" - "github.com/coder/coder/v2/coderd/util/slice" - "github.com/coder/coder/v2/codersdk" - agpl "github.com/coder/coder/v2/tailnet" - "github.com/coder/coder/v2/tailnet/proto" -) - -// NewCoordinator creates a new high availability coordinator -// that uses PostgreSQL pubsub to exchange handshakes. -func NewCoordinator(logger slog.Logger, ps pubsub.Pubsub) (agpl.Coordinator, error) { - ctx, cancelFunc := context.WithCancel(context.Background()) - - nameCache, err := lru.New[uuid.UUID, string](512) - if err != nil { - panic("make lru cache: " + err.Error()) - } - - coord := &haCoordinator{ - id: uuid.New(), - log: logger, - pubsub: ps, - closeFunc: cancelFunc, - close: make(chan struct{}), - nodes: map[uuid.UUID]*agpl.Node{}, - agentSockets: map[uuid.UUID]agpl.Queue{}, - agentToConnectionSockets: map[uuid.UUID]map[uuid.UUID]agpl.Queue{}, - agentNameCache: nameCache, - clients: map[uuid.UUID]agpl.Queue{}, - clientsToAgents: map[uuid.UUID]map[uuid.UUID]agpl.Queue{}, - legacyAgents: map[uuid.UUID]struct{}{}, - } - - if err := coord.runPubsub(ctx); err != nil { - return nil, xerrors.Errorf("run coordinator pubsub: %w", err) - } - - return coord, nil -} - -func (c *haCoordinator) ServeMultiAgent(id uuid.UUID) agpl.MultiAgentConn { - m := (&agpl.MultiAgent{ - ID: id, - AgentIsLegacyFunc: c.agentIsLegacy, - OnSubscribe: c.clientSubscribeToAgent, - OnUnsubscribe: c.clientUnsubscribeFromAgent, - OnNodeUpdate: c.clientNodeUpdate, - OnRemove: c.clientDisconnected, - }).Init() - c.addClient(id, m) - return m -} - -func (c *haCoordinator) addClient(id uuid.UUID, q agpl.Queue) { - c.mutex.Lock() - c.clients[id] = q - c.clientsToAgents[id] = map[uuid.UUID]agpl.Queue{} - c.mutex.Unlock() -} - -func (c *haCoordinator) clientSubscribeToAgent(enq agpl.Queue, agentID uuid.UUID) (*agpl.Node, error) { - c.mutex.Lock() - defer c.mutex.Unlock() - - c.initOrSetAgentConnectionSocketLocked(agentID, enq) - - node := c.nodes[enq.UniqueID()] - if node != nil { - err := c.sendNodeToAgentLocked(agentID, node) - if err != nil { - return nil, xerrors.Errorf("handle client update: %w", err) - } - } - - agentNode, ok := c.nodes[agentID] - // If we have the node locally, give it back to the multiagent. - if ok { - return agentNode, nil - } - - // If we don't have the node locally, notify other coordinators. - err := c.publishClientHello(agentID) - if err != nil { - return nil, xerrors.Errorf("publish client hello: %w", err) - } - - // nolint:nilnil - return nil, nil -} - -func (c *haCoordinator) clientUnsubscribeFromAgent(enq agpl.Queue, agentID uuid.UUID) error { - c.mutex.Lock() - defer c.mutex.Unlock() - - connectionSockets, ok := c.agentToConnectionSockets[agentID] - if !ok { - return nil - } - delete(connectionSockets, enq.UniqueID()) - if len(connectionSockets) == 0 { - delete(c.agentToConnectionSockets, agentID) - } - - return nil -} - -type haCoordinator struct { - id uuid.UUID - log slog.Logger - mutex sync.RWMutex - pubsub pubsub.Pubsub - close chan struct{} - closeFunc context.CancelFunc - - // nodes maps agent and connection IDs their respective node. - nodes map[uuid.UUID]*agpl.Node - // agentSockets maps agent IDs to their open websocket. - agentSockets map[uuid.UUID]agpl.Queue - // agentToConnectionSockets maps agent IDs to connection IDs of conns that - // are subscribed to updates for that agent. - agentToConnectionSockets map[uuid.UUID]map[uuid.UUID]agpl.Queue - - // clients holds a map of all clients connected to the coordinator. This is - // necessary because a client may not be subscribed into any agents. - clients map[uuid.UUID]agpl.Queue - // clientsToAgents is an index of clients to all of their subscribed agents. - clientsToAgents map[uuid.UUID]map[uuid.UUID]agpl.Queue - - // agentNameCache holds a cache of agent names. If one of them disappears, - // it's helpful to have a name cached for debugging. - agentNameCache *lru.Cache[uuid.UUID, string] - - // legacyAgents holda a mapping of all agents detected as legacy, meaning - // they only listen on codersdk.WorkspaceAgentIP. They aren't compatible - // with the new ServerTailnet, so they must be connected through - // wsconncache. - legacyAgents map[uuid.UUID]struct{} -} - -func (c *haCoordinator) Coordinate(ctx context.Context, _ uuid.UUID, _ string, _ agpl.TunnelAuth) (chan<- *proto.CoordinateRequest, <-chan *proto.CoordinateResponse) { - // HA Coordinator does NOT support v2 API and this is just here to appease the compiler and prevent - // panics while we build out v2 support elsewhere. We will retire the HA Coordinator in favor of - // PG Coordinator before we turn on the v2 API. - c.log.Warn(ctx, "v2 API invoked but unimplemented") - resp := make(chan *proto.CoordinateResponse) - close(resp) - req := make(chan *proto.CoordinateRequest) - go func() { - for { - if _, ok := <-req; !ok { - return - } - } - }() - return req, resp -} - -// Node returns an in-memory node by ID. -func (c *haCoordinator) Node(id uuid.UUID) *agpl.Node { - c.mutex.Lock() - defer c.mutex.Unlock() - node := c.nodes[id] - return node -} - -func (c *haCoordinator) clientLogger(id, agent uuid.UUID) slog.Logger { - return c.log.With(slog.F("client_id", id), slog.F("agent_id", agent)) -} - -func (c *haCoordinator) agentLogger(agent uuid.UUID) slog.Logger { - return c.log.With(slog.F("agent_id", agent)) -} - -// ServeClient accepts a WebSocket connection that wants to connect to an agent -// with the specified ID. -func (c *haCoordinator) ServeClient(conn net.Conn, id, agentID uuid.UUID) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - logger := c.clientLogger(id, agentID) - - tc := agpl.NewTrackedConn(ctx, cancel, conn, id, logger, id.String(), 0, agpl.QueueKindClient) - defer tc.Close() - - c.addClient(id, tc) - defer c.clientDisconnected(tc) - - agentNode, err := c.clientSubscribeToAgent(tc, agentID) - if err != nil { - return xerrors.Errorf("subscribe agent: %w", err) - } - - if agentNode != nil { - err := tc.Enqueue([]*agpl.Node{agentNode}) - if err != nil { - logger.Debug(ctx, "enqueue initial node", slog.Error(err)) - } - } - - go tc.SendUpdates() - - decoder := json.NewDecoder(conn) - // Indefinitely handle messages from the client websocket. - for { - err := c.handleNextClientMessage(id, decoder) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) { - return nil - } - return xerrors.Errorf("handle next client message: %w", err) - } - } -} - -func (c *haCoordinator) initOrSetAgentConnectionSocketLocked(agentID uuid.UUID, enq agpl.Queue) { - connectionSockets, ok := c.agentToConnectionSockets[agentID] - if !ok { - connectionSockets = map[uuid.UUID]agpl.Queue{} - c.agentToConnectionSockets[agentID] = connectionSockets - } - connectionSockets[enq.UniqueID()] = enq - c.clientsToAgents[enq.UniqueID()][agentID] = c.agentSockets[agentID] -} - -func (c *haCoordinator) clientDisconnected(enq agpl.Queue) { - c.mutex.Lock() - defer c.mutex.Unlock() - - for agentID := range c.clientsToAgents[enq.UniqueID()] { - connectionSockets, ok := c.agentToConnectionSockets[agentID] - if !ok { - continue - } - delete(connectionSockets, enq.UniqueID()) - if len(connectionSockets) == 0 { - delete(c.agentToConnectionSockets, agentID) - } - } - - delete(c.nodes, enq.UniqueID()) - delete(c.clients, enq.UniqueID()) - delete(c.clientsToAgents, enq.UniqueID()) -} - -func (c *haCoordinator) handleNextClientMessage(id uuid.UUID, decoder *json.Decoder) error { - var node agpl.Node - err := decoder.Decode(&node) - if err != nil { - return xerrors.Errorf("read json: %w", err) - } - - return c.clientNodeUpdate(id, &node) -} - -func (c *haCoordinator) clientNodeUpdate(id uuid.UUID, node *agpl.Node) error { - c.mutex.Lock() - defer c.mutex.Unlock() - // Update the node of this client in our in-memory map. If an agent entirely - // shuts down and reconnects, it needs to be aware of all clients attempting - // to establish connections. - c.nodes[id] = node - - for agentID, agentSocket := range c.clientsToAgents[id] { - if agentSocket == nil { - // If we don't own the agent locally, send it over pubsub to a node that - // owns the agent. - err := c.publishNodesToAgent(agentID, []*agpl.Node{node}) - if err != nil { - c.log.Error(context.Background(), "publish node to agent", slog.Error(err), slog.F("agent_id", agentID)) - } - } else { - // Write the new node from this client to the actively connected agent. - err := agentSocket.Enqueue([]*agpl.Node{node}) - if err != nil { - c.log.Error(context.Background(), "enqueue node to agent", slog.Error(err), slog.F("agent_id", agentID)) - } - } - } - - return nil -} - -func (c *haCoordinator) sendNodeToAgentLocked(agentID uuid.UUID, node *agpl.Node) error { - agentSocket, ok := c.agentSockets[agentID] - if !ok { - // If we don't own the agent locally, send it over pubsub to a node that - // owns the agent. - err := c.publishNodesToAgent(agentID, []*agpl.Node{node}) - if err != nil { - return xerrors.Errorf("publish node to agent") - } - return nil - } - err := agentSocket.Enqueue([]*agpl.Node{node}) - if err != nil { - return xerrors.Errorf("enqueue node: %w", err) - } - return nil -} - -// ServeAgent accepts a WebSocket connection to an agent that listens to -// incoming connections and publishes node updates. -func (c *haCoordinator) ServeAgent(conn net.Conn, id uuid.UUID, name string) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - logger := c.agentLogger(id) - c.agentNameCache.Add(id, name) - - c.mutex.Lock() - overwrites := int64(0) - // If an old agent socket is connected, we Close it to avoid any leaks. This - // shouldn't ever occur because we expect one agent to be running, but it's - // possible for a race condition to happen when an agent is disconnected and - // attempts to reconnect before the server realizes the old connection is - // dead. - oldAgentSocket, ok := c.agentSockets[id] - if ok { - overwrites = oldAgentSocket.Overwrites() + 1 - _ = oldAgentSocket.Close() - } - // This uniquely identifies a connection that belongs to this goroutine. - unique := uuid.New() - tc := agpl.NewTrackedConn(ctx, cancel, conn, unique, logger, name, overwrites, agpl.QueueKindAgent) - - // Publish all nodes on this instance that want to connect to this agent. - nodes := c.nodesSubscribedToAgent(id) - if len(nodes) > 0 { - err := tc.Enqueue(nodes) - if err != nil { - c.mutex.Unlock() - return xerrors.Errorf("enqueue nodes: %w", err) - } - } - c.agentSockets[id] = tc - for clientID := range c.agentToConnectionSockets[id] { - c.clientsToAgents[clientID][id] = tc - } - c.mutex.Unlock() - go tc.SendUpdates() - - // Tell clients on other instances to send a callmemaybe to us. - err := c.publishAgentHello(id) - if err != nil { - return xerrors.Errorf("publish agent hello: %w", err) - } - - defer func() { - c.mutex.Lock() - defer c.mutex.Unlock() - - // Only delete the connection if it's ours. It could have been - // overwritten. - if idConn, ok := c.agentSockets[id]; ok && idConn.UniqueID() == unique { - delete(c.agentSockets, id) - delete(c.nodes, id) - } - for clientID := range c.agentToConnectionSockets[id] { - c.clientsToAgents[clientID][id] = nil - } - }() - - decoder := json.NewDecoder(conn) - for { - node, err := c.handleAgentUpdate(id, decoder) - if err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, io.ErrClosedPipe) || errors.Is(err, context.Canceled) { - return nil - } - return xerrors.Errorf("handle next agent message: %w", err) - } - - err = c.publishAgentToNodes(id, node) - if err != nil { - return xerrors.Errorf("publish agent to nodes: %w", err) - } - } -} - -func (c *haCoordinator) nodesSubscribedToAgent(agentID uuid.UUID) []*agpl.Node { - sockets, ok := c.agentToConnectionSockets[agentID] - if !ok { - return nil - } - - nodes := make([]*agpl.Node, 0, len(sockets)) - for targetID := range sockets { - node, ok := c.nodes[targetID] - if !ok { - continue - } - nodes = append(nodes, node) - } - - return nodes -} - -func (c *haCoordinator) handleClientHello(id uuid.UUID) error { - c.mutex.Lock() - node, ok := c.nodes[id] - c.mutex.Unlock() - if !ok { - return nil - } - return c.publishAgentToNodes(id, node) -} - -func (c *haCoordinator) agentIsLegacy(agentID uuid.UUID) bool { - c.mutex.RLock() - _, ok := c.legacyAgents[agentID] - c.mutex.RUnlock() - return ok -} - -func (c *haCoordinator) handleAgentUpdate(id uuid.UUID, decoder *json.Decoder) (*agpl.Node, error) { - var node agpl.Node - err := decoder.Decode(&node) - if err != nil { - return nil, xerrors.Errorf("read json: %w", err) - } - - c.mutex.Lock() - // Keep a cache of all legacy agents. - if len(node.Addresses) > 0 && node.Addresses[0].Addr() == codersdk.WorkspaceAgentIP { - c.legacyAgents[id] = struct{}{} - } - - oldNode := c.nodes[id] - if oldNode != nil { - if oldNode.AsOf.After(node.AsOf) { - c.mutex.Unlock() - return oldNode, nil - } - } - c.nodes[id] = &node - connectionSockets, ok := c.agentToConnectionSockets[id] - if !ok { - c.mutex.Unlock() - return &node, nil - } - - // Publish the new node to every listening socket. - for _, connectionSocket := range connectionSockets { - _ = connectionSocket.Enqueue([]*agpl.Node{&node}) - } - - c.mutex.Unlock() - - return &node, nil -} - -// Close closes all of the open connections in the coordinator and stops the -// coordinator from accepting new connections. -func (c *haCoordinator) Close() error { - c.mutex.Lock() - defer c.mutex.Unlock() - select { - case <-c.close: - return nil - default: - } - close(c.close) - c.closeFunc() - - wg := sync.WaitGroup{} - - wg.Add(len(c.agentSockets)) - for _, socket := range c.agentSockets { - socket := socket - go func() { - _ = socket.CoordinatorClose() - wg.Done() - }() - } - - wg.Add(len(c.clients)) - for _, client := range c.clients { - client := client - go func() { - _ = client.CoordinatorClose() - wg.Done() - }() - } - - wg.Wait() - return nil -} - -func (c *haCoordinator) publishNodesToAgent(recipient uuid.UUID, nodes []*agpl.Node) error { - msg, err := c.formatCallMeMaybe(recipient, nodes) - if err != nil { - return xerrors.Errorf("format publish message: %w", err) - } - - err = c.pubsub.Publish("wireguard_peers", msg) - if err != nil { - return xerrors.Errorf("publish message: %w", err) - } - - return nil -} - -func (c *haCoordinator) publishAgentHello(id uuid.UUID) error { - msg, err := c.formatAgentHello(id) - if err != nil { - return xerrors.Errorf("format publish message: %w", err) - } - - err = c.pubsub.Publish("wireguard_peers", msg) - if err != nil { - return xerrors.Errorf("publish message: %w", err) - } - - return nil -} - -func (c *haCoordinator) publishClientHello(id uuid.UUID) error { - msg, err := c.formatClientHello(id) - if err != nil { - return xerrors.Errorf("format client hello: %w", err) - } - err = c.pubsub.Publish("wireguard_peers", msg) - if err != nil { - return xerrors.Errorf("publish client hello: %w", err) - } - return nil -} - -func (c *haCoordinator) publishAgentToNodes(id uuid.UUID, node *agpl.Node) error { - msg, err := c.formatAgentUpdate(id, node) - if err != nil { - return xerrors.Errorf("format publish message: %w", err) - } - - err = c.pubsub.Publish("wireguard_peers", msg) - if err != nil { - return xerrors.Errorf("publish message: %w", err) - } - - return nil -} - -func (c *haCoordinator) runPubsub(ctx context.Context) error { - messageQueue := make(chan []byte, 64) - cancelSub, err := c.pubsub.Subscribe("wireguard_peers", func(ctx context.Context, message []byte) { - select { - case messageQueue <- message: - case <-ctx.Done(): - return - } - }) - if err != nil { - return xerrors.Errorf("subscribe wireguard peers") - } - go func() { - for { - select { - case <-ctx.Done(): - return - case message := <-messageQueue: - c.handlePubsubMessage(ctx, message) - } - } - }() - - go func() { - defer cancelSub() - <-c.close - }() - - return nil -} - -func (c *haCoordinator) handlePubsubMessage(ctx context.Context, message []byte) { - sp := bytes.Split(message, []byte("|")) - if len(sp) != 4 { - c.log.Error(ctx, "invalid wireguard peer message", slog.F("msg", string(message))) - return - } - - var ( - coordinatorID = sp[0] - eventType = sp[1] - agentID = sp[2] - nodeJSON = sp[3] - ) - - sender, err := uuid.ParseBytes(coordinatorID) - if err != nil { - c.log.Error(ctx, "invalid sender id", slog.F("id", string(coordinatorID)), slog.F("msg", string(message))) - return - } - - // We sent this message! - if sender == c.id { - return - } - - switch string(eventType) { - case "callmemaybe": - agentUUID, err := uuid.ParseBytes(agentID) - if err != nil { - c.log.Error(ctx, "invalid agent id", slog.F("id", string(agentID))) - return - } - - c.mutex.Lock() - agentSocket, ok := c.agentSockets[agentUUID] - c.mutex.Unlock() - if !ok { - return - } - - // Socket takes a slice of Nodes, so we need to parse the JSON here. - var nodes []*agpl.Node - err = json.Unmarshal(nodeJSON, &nodes) - if err != nil { - c.log.Error(ctx, "invalid nodes JSON", slog.F("id", agentID), slog.Error(err), slog.F("node", string(nodeJSON))) - } - err = agentSocket.Enqueue(nodes) - if err != nil { - c.log.Error(ctx, "send callmemaybe to agent", slog.Error(err)) - return - } - case "clienthello": - agentUUID, err := uuid.ParseBytes(agentID) - if err != nil { - c.log.Error(ctx, "invalid agent id", slog.F("id", string(agentID))) - return - } - - err = c.handleClientHello(agentUUID) - if err != nil { - c.log.Error(ctx, "handle agent request node", slog.Error(err)) - return - } - case "agenthello": - agentUUID, err := uuid.ParseBytes(agentID) - if err != nil { - c.log.Error(ctx, "invalid agent id", slog.F("id", string(agentID))) - return - } - - c.mutex.RLock() - nodes := c.nodesSubscribedToAgent(agentUUID) - c.mutex.RUnlock() - if len(nodes) > 0 { - err := c.publishNodesToAgent(agentUUID, nodes) - if err != nil { - c.log.Error(ctx, "publish nodes to agent", slog.Error(err)) - return - } - } - case "agentupdate": - agentUUID, err := uuid.ParseBytes(agentID) - if err != nil { - c.log.Error(ctx, "invalid agent id", slog.F("id", string(agentID))) - return - } - - decoder := json.NewDecoder(bytes.NewReader(nodeJSON)) - _, err = c.handleAgentUpdate(agentUUID, decoder) - if err != nil { - c.log.Error(ctx, "handle agent update", slog.Error(err)) - return - } - default: - c.log.Error(ctx, "unknown peer event", slog.F("name", string(eventType))) - } -} - -// format:
{{ .ID }}
): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago, overwrites {{ .Overwrites }}
- {{ .ID }}
): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago {{ .ID }}
): created ? ago, write ? ago, overwrites ? {{ .ID }}
): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago {{ .ID }}
):
- {{ marshal .Node }}
- Try the following text out in a screen reader!
+
+ The physical pain of getting bonked on the head with a cartoon mallet
+ lasts precisely 593{" "}
+
+