Compare commits

..

77 Commits

Author SHA1 Message Date
Phorcys 885201bc1b Merge branch 'main' into phorcys/refactor-devcontainer-docs 2026-02-18 00:30:50 +01:00
Jake Howell bab99db9e7 fix: update <RequestLogsPage /> permissions check (#22129)
Closes #20965 

This pull-request enables a quick permission check that the user is
allowed to view the `<RequestLogsPage />` under the admin panel.
Previously, users would be able to view this page and browse their own
logs if they had this permission (which was fine), however now we've
decided as this is an admin page, they should only be able to do this
via the API/CLI not from the main admin panel.
2026-02-18 10:26:45 +11:00
Phorcys 089a1d6b25 chore: make fmt && `make lint
/markdown`
2026-02-17 23:24:31 +00:00
Phorcys 455e711e4d fix(docs/admin/integrations): fix link to comparison section 2026-02-17 23:23:04 +00:00
Phorcys 65e83992fe wip 2026-02-17 23:09:29 +00:00
Phorcys 50e672e480 chore: correct wording 2026-02-17 22:35:28 +00:00
Phorcys 26205e5850 docs: add dev container approach comparison table and CI/CD Pre-built section
Update the devcontainers index page to cover three approaches instead
of two: Dev Container CLI, Envbuilder, and CI/CD Pre-built.

- Change intro from 'two approaches' to 'multiple approaches'
- Add comparison table with feature-by-feature breakdown
- Expand Dev Container CLI section with detailed pros/cons
- Add maintenance mode notice to Envbuilder section
- Add new CI/CD Pre-built section with pros/cons and example link
2026-02-17 22:22:03 +00:00
Faur Ioan-Aurel 2ee54b0af1 fix(site): redirect unauthorized users during oauth login (#22101)
The login page component incorrectly uses client-side routing to handle
redirects to /oauth2/authorize. Since this path is not defined as a
route in the react application but as a backend endpoint for the OAuth2
provider flow, the frontend displays a 404 "Route not found" error.

- resolves #22097

<!--

If you have used AI to produce some or all of this PR, please ensure you
have read our [AI Contribution
guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING)
before submitting.

-->
2026-02-17 19:04:19 +02:00
Danielle Maywood d737f8c104 feat(cli): add coder task resume command (#22066)
Complements https://github.com/coder/coder/pull/22012 by adding a `coder task resume` command
2026-02-17 16:24:13 +00:00
Cian Johnston f8eea54e97 fix(coderd): use BuildReasonTaskAutoPause for task workspaces (#22126)
Relates to https://github.com/coder/internal/issues/1252

When a workspace with a TaskID hits its deadline, use
BuildReasonTaskAutoPause instead of BuildReasonAutostop. This allows
downstream systems to distinguish between regular autostop and task
workspace pauses.

Created by Mux using Opus 4.5.
2026-02-17 15:11:04 +00:00
Paweł Banaszewski 90c11f3386 feat: add client column to aibridge_interceptions table (#21839)
Adds `client` column to `aibridge_interceptions` table. It is set accordingly to what is passed from AI Bridge in `RecordInterception`.
Adds interception filtering by `client` value.

Depends on: https://github.com/coder/aibridge/pull/158
Updates aibridge library to include this change.

Fixes: https://github.com/coder/aibridge/issues/31
2026-02-17 15:43:02 +01:00
blinkagent[bot] 81a928915c docs: remove outdated Toolbox log level persistence warning (#22139)
Remove the warning about JetBrains Toolbox not persisting log level
configuration between restarts.

As of JetBrains Toolbox 3.2, log level configuration now persists
between restarts, making this warning outdated.

Created on behalf of @matifali

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-02-17 19:17:22 +05:00
Cian Johnston 4a3304fc38 feat(cli)!: expire tokens by default (#21783)
## Summary

> NOTE: Calling this out as a breaking change in case existing consumers
of the CLI depend on being able to see expired tokens OR being able to
delete tokens immediately.

Updates the `coder tokens rm` command to immediately expire a token by
ID, preserving the token record for audit trail purposes. Tokens can
still be deleted by passing `--delete`.

## Problem

During an incident on dev.coder.com, operators needed to urgently expire
an API key that was stuck in a hot loop. The only way to do this was via
direct database access:

```sql
UPDATE api_keys SET expires_at = NOW() WHERE id = '...';
```

This is not ideal for operators who may not have direct DB access or
want to avoid manual SQL.

## Solution

This PR adds:

- **API endpoint**: `PUT /api/v2/users/{user}/keys/{keyid}/expire` -
Sets the token's `expires_at` to now
- **SDK method**: `ExpireAPIKey(ctx, userID, keyID)` 
- **Updates CLI**: `coder tokens rm <name|id|token>` now _expires_ by
default. You can still delete by passing the `--delete` flag. The `coder
tokens list` command now also hides expired tokens by default. You can
`--include-expired` if needed to include them.
- **Audit logging**: The expire action is logged with old and new key
states

## Test plan

- Tests cover: owner expiring own token, admin expiring other user's
token, non-admin cannot expire other's token, 404 for non-existent token

Closes #21782

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-17 13:16:46 +00:00
Jake Howell a5f3acac2f fix: permissions check on <TemplateInsightsPage /> (#22110)
Closes #20859 

This page previously wasn't rendered to the user, however, there is a
possibility that they can navigate to this page and things will end up
in `<Spinner />`s until the requests ultimately fail. We can mitigate
this problem by showing them the `<RequirePermission />` modal.

<img width="1456" height="861" alt="image"
src="https://github.com/user-attachments/assets/57195643-ad55-4340-9c97-f8247b05a13b"
/>
2026-02-17 13:46:40 +11:00
Atif Ali 63563e57db docs: add registry mirroring guide for Artifactory (#22025)
Verified to be working locally.

---------

Co-authored-by: Phorcys <57866459+phorcys420@users.noreply.github.com>
2026-02-16 18:29:48 +01:00
dependabot[bot] b40ebfb7e8 chore: bump google.golang.org/grpc from 1.78.0 to 1.79.1 (#22122)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from
1.78.0 to 1.79.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's
releases</a>.</em></p>
<blockquote>
<h2>Release 1.79.1</h2>
<h1>Bug Fixes</h1>
<ul>
<li>grpc: Remove the <code>-dev</code> suffix from the User-Agent
header. (<a
href="https://redirect.github.com/grpc/grpc-go/pull/8902">grpc/grpc-go#8902</a>)</li>
</ul>
<h2>Release 1.79.0</h2>
<h1>API Changes</h1>
<ul>
<li>mem: Add experimental API <code>SetDefaultBufferPool</code> to
change the default buffer pool. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8806">#8806</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/vanja-p"><code>@​vanja-p</code></a></li>
</ul>
</li>
<li>experimental/stats: Update <code>MetricsRecorder</code> to require
embedding the new <code>UnimplementedMetricsRecorder</code> (a no-op
struct) in all implementations for forward compatibility. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8780">#8780</a>)</li>
</ul>
<h1>Behavior Changes</h1>
<ul>
<li>balancer/weightedtarget: Remove handling of <code>Addresses</code>
and only handle <code>Endpoints</code> in resolver updates. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8841">#8841</a>)</li>
</ul>
<h1>New Features</h1>
<ul>
<li>experimental/stats: Add support for asynchronous gauge metrics
through the new <code>AsyncMetricReporter</code> and
<code>RegisterAsyncReporter</code> APIs. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8780">#8780</a>)</li>
<li>pickfirst: Add support for weighted random shuffling of endpoints,
as described in <a
href="https://redirect.github.com/grpc/proposal/pull/535">gRFC A113</a>.
<ul>
<li>This is enabled by default, and can be turned off using the
environment variable
<code>GRPC_EXPERIMENTAL_PF_WEIGHTED_SHUFFLING</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8864">#8864</a>)</li>
</ul>
</li>
<li>xds: Implement <code>:authority</code> rewriting, as specified in <a
href="https://github.com/grpc/proposal/blob/master/A81-xds-authority-rewriting.md">gRFC
A81</a>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8779">#8779</a>)</li>
<li>balancer/randomsubsetting: Implement the
<code>random_subsetting</code> LB policy, as specified in <a
href="https://github.com/grpc/proposal/blob/master/A68-random-subsetting.md">gRFC
A68</a>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8650">#8650</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/marek-szews"><code>@​marek-szews</code></a></li>
</ul>
</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>credentials/tls: Fix a bug where the port was not stripped from the
authority override before validation. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8726">#8726</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/Atul1710"><code>@​Atul1710</code></a></li>
</ul>
</li>
<li>xds/priority: Fix a bug causing delayed failover to lower-priority
clusters when a higher-priority cluster is stuck in
<code>CONNECTING</code> state. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8813">#8813</a>)</li>
<li>health: Fix a bug where health checks failed for clients using
legacy compression options (<code>WithDecompressor</code> or
<code>RPCDecompressor</code>). (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8765">#8765</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/sanki92"><code>@​sanki92</code></a></li>
</ul>
</li>
<li>transport: Fix an issue where the HTTP/2 server could skip header
size checks when terminating a stream early. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8769">#8769</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/joybestourous"><code>@​joybestourous</code></a></li>
</ul>
</li>
<li>server: Propagate status detail headers, if available, when
terminating a stream during request header processing. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8754">#8754</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/joybestourous"><code>@​joybestourous</code></a></li>
</ul>
</li>
</ul>
<h1>Performance Improvements</h1>
<ul>
<li>credentials/alts: Optimize read buffer alignment to reduce copies.
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8791">#8791</a>)</li>
<li>mem: Optimize pooling and creation of <code>buffer</code> objects.
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8784">#8784</a>)</li>
<li>transport: Reduce slice re-allocations by reserving slice capacity.
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8797">#8797</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/grpc/grpc-go/commit/782f2de44f597af18a120527e7682a6670d84289"><code>782f2de</code></a>
Change version to 1.79.1 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8902">#8902</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/850eccbb2257bd2de6ac28ee88a7172ab6175629"><code>850eccb</code></a>
Change version to 1.79.1-dev (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8851">#8851</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/765ff056b6890f6c8341894df4e9668e9bfc18ef"><code>765ff05</code></a>
Change version to 1.79.0 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8850">#8850</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/68804be0e78ed0365bb5a576dedc12e2168ed63e"><code>68804be</code></a>
Cherry pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/8864">#8864</a> to
v1.79.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8896">#8896</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/0381eb650acdae8e423473e64eef07693fe36305"><code>0381eb6</code></a>
xds: Support <code>:authority</code> header rewriting for LOGICAL_DNS
clusters (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8822">#8822</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/90f571db95a0ec223ec45187f7399a06ccdc10cf"><code>90f571d</code></a>
xds: remove references to ResolverState.Addresses (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8841">#8841</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/679565f9ae655079807f5ab10e07f41acd2af943"><code>679565f</code></a>
xds: remove <code>HashKey</code> field from
<code>xdsresource.Endpoint</code> struct (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8844">#8844</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/bb2073d1e5551b900763979e08e1c11a47a8f150"><code>bb2073d</code></a>
mem: Allow overriding the default buffer pool. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8806">#8806</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/bd4444a0a2fdd66245f9e0f0d140aafb5b49044c"><code>bd4444a</code></a>
Fix flaky <code>TestServer_RedundantUpdateSuppression</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/8839">#8839</a>)</li>
<li><a
href="https://github.com/grpc/grpc-go/commit/623b3f000b3625aa4a1413f90add1ea367db17c2"><code>623b3f0</code></a>
test: add regression test for RecvMsg() error shadowing <a
href="https://redirect.github.com/grpc/grpc-go/issues/7510">#7510</a>
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/8820">#8820</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/grpc/grpc-go/compare/v1.78.0...v1.79.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/grpc&package-manager=go_modules&previous-version=1.78.0&new-version=1.79.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 13:28:28 +00:00
dependabot[bot] 06cfe2705a ci: bump the github-actions group with 3 updates (#22125)
Bumps the github-actions group with 3 updates:
[step-security/harden-runner](https://github.com/step-security/harden-runner),
[depot/setup-action](https://github.com/depot/setup-action) and
[depot/build-push-action](https://github.com/depot/build-push-action).

Updates `step-security/harden-runner` from 2.14.1 to 2.14.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/step-security/harden-runner/releases">step-security/harden-runner's
releases</a>.</em></p>
<blockquote>
<h2>v2.14.2</h2>
<h2>What's Changed</h2>
<p>Security fix: Fixed a medium severity vulnerability where outbound
network connections using sendto, sendmsg, and sendmmsg socket system
calls could bypass audit logging when using egress-policy: audit. This
issue only affects the Community Tier in audit mode; block mode and
Enterprise Tier were not affected. See <a
href="https://github.com/step-security/harden-runner/security/advisories/GHSA-cpmj-h4f6-r6pq">GHSA-cpmj-h4f6-r6pq</a>
for details.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.14.1...v2.14.2">https://github.com/step-security/harden-runner/compare/v2.14.1...v2.14.2</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/step-security/harden-runner/commit/5ef0c079ce82195b2a36a210272d6b661572d83e"><code>5ef0c07</code></a>
Merge pull request <a
href="https://redirect.github.com/step-security/harden-runner/issues/635">#635</a>
from step-security/rc-34</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/eb43c7b3fd5a30c42ff1ab84b494f1cc6c7cc3b6"><code>eb43c7b</code></a>
update agent</li>
<li>See full diff in <a
href="https://github.com/step-security/harden-runner/compare/e3f713f2d8f53843e71c69a996d56f51aa9adfb9...5ef0c079ce82195b2a36a210272d6b661572d83e">compare
view</a></li>
</ul>
</details>
<br />

Updates `depot/setup-action` from 1.6.0 to 1.7.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/depot/setup-action/releases">depot/setup-action's
releases</a>.</em></p>
<blockquote>
<h2>v1.7.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update release workflow to ubuntu-latest (<a
href="https://redirect.github.com/depot/setup-action/issues/19">#19</a>)
<a
href="https://github.com/jacobwgillespie"><code>@​jacobwgillespie</code></a></li>
</ul>
<h2>v1.7.0</h2>
<h2>What's Changed</h2>
<ul>
<li>chore: update node to v24 (<a
href="https://redirect.github.com/depot/setup-action/issues/18">#18</a>)
<a
href="https://github.com/WitoDelnat"><code>@​WitoDelnat</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/depot/setup-action/commit/15c09a5f77a0840ad4bce955686522a257853461"><code>15c09a5</code></a>
Merge pull request <a
href="https://redirect.github.com/depot/setup-action/issues/19">#19</a>
from depot/jacobwgillespie-patch-1</li>
<li><a
href="https://github.com/depot/setup-action/commit/3194a53ed0aa42e00bc88f2411ab49d3f60219ba"><code>3194a53</code></a>
Update release workflow to ubuntu-latest</li>
<li><a
href="https://github.com/depot/setup-action/commit/c0b08c3ba30137f89e75961508b587484558ff0f"><code>c0b08c3</code></a>
Merge pull request <a
href="https://redirect.github.com/depot/setup-action/issues/18">#18</a>
from depot/wito/dep-2955-update-our-actions-to-use-nod...</li>
<li><a
href="https://github.com/depot/setup-action/commit/23e67ebf8b5a313459580616cd34c7787607ac86"><code>23e67eb</code></a>
chore: update action dependencies</li>
<li><a
href="https://github.com/depot/setup-action/commit/0886069e678860faf0d22c46e9f154d35beaa529"><code>0886069</code></a>
chore: update node to v24</li>
<li>See full diff in <a
href="https://github.com/depot/setup-action/compare/b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5...15c09a5f77a0840ad4bce955686522a257853461">compare
view</a></li>
</ul>
</details>
<br />

Updates `depot/build-push-action` from 1.16.2 to 1.17.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/depot/build-push-action/releases">depot/build-push-action's
releases</a>.</em></p>
<blockquote>
<h2>v1.17.0</h2>
<h2>What's Changed</h2>
<ul>
<li>chore: update node to v24 (<a
href="https://redirect.github.com/depot/build-push-action/issues/46">#46</a>)
<a
href="https://github.com/WitoDelnat"><code>@​WitoDelnat</code></a></li>
<li>Fix typo (<a
href="https://redirect.github.com/depot/build-push-action/issues/45">#45</a>)
<a href="https://github.com/gavrie"><code>@​gavrie</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/depot/build-push-action/commit/5f3b3c2e5a00f0093de47f657aeaefcedff27d18"><code>5f3b3c2</code></a>
Merge pull request <a
href="https://redirect.github.com/depot/build-push-action/issues/46">#46</a>
from depot/wito/dep-2955-update-our-actions-to-use-nod...</li>
<li><a
href="https://github.com/depot/build-push-action/commit/1c8a5de5bf842e36234a9f65b76b66173541193f"><code>1c8a5de</code></a>
chore: update actions dependency</li>
<li><a
href="https://github.com/depot/build-push-action/commit/88deb2d202089ac6bada8368187086a1d5cb6daa"><code>88deb2d</code></a>
chore: update node to v24</li>
<li><a
href="https://github.com/depot/build-push-action/commit/eb4edcff909a80720d31732b633e4691b0c7cf02"><code>eb4edcf</code></a>
Merge pull request <a
href="https://redirect.github.com/depot/build-push-action/issues/45">#45</a>
from gavrie/fix-typo</li>
<li><a
href="https://github.com/depot/build-push-action/commit/b7a09de6c74816aaa562b09c2d4082c63c68629a"><code>b7a09de</code></a>
Fix typo</li>
<li>See full diff in <a
href="https://github.com/depot/build-push-action/compare/9785b135c3c76c33db102e45be96a25ab55cd507...5f3b3c2e5a00f0093de47f657aeaefcedff27d18">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 13:24:16 +00:00
dependabot[bot] c247dc04a7 chore: bump github.com/charmbracelet/bubbles from 0.21.0 to 1.0.0 (#22123)
Bumps
[github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles)
from 0.21.0 to 1.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/charmbracelet/bubbles/releases">github.com/charmbracelet/bubbles's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.0</h2>
<p>This is just an honorary release of Bubbles v1. Stay tuned for the
next major version 🫧</p>
<h2>Changelog</h2>
<h3>Fixed</h3>
<ul>
<li>d0166363eb8176b331de98dba1d6e997560f216f: fix: changed 'recieve' to
'receive' for 100% quality of Go Report Card (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/881">#881</a>)
(<a href="https://github.com/Atennop1"><code>@​Atennop1</code></a>)</li>
</ul>
<hr />
<p><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML
omitted --></p>
<p>Thoughts? Questions? We love hearing from you. Feel free to reach out
on <a href="https://x.com/charmcli">X</a>, <a
href="https://charm.land/discord">Discord</a>, <a
href="https://charm.land/slack">Slack</a>, <a
href="https://mastodon.social/@charmcli">The Fediverse</a>, <a
href="https://bsky.app/profile/charm.land">Bluesky</a>.</p>
<h2>v0.21.1</h2>
<h2>Changelog</h2>
<h3>New!</h3>
<ul>
<li>dff42ddb7cf28f022da475c69dba2e74f75af34d: feat: update keybindings
in list setSize method (<a
href="https://github.com/Broderick-Westrope"><code>@​Broderick-Westrope</code></a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>c376ce3ef18cc26bbf1f6338cc8518ae329a18d6: fix(cursor): fix data race
on blinkTag (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/784">#784</a>)
(<a
href="https://github.com/DryHumour"><code>@​DryHumour</code></a>)</li>
<li>11d52ca426e5c594f7c6c10766935a7f30a83225: fix(table): preventing
cursor from being out-of-bounds. (<a
href="https://github.com/s0ders"><code>@​s0ders</code></a>)</li>
<li>49ff5c03b7bada572da36c79269dc15ab03d569b: fix(textinput): improve
placeholder (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/768">#768</a>)
(<a href="https://github.com/caarlos0"><code>@​caarlos0</code></a>)</li>
<li>7c44f63d3185e6f1d795e9369ba85185e6efe956: v1: fix(list): ensure
correct cursor positions with page/cursor methods (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/831">#831</a>)
(<a
href="https://github.com/lrstanley"><code>@​lrstanley</code></a>)</li>
</ul>
<h3>Docs</h3>
<ul>
<li>7fcf75da535ee7db938586044a02f0f74f40339e: docs(readme): update
footer image and copyright date (<a
href="https://github.com/meowgorithm"><code>@​meowgorithm</code></a>)</li>
<li>d4feefed7d674edbfbc8f09e99c56704706038c5: docs: remove Charm Cloud
reference (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/785">#785</a>)
(<a
href="https://github.com/ShalokShalom"><code>@​ShalokShalom</code></a>)</li>
</ul>
<h3>Other stuff</h3>
<ul>
<li>daab808a4d85e0b616ca9e30c1c5d9acd365aa02: ci: sync dependabot config
(<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/786">#786</a>)
(<a href="https://github.com/charmcli"><code>@​charmcli</code></a>)</li>
<li>4b2d311076480670a00b3f24fd9ad280c35c7c57: ci: sync dependabot config
(<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/835">#835</a>)
(<a href="https://github.com/charmcli"><code>@​charmcli</code></a>)</li>
<li>8562e9075fb87edf45e99c5d63a6610254d6c6e7: ci: sync golangci-lint
config (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/781">#781</a>)
(<a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot])</li>
<li>f54a125f7decd8fefa0db4a0853720200d50a631: test(table): improve table
unit tests (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/601">#601</a>)
(<a
href="https://github.com/Broderick-Westrope"><code>@​Broderick-Westrope</code></a>)</li>
</ul>
<hr />
<p><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML
omitted --></p>
<p>Thoughts? Questions? We love hearing from you. Feel free to reach out
on <a href="https://x.com/charmcli">X</a>, <a
href="https://charm.land/discord">Discord</a>, <a
href="https://charm.land/slack">Slack</a>, <a
href="https://mastodon.social/@charmcli">The Fediverse</a>, <a
href="https://bsky.app/profile/charm.land">Bluesky</a>.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/4824effc3f91c9517c776d8200ef99a1207136e0"><code>4824eff</code></a>
chore(deps): bump github.com/charmbracelet/x/ansi in the all group (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/859">#859</a>)</li>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/d0166363eb8176b331de98dba1d6e997560f216f"><code>d016636</code></a>
fix: changed 'recieve' to 'receive' for 100% quality of Go Report Card
(<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/881">#881</a>)</li>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/9329772de61d80756b4f1ea3acea4000a499bf71"><code>9329772</code></a>
chore: update dependencies</li>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/ff8b5a8e17c91972211d0b9f03e7764ddaa2f6d0"><code>ff8b5a8</code></a>
chore(deps): bump actions/checkout from 5 to 6 in the all group (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/863">#863</a>)</li>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/62c791108e041076f9ec79a5f980d87a75a51ffa"><code>62c7911</code></a>
chore(deps): bump the all group with 2 updates (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/855">#855</a>)</li>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/49ff5c03b7bada572da36c79269dc15ab03d569b"><code>49ff5c0</code></a>
fix(textinput): improve placeholder (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/768">#768</a>)</li>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/d6934a175bbbc1154ba37f8030d5b4bd8ecd876c"><code>d6934a1</code></a>
chore(deps): bump github.com/mattn/go-runewidth in the all group (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/852">#852</a>)</li>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/f2d12667c41159491f8bdbac2f301db032073d05"><code>f2d1266</code></a>
chore(deps): bump github.com/charmbracelet/bubbletea in the all group
(<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/850">#850</a>)</li>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/5caedd7607ed3e58a5baac7d0c5bdaacee5115d6"><code>5caedd7</code></a>
chore(deps): bump the all group with 2 updates (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/848">#848</a>)</li>
<li><a
href="https://github.com/charmbracelet/bubbles/commit/cfdc19ba97aea803f26c3775f2518c0dfd1b940a"><code>cfdc19b</code></a>
chore(deps): bump actions/setup-go from 5 to 6 in the all group (<a
href="https://redirect.github.com/charmbracelet/bubbles/issues/842">#842</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/charmbracelet/bubbles/compare/v0.21.0...v1.0.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/charmbracelet/bubbles&package-manager=go_modules&previous-version=0.21.0&new-version=1.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 13:15:52 +00:00
dependabot[bot] b12b389455 chore: bump the x group with 6 updates (#22120)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps the x group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.47.0` |
`0.48.0` |
| [golang.org/x/mod](https://github.com/golang/mod) | `0.32.0` |
`0.33.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.49.0` |
`0.50.0` |
| [golang.org/x/term](https://github.com/golang/term) | `0.39.0` |
`0.40.0` |
| [golang.org/x/text](https://github.com/golang/text) | `0.33.0` |
`0.34.0` |
| [golang.org/x/tools](https://github.com/golang/tools) | `0.41.0` |
`0.42.0` |

Updates `golang.org/x/crypto` from 0.47.0 to 0.48.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/crypto/commit/e08b06753d6a72f1fe375b6e0fefefb39917c165"><code>e08b067</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/crypto/commit/7d0074ccc6f17acbf2ebb10db06d492e08f887dc"><code>7d0074c</code></a>
scrypt: fix panic on parameters &lt;= 0</li>
<li>See full diff in <a
href="https://github.com/golang/crypto/compare/v0.47.0...v0.48.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/mod` from 0.32.0 to 0.33.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/mod/commit/27761a2ad800eabb86324226bb5e39adb07759da"><code>27761a2</code></a>
go.mod: update golang.org/x dependencies</li>
<li>See full diff in <a
href="https://github.com/golang/mod/compare/v0.32.0...v0.33.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/net` from 0.49.0 to 0.50.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/net/commit/ebddb99633e0fc35d135f62e9400678492c1d3be"><code>ebddb99</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/net/commit/4a490d4f5331699af2b0987af2e4cea9925f65af"><code>4a490d4</code></a>
internal/http3: add Expect: 100-continue support to ClientConn</li>
<li><a
href="https://github.com/golang/net/commit/73fe7011adb0a690693f0318325c60d477c7bb3a"><code>73fe701</code></a>
internal/http3: add Expect: 100-continue support to Server</li>
<li><a
href="https://github.com/golang/net/commit/af0c9df79dbe35108ead105e4ca28133551e4d1a"><code>af0c9df</code></a>
html: add NodeType.String() method</li>
<li><a
href="https://github.com/golang/net/commit/e02fb33933734f5c17f8474ef9f54461e776d119"><code>e02fb33</code></a>
internal/http3: make responseWriter.Flush write headers if not done
yet</li>
<li><a
href="https://github.com/golang/net/commit/da558ff100e05eb3fd3c94d2f978c062edc070a2"><code>da558ff</code></a>
internal/http3: ensure bodyReader cannot be read after being closed</li>
<li><a
href="https://github.com/golang/net/commit/d7c76faf077586c8657a8bdd404484c090764e2b"><code>d7c76fa</code></a>
internal/http3: make responseWriter behave closer to other
http.ResponseWriter</li>
<li><a
href="https://github.com/golang/net/commit/64b3af9625ad94ce3f68ac1ab80733f691a49319"><code>64b3af9</code></a>
http2: prevent transport deadlock due to WINDOW_UPDATE that exceeds
limit</li>
<li><a
href="https://github.com/golang/net/commit/1973e8da2d578cd964f4c1df41ed8c553f1e17b7"><code>1973e8d</code></a>
internal/http3: add Server support for handling HEAD requests</li>
<li><a
href="https://github.com/golang/net/commit/57ea86db083ff804aa7dfca3489c0f965ab3b0da"><code>57ea86d</code></a>
icmp, internal/socket, ipv4, ipv6: use binary.NativeEndian</li>
<li>Additional commits viewable in <a
href="https://github.com/golang/net/compare/v0.49.0...v0.50.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/term` from 0.39.0 to 0.40.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/term/commit/3aff3041f556e280e3e814347086e94b8ab76b95"><code>3aff304</code></a>
go.mod: update golang.org/x dependencies</li>
<li>See full diff in <a
href="https://github.com/golang/term/compare/v0.39.0...v0.40.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/text` from 0.33.0 to 0.34.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/text/commit/817fba9abd337b4d9097b10c61a540c74feaaeff"><code>817fba9</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/text/commit/3264de9174be11be843825a6dae979beffb483a2"><code>3264de9</code></a>
all: clean up old Go hacks</li>
<li><a
href="https://github.com/golang/text/commit/74af29835a6da65bf18d985619fa645e04a01549"><code>74af298</code></a>
all: fix tags in remaining Unicode tables</li>
<li><a
href="https://github.com/golang/text/commit/117e03b3060d70a9f514eef78f07e5210cc44a4c"><code>117e03b</code></a>
all: delete old Unicode tables</li>
<li><a
href="https://github.com/golang/text/commit/9463ea4393e969a59c7a0af1b497b4361ff1dc52"><code>9463ea4</code></a>
all: update to Unicode 17</li>
<li><a
href="https://github.com/golang/text/commit/7278b25aaff94e106b7de0bfaaa4a1861d3b3c89"><code>7278b25</code></a>
internal/export/idna: update for post-Unicode 10 idna changes</li>
<li><a
href="https://github.com/golang/text/commit/f964ad80f9e4185588298009b5195d8ecfe34d1b"><code>f964ad8</code></a>
internal/export/idna: delete old code</li>
<li><a
href="https://github.com/golang/text/commit/678d34e5c1bfbefd51bbd8e6a639744c36be73fd"><code>678d34e</code></a>
unicode/norm: preserve QC Maybe bit in packed forminfo</li>
<li>See full diff in <a
href="https://github.com/golang/text/compare/v0.33.0...v0.34.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `golang.org/x/tools` from 0.41.0 to 0.42.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/tools/commit/009367f5c17a8d4c45a961a3a509277190a9a6f0"><code>009367f</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/tools/commit/2182926e30144d252c74753cb717211a189309e9"><code>2182926</code></a>
go/ast/inspector: add Cursor.ParentEdge{Kind,Index} methods</li>
<li><a
href="https://github.com/golang/tools/commit/27020ac4d8e30db1069a7e926db41a51ab041241"><code>27020ac</code></a>
internal/server: add module upgrade pathway after vulncheck
scanning</li>
<li><a
href="https://github.com/golang/tools/commit/c4ec0f5f00386bf3fd47d80f6247a6a49e7b7c55"><code>c4ec0f5</code></a>
internal/server: list vulnerabilities within vulncheck prompt</li>
<li><a
href="https://github.com/golang/tools/commit/80d17157f262039c88c33b6d6864e0d53fb7abb1"><code>80d1715</code></a>
gopls/internal/protocol: add document uri field type</li>
<li><a
href="https://github.com/golang/tools/commit/0e23509d6eb3ecd0590130fcffe2522bb0e262d6"><code>0e23509</code></a>
gopls/doc: update link to Acme LSP plugin</li>
<li><a
href="https://github.com/golang/tools/commit/7b3ed75de0778c292b3f55c661cce98ce6028f94"><code>7b3ed75</code></a>
gopls/internal/server: respect SemanticTokens option during
initialization</li>
<li><a
href="https://github.com/golang/tools/commit/fddd4a6d592a2e90c0c329d7da34adfee9eed711"><code>fddd4a6</code></a>
gopls/filecache: prevent premature CAS file eviction</li>
<li><a
href="https://github.com/golang/tools/commit/e3a69ffcdbb984f50100e76ebca6ff53cf88de9c"><code>e3a69ff</code></a>
gopls/internal/golang: refactor.inline.variable: add parens</li>
<li><a
href="https://github.com/golang/tools/commit/955d132a29b63e5fdad701ff278c420250858cb0"><code>955d132</code></a>
gopls/internal/golang: migrate pkgdoc to cursor</li>
<li>Additional commits viewable in <a
href="https://github.com/golang/tools/compare/v0.41.0...v0.42.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 13:12:32 +00:00
dependabot[bot] ca1016c6ca chore: bump google.golang.org/api from 0.265.0 to 0.266.0 (#22121)
Bumps
[google.golang.org/api](https://github.com/googleapis/google-api-go-client)
from 0.265.0 to 0.266.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/googleapis/google-api-go-client/releases">google.golang.org/api's
releases</a>.</em></p>
<blockquote>
<h2>v0.266.0</h2>
<h2><a
href="https://github.com/googleapis/google-api-go-client/compare/v0.265.0...v0.266.0">0.266.0</a>
(2026-02-10)</h2>
<h3>Features</h3>
<ul>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3483">#3483</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/a3a61ce2214c8d18bb640c724fae2cda8cb77b58">a3a61ce</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3485">#3485</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/200d1409ecc830131f0b5b92fd59708fef24dd8e">200d140</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3486">#3486</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/870909e466b1bf8172dfe9bd5c096b1df45b0491">870909e</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3487">#3487</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/6018e80ff5cadadb81c7b7be9f5de01b4b4c2132">6018e80</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3489">#3489</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/402353be95579bccda6b6623e67e9f028163905b">402353b</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3490">#3490</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/49c652fb9c5e08c9d1a2587f41017b6011dc03da">49c652f</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md">google.golang.org/api's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/googleapis/google-api-go-client/compare/v0.265.0...v0.266.0">0.266.0</a>
(2026-02-10)</h2>
<h3>Features</h3>
<ul>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3483">#3483</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/a3a61ce2214c8d18bb640c724fae2cda8cb77b58">a3a61ce</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3485">#3485</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/200d1409ecc830131f0b5b92fd59708fef24dd8e">200d140</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3486">#3486</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/870909e466b1bf8172dfe9bd5c096b1df45b0491">870909e</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3487">#3487</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/6018e80ff5cadadb81c7b7be9f5de01b4b4c2132">6018e80</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3489">#3489</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/402353be95579bccda6b6623e67e9f028163905b">402353b</a>)</li>
<li><strong>all:</strong> Auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3490">#3490</a>)
(<a
href="https://github.com/googleapis/google-api-go-client/commit/49c652fb9c5e08c9d1a2587f41017b6011dc03da">49c652f</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/26a317d5406788914bd807821ca316e488608573"><code>26a317d</code></a>
chore(main): release 0.266.0 (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3484">#3484</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/49c652fb9c5e08c9d1a2587f41017b6011dc03da"><code>49c652f</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3490">#3490</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/b9fd5c2a7ba8cf9682c821eb78f2d862e1a78727"><code>b9fd5c2</code></a>
chore(all): update all (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3488">#3488</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/402353be95579bccda6b6623e67e9f028163905b"><code>402353b</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3489">#3489</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/6018e80ff5cadadb81c7b7be9f5de01b4b4c2132"><code>6018e80</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3487">#3487</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/870909e466b1bf8172dfe9bd5c096b1df45b0491"><code>870909e</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3486">#3486</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/200d1409ecc830131f0b5b92fd59708fef24dd8e"><code>200d140</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3485">#3485</a>)</li>
<li><a
href="https://github.com/googleapis/google-api-go-client/commit/a3a61ce2214c8d18bb640c724fae2cda8cb77b58"><code>a3a61ce</code></a>
feat(all): auto-regenerate discovery clients (<a
href="https://redirect.github.com/googleapis/google-api-go-client/issues/3483">#3483</a>)</li>
<li>See full diff in <a
href="https://github.com/googleapis/google-api-go-client/compare/v0.265.0...v0.266.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/api&package-manager=go_modules&previous-version=0.265.0&new-version=0.266.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 13:12:16 +00:00
dependabot[bot] 65fb0e22a8 chore: bump rust from 760ad1d to 9663b80 in /dogfood/coder (#22124)
Bumps rust from `760ad1d` to `9663b80`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rust&package-manager=docker&previous-version=slim&new-version=slim)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 13:10:55 +00:00
Jake Howell 5e7b3c3c28 fix: disable Activity bump when Default autostop is 0 (#22112)
Closes #21703

This doesn't make sense to have an `Activity bump` value when the
`Default autostop` is set to `0`. There is nothing to bump if we don't
have a timed stopping mechanism on the container. This is already
present on the backend and now we're describing this to the user on the
frontend.
2026-02-16 21:28:30 +11:00
blinkagent[bot] 2ed9e7fa6d fix: show accurate removal dialog for expired licenses (#22018)
## Summary

The license removal confirmation dialog always showed:

> Removing this license will disable all Premium features. You add a new
license at any time.

This is misleading when the license being removed is already expired —
an expired license isn't providing any features, so removing it won't
disable anything.

## Changes

- Extracted `isExpired` variable in `LicenseCard` (reusing the existing
expiry check)
- Made the dialog description conditional:
- **Expired license**: "This license has already expired and is not
providing any features. Removing it will not affect your current
entitlements."
- **Active license**: "Removing this license will disable all Premium
features. You can add a new license at any time."
- Also fixed a minor typo in the active license message ("You add" →
"You can add")
- Added two new tests covering both dialog variants

## Testing

All 5 `LicenseCard` tests pass, including the 2 new ones:
- `shows expired removal message for expired licenses`
- `shows disabling features warning for active licenses`

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-02-16 07:34:51 +00:00
Ethan 4b3889e4f9 fix(cli): allow site admins to use coder create --org for any organization (#21528)
## Problem

Site-wide admins (e.g., Owners) could not use `coder create --org <org>`
to create workspaces in organizations they are not members of. The error
was:

```
$ coder create my-workspace -t docker --org data-science
error: organization "data-science" not found, are you sure you are a member of this organization?
```

This was inconsistent with the web UI, where Owners can create
workspaces in any organization.

## Root Cause

The CLI's `OrganizationContext.Selected()` function only checked the
user's membership list, ignoring site-wide RBAC permissions that grant
Owners access to all organizations.

## Solution

Added a fallback in `OrganizationContext.Selected()` that fetches the
org directly via the API when not found in the membership list. This
works because the API endpoint applies RBAC filtering, allowing Owners
to read any org.

## Impact

This fixes `coder create --org` and all other CLI commands that use
`OrganizationContext.Selected()` (29+ commands), including:
- `coder templates push --org <any-org>`
- `coder organizations members add --org <any-org>`
- `coder provisioner list --org <any-org>`

## Testing

Added `TestEnterpriseCreate/OwnerCanCreateInNonMemberOrg` which:
- Creates an Owner user who is NOT a member of a second org
- Verifies they can create a workspace there using `--org`
- Properly fails without the code fix, passes with it

---

*This PR was generated by [mux](https://mux.coder.com) but reviewed by a
human.*
2026-02-16 12:16:08 +11:00
dependabot[bot] 7224977fa6 chore: bump the coder-modules group across 2 directories with 2 updates (#22116)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 00:38:40 +00:00
Jake Howell 47a621cd4e fix: align global layout for settings pages (#22109)
Closes #16148 

This pull-request resolves a few issues with wider displays.
Particularly in ensuring the content's container center's as one would
expect and the content of the headings isn't being contained into a
`max-w-prose`.
2026-02-16 02:40:24 +11:00
Jake Howell a35f9810d0 feat: remove duplicate Cancel from <CreateTemplatePage /> (#22108) 2026-02-15 13:32:07 +11:00
Jake Howell 06039a51ff fix: use 2 characters on <Avatar /> fallback (#22106) 2026-02-15 13:31:44 +11:00
Jake Howell 1591f42d9b fix: solid background on mobile navigation (#22105) 2026-02-15 13:31:22 +11:00
Jake Howell 0822cbdafe fix: resolve interception.model badge size (#22104) 2026-02-15 13:30:54 +11:00
Jake Howell 6ed10c05af fix: reappend fe0f to emoji urls (#22111) 2026-02-15 13:30:09 +11:00
Jeremy Ruppel 0df864fb88 fix: hide "Create Workspace" button for deleted templates (#22092)
**Background**

Reported in #17417, there is a `deleted` query parameter supported by
/api/v2/templates, but we do not respect this field on the client,
showing the "Create Workspace" button for deleted templates.

**Expected Behavior**

Don't show the "Create Workspace" button for deleted templates.

**Notes**

This PR adds a new `deleted` field to the templates API response.

Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
2026-02-13 19:44:50 -05:00
Ehab Younes ebd7ab11cb feat(site): add missing Tasks API client methods (#22079)
Add getTaskLogs, pauseTask, resumeTask, and sendTaskInput methods to the
API client to cover remaining Tasks API endpoints.
2026-02-13 19:11:46 +03:00
blinkagent[bot] 00713385fb feat: remove license gate from workspace and task bulk actions (#22090)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-02-13 20:08:36 +05:00
Danielle Maywood 6d41d98b65 feat(cli): add coder task pause command (#22012)
Adds a new `coder task pause`
2026-02-13 14:21:31 +00:00
Steven Masley 01f06671a1 chore: return 404, not 400 if missing or authz deny (#22069) 2026-02-13 08:19:07 -06:00
Susana Ferreira a613ffa3d6 chore: integrate metrics scanner into Makefile (#21465)
## Description

This PR wires up the metrics scanner in the Makefile to automatically regenerate metrics documentation when source files change.

## Changes

* Add Makefile target `scripts/metricsdocgen/generated_metrics` to run the AST scanner to generate the metrics file
* Update `docs/admin/integrations/prometheus.md` Makefile target to depend on `scripts/metricsdocgen/generated_metrics`
* Add `scripts/metricsdocgen/README.md` documenting the metrics generation process

Closes: https://github.com/coder/coder/issues/13223
2026-02-13 12:31:33 +00:00
Susana Ferreira df84cea924 feat(scripts/metricsdocgen): support merging static and generated metrics files (#21464)
## Description

This PR refactors `scripts/metricsdocgen/main.go` to support merging static and generated metrics files for documentation generation.

The static `metrics` file remains necessary for metrics not defined in the coder codebase (`go_*`, `process_*`, `promhttp_*`, `coder_aibridged_*`), as well as **edge cases** the scanner cannot handle (e.g.,  such as metrics with runtime-determined labels or function-local variable references for fields, ...). Handling these edge cases in the scanner would make it significantly more complex, so we keep this hybrid approach to accommodate them. This means that in such cases, developers need to update the `metrics` file directly, meaning there is still a risk of out-of-date information in the documentation. However, this solution should already encompass most cases.

Static metrics take priority over generated metrics when both files contain the same metric name, allowing manual overrides without modifying the scanner. Some of these edge cases could be easily fixed by updating the codebase to use one of the supported patterns.

## Changes

* Update `scripts/metricsdocgen/main.go` to read from two separate metrics files:
  * `metrics`: static, manually maintained metrics (e.g., `go_*`, `process_*`, `promhttp_*`, `coder_aibridged_*`)
  * `generated_metrics`: auto-generated by the AST scanner
* Update `metrics` file to contain only static and edge-case metrics
* Skip metrics with empty HELP descriptions in the scanner
* Update `generated_metrics` to reflect skipped metrics
* Update `docs/admin/integrations/prometheus.md` with merged metrics

Related to: https://github.com/coder/coder/issues/13223

**Disclosure:** This PR was mainly developed with Claude Sonnet 4, with iterative review and refinement by @ssncferreira
2026-02-13 12:19:33 +00:00
Susana Ferreira 55d1a32424 feat(scripts/metricsdocgen): add promauto.With() pattern to metrics scanner (#21463)
## Description

This PR implements extraction of metrics defined using `promauto.With()` factory patterns.

## Changes

* Add `extractPromautoMetric()` to handle:
  * `promauto.With(reg).NewCounterVec(prometheus.CounterOpts{...}, labels)`
  * `factory.NewGaugeVec(prometheus.GaugeOpts{...}, labels)`
* Script generates an updated `scripts/metricsdocgen/generated_metrics` file

Related to: https://github.com/coder/coder/issues/13223

**Disclosure:** This PR was mainly developed with Claude Sonnet 4, with iterative review and refinement by @ssncferreira
2026-02-13 11:24:33 +00:00
Susana Ferreira bcb437d281 feat(scripts/metricsdocgen): add prometheus.New*() and New*Vec() patterns to metrics scanner (#21462)
## Description

This PR implements extraction of metrics defined using `prometheus.New*()` and `prometheus.New*Vec()` patterns with `*Opts{}` structs.

## Changes

* Add `extractOptsMetric()` to handle:
  * `prometheus.NewGauge(prometheus.GaugeOpts{...})`
  * `prometheus.NewCounter(prometheus.CounterOpts{...})`
  * `prometheus.NewHistogram(prometheus.HistogramOpts{...})`
  * `prometheus.NewSummary(prometheus.SummaryOpts{...})`
  * `prometheus.New*Vec(prometheus.*Opts{...}, labels)`
* Script generates an updated `scripts/metricsdocgen/generated_metrics` file

Related to: https://github.com/coder/coder/issues/13223

**Disclosure:** This PR was mainly developed with Claude Sonnet 4, with iterative review and refinement by @ssncferreira
2026-02-13 11:13:55 +00:00
Susana Ferreira 45280d5516 feat(scripts/metricsdocgen): add prometheus.NewDesc() pattern to metrics scanner (#21461)
## Description

This PR implements extraction of metrics defined using the `prometheus.NewDesc()` pattern.

## Changes

* Add `extractNewDescMetric()` to extract metrics from `prometheus.NewDesc()` calls
* Script generates an updated `scripts/metricsdocgen/generated_metrics` file

Related to: https://github.com/coder/coder/issues/13223

**Disclosure:** This PR was mainly developed with Claude Sonnet 4, with iterative review and refinement by @ssncferreira
2026-02-13 11:01:34 +00:00
Jake Howell 8e947e506f feat: animate <TerminalAlerts /> when isRefreshing (#22073) 2026-02-13 21:58:42 +11:00
Jake Howell b7f08811c3 fix: remove fullscreen from <ExternalAuthPageView /> <Loading /> (#22074) 2026-02-13 21:58:22 +11:00
Susana Ferreira a9180d406e feat(scripts/metricsdocgen): add AST scanner core for metrics doc generation (#21460)
## Description

This PR adds an AST-based scanner to automatically generate Prometheus metrics documentation from the coder source code.

## Changes

* Add `scripts/metricsdocgen/scanner/scanner.go` with:
  * Directory walking for `agent/`, `coderd/`, `enterprise/`, `provisionerd/`
  * Go file parsing (skipping `*_test.go` files)
  * AST inspection for metric extraction
  * `Metric.String()` for Prometheus text exposition format rendering
  * `writeMetrics()` to output metrics to stdout
  * Placeholder `extractMetricFromCall()` (implemented in subsequent PRs)
* Empty `scripts/metricsdocgen/generated_metrics` placeholder (populated by subsequent PRs)

**Note:** To facilitate the review process, this was separated into scoped stacked PRs. The division was based on the main structure, the different Prometheus patterns currently present in the codebase, and updates to the build process.

Related to: https://github.com/coder/coder/issues/13223

**Disclosure:** This PR was mainly developed with Claude Sonnet 4, with iterative review and refinement by @ssncferreira
2026-02-13 10:48:55 +00:00
Jaayden Halko b1d5f77cf0 chore: update colors (#22070) 2026-02-13 08:37:00 +00:00
Jake Howell ef25baf581 feat: refactor <Combobox/> with compound componentisation (#21778)
This pull-request refactors the `<Combobox />` component from a
monolithic design to a composable compound component pattern, providing
more flexibility and reusability across the codebase

- Migrates `<SelectFilter />` to use the new `<Combobox />` instead of
the legacy `<SelectMenu />` components
- Updates all existing consumers of `<Combobox />` and `<SelectFilter
/>` to use the new API

<img
src="https://github.com/user-attachments/assets/a3336431-590c-48b5-adde-3fc5c16f459d"
/>

The `<Combobox />` component has been refactored to use a compound
component pattern, exposing:

- `Combobox` - Root component with context provider for open/value state
- `ComboboxTrigger` - Trigger wrapper (re-exports PopoverTrigger)
- `ComboboxButton` - Styled button with chevron and selected option
display
- `ComboboxContent` - Popover content with Command wrapper
- `ComboboxInput` - Search input (re-exports CommandInput)
- `ComboboxList` - List container (re-exports CommandList)
- `ComboboxItem` - Individual option with checkmark indicator
- `ComboboxEmpty` - Empty state (re-exports CommandEmpty)
- `useCombobox` - Hook to access combobox context

This pattern allows consumers to compose their own combobox layouts
while sharing consistent behavior and styling.

Furthermore, we had an issue with `CreateWorkspacePageView.stories.tsx`
lacking stories which would let us see the passed parameters and presets
in context. I've added stories to surround this.

### Updated Consumers

- `DynamicParameter.tsx` - Updated to use new Combobox API for parameter
options
- `CreateWorkspacePageView.tsx` - Updated preset combobox usage
- `IdpOrgSyncPageView.tsx` - Updated organization sync form
- `IdpGroupSyncForm.tsx` - Updated group sync form
- `IdpRoleSyncForm.tsx` - Updated role sync form
- `WorkspacesPage/filter/menus.tsx` - Updated workspace filter menus

---------

Co-authored-by: ケイラ <mckayla@hey.com>
2026-02-13 15:44:26 +11:00
ケイラ 2cc8cc59fa chore: tell claude to use react-query (#22076) 2026-02-12 16:16:33 -07:00
Callum Styan 5f3be6b288 feat: add provisioner job queue wait time histogram and jobs enqueued counter (#21869)
This PR adds some metrics to help identify job enqueue rates and
latencies. This work was initiated as a way to help reduce the cost of
the observation/measurement itself for autostart scaletests, which
impacts our ability to identify/reason about the load caused by
autostart. See: https://github.com/coder/internal/issues/1209

I've extended the metrics here to account for regular user initiated
builds, prebuilds, autostarts, etc. IMO there is still the question here
of whether we want to include or need the `transition` label, which is
only present on workspace builds. Including it does lead to an increase
in cardinality, and in the case of the histogram (when not using native
histograms) that's at least a few extra series for every bucket. We
could remove the transition label there but keep it on the counter.

Additionally, the histogram is currently observing latencies for other
jobs, such as template builds/version imports, those do not have a
transition type associated with them.

Tested briefly in a workspace, can see metric values like the following:
-
`coderd_workspace_builds_enqueued_total{build_reason="autostart",provisioner_type="terraform",status="success",transition="start"}
1`
-
`coderd_provisioner_job_queue_wait_seconds_bucket{build_reason="autostart",job_type="workspace_build",provisioner_type="terraform",transition="start",le="0.025"}
1`

---------

Signed-off-by: Callum Styan <callumstyan@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-12 13:40:47 -08:00
ケイラ b1f48f8d47 chore: disallow forwardRef (#21906) 2026-02-12 13:31:03 -07:00
Jake Howell 21d4d0196d fix: resolve manual button icon sizes (#22058)
Closes #21830 

Remove redundant icon sizing across the frontend. Components like
`Button`, `DropdownMenuItem`, and `CommandItem` already control child
SVG sizes via CSS selectors (e.g., `[&>svg]:size-icon-lg`), so explicit
`size` props and `className` overrides on icons nested inside them are
unnecessary. This PR strips those out and lets parent components handle
sizing consistently.

As a bonus, also migrates the `DropdownArrow` component from Emotion
CSS-in-JS to Tailwind utilities, replaces raw `<a>` tags with the `<Link
/>` component in the Premium page, and adds Storybook coverage for
`PremiumPageView`.
2026-02-13 05:25:04 +11:00
blinkagent[bot] 1e1d312cab docs: split env var declaration from command in AI Bridge setup (#22072)
The AI Bridge setup docs showed `CODER_AIBRIDGE_ENABLED=true coder
server` as a single line, which can confuse users into thinking the env
var is a one-time prefix rather than a persistent setting.

Split this into `export CODER_AIBRIDGE_ENABLED=true` on its own line
followed by `coder server`, which is clearer and consistent with how the
Bedrock credentials section already handles env vars in the same file.

Created on behalf of @dannykopping

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-02-12 18:15:44 +00:00
Jaayden Halko c9909817a8 chore: replace Inter with Geist variable font (#22060) 2026-02-12 17:26:47 +00:00
Jake Howell 72438a0e55 fix: upgrade font-weight in <Welcome /> heading (#22067)
This pull-request ensures we're globally using a similar `font-weight`
in the `<Welcome />` component (specifically noticed on `cli-auth`).
2026-02-13 03:08:13 +11:00
Susana Ferreira 220b9f3cc5 fix: track goroutines and fix race condition in reconciler (#21980)
## Problem

CI failure showed 3 goroutines leaked in the prebuilds reconciler, all
stuck in `select` state:

1) `MetricsCollector.BackgroundFetch` (metrics goroutine)
2) `StoreReconciler.Run` (main reconciliation loop)
3) `StoreReconciler.Run.func3()` (provisioner job publisher goroutine)

All three goroutines were waiting for `ctx.Done()`, which likely means
`cancelFn()` was never called to trigger shutdown.

**Note:** I was unable to reproduce the flake locally. The likely cause
was a race condition between `Run()` and `Stop()` where `Stop()` could
check `running` (seeing `false`), return early, and then `Run()` would
start goroutines that never get cleaned up. This could happen in any
`coderd` test that starts a server with prebuilds enabled.

### Problems identified

1) Missing waitgoroup tracking: provisioner job publisher goroutine was
not tracked in the waitgroup, therefore, this goroutine was not tracked
for a clean shutdown in `Run defer func()`.
2) The provisioner job publisher goroutine had a redundant `case
<-c.done` that could race with `Stop()` select statement.
3) Race condition between `Run()` and `Stop()`: the `running` and
`stopped` fields were `atomic.Bool` values checked and set
independently, allowing a window where `Stop()` could see
`running=false` and return early, then `Run()` would set `running=true`
and start goroutines that would never be cleaned up. This could happen
in any `coderd` test that starts a server with prebuilds enabled.

## Changes

* Added `wg.Add(1)` and `defer wg.Done()` to track provisioner job
publisher goroutine in waitgroup
* Removed redundant `case <-c.done` from provisioner job publisher
goroutine to eliminate race condition
* Replaced `atomic.Bool` for `running` and `stopped` with a `sync.Mutex`
lifecycle state, also protecting `cancelFn` under the same mutex, to
eliminate the race between `Run()` and `Stop()`
* Added a guard in `Run()` to prevent double-start (`c.stopped ||
c.running`)
* Improved comments in Stop() and Run() to clarify shutdown behavior

Closes: https://github.com/coder/internal/issues/1116
2026-02-12 15:35:42 +00:00
Kacper Sawicki 60e3ab7632 feat(site)!: add consent prompt for auto-creation with prefilled parameters (#22011)
### Summary

Workspace created via mode=auto links now require explicit user
confirmation before provisioning. A warning dialog shows all prefilled
param.* values from the URL and blocks creation until the user clicks
`Confirm and Create`. Clicking `Cancel` falls back to the standard form
view.

<img width="820" height="475" alt="auto-create-consent-dialog"
src="https://github.com/user-attachments/assets/8339e3bd-434f-4a04-9385-436bf95f49d7"
/>

### Breaking behavior change

Links using `mode=auto` (e.g., "Open in Coder" buttons) will no longer
silently create workspaces. Users will now see a consent dialog and must
explicitly confirm before the workspace is provisioned. Any existing
integrations or automation relying on `mode=auto` for seamless workspace
creation will now require manual user interaction.

---------

Co-authored-by: Jake Howell <jacob@coder.com>
2026-02-12 15:39:02 +01:00
Jake Howell 35c7cda760 fix: resolve overflow on <TemplateInsightsPage /> Parameters usage (#22064)
Closes #19954 

This pull-request ensures content doesn't overflow the screen when
looking at `Parameters usage` in `<TemplateInsightsPage />`.

| Old | New |
| --- | --- | 
| <img width="1120" height="211" alt="TEMPLATE_ANALYTICS_OLD"
src="https://github.com/user-attachments/assets/88f35aef-6ade-425c-ae03-7e43d9da192a"
/> | <img width="1121" height="211" alt="TEMPLATE_ANALYTICS_NEW"
src="https://github.com/user-attachments/assets/7cde6baa-ea0e-4a94-9246-a5fdf3c9c081"
/> |

---------

Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
2026-02-13 01:21:51 +11:00
Michael Suchacz adc7775405 feat(vpn): add Linux support for vpn-daemon and OS networking stack (#22051)
This change adds Linux support for Desktop VPN by aligning Linux
behavior with the existing Windows daemon implementation and adding a
Linux networking stack implementation.

### What changed
- Consolidated the daemon command implementation into a shared file:
  - `cli/vpndaemon_windows_linux.go` (`//go:build windows || linux`)
- Consolidated daemon tests into a shared file:
- `cli/vpndaemon_windows_linux_test.go` (`//go:build windows || linux`)
- Removed Linux-only duplicate daemon files:
  - `cli/vpndaemon_linux.go`
  - `cli/vpndaemon_linux_test.go`
- Removed unsupported-platform stubs per current supported OS targets:
  - `cli/vpndaemon_other.go`
  - `vpn/tun.go`
- Kept Linux networking stack implementation in:
  - `vpn/tun_linux.go`

### Notes
- Linux now uses the same `rpc-read-handle` / `rpc-write-handle` flags
and env vars as Windows.
- The daemon logs to stderr (via CLI logger sinks), and does not forward
logs over the RPC pipe.
2026-02-12 12:14:56 +01:00
Cian Johnston 194d79402e chore: remove dbmem comment references (#22056)
👻 The ghost of dbmem managed to live on... until now.
2026-02-12 09:06:33 +00:00
Sas Swart 47b8ca940c feat: add an endpoint to manually resume a coder task (#21948)
Closes https://github.com/coder/internal/issues/1262.

This PR adds:
* the `POST /api/experimental/tasks/{user}/{task}/resume` endpoint
* follows conventions from https://github.com/coder/internal/issues/1261
* sets the build reason to `task_resume`
* a task that is not paused (ie. is already running), cannot be resumed.
2026-02-12 09:59:53 +02:00
Steven Masley 7f7ff9cd40 chore: add dockerignore to ignore build directory (#22053) 2026-02-11 11:52:45 -06:00
Susana Ferreira 5cf97955a0 feat(site): add copilot as a provider option in AI Bridge logs filter (#22023)
## Problem

The Copilot provider was missing from the AI Bridge logs filter dropdown, so users couldn't filter interceptions by Copilot. Additionally, the `AIBridgeProviderIcon` component didn't handle the copilot provider, so it would render a fallback question mark icon.

<img width="1392" height="333" alt="Screenshot 2026-02-10 at 09 26 16" src="https://github.com/user-attachments/assets/ecb97400-a4dd-4e88-accc-68d7fdf19b2a" />

## Changes

* Added `copilot` case to `AIBridgeProviderIcon`, using the existing `/icon/github.svg`.
* Added Copilot as a provider option in the filter dropdown.
* Added `MockInterceptionAnthropic` and `MockInterceptionCopilot` mock data with sample prompts, and updated the Storybook stories to use one interception per provider.
2026-02-11 14:42:38 +00:00
Susana Ferreira 8e9638c750 feat(site): infer model family icon from model name in AI Bridge logs (#22022)
## Problem

Previously, the AI Bridge model column icon was derived from the provider field. This worked because each provider only served its own models: OpenAI interceptions always used OpenAI models, and Anthropic interceptions always used Anthropic models.

With the introduction of the Copilot provider, this assumption no longer holds. Copilot can forward requests to both OpenAI and Anthropic models, so the provider field alone is not enough to determine the correct model icon. This caused Copilot interceptions to display a fallback question mark icon for the model.

<img width="1337" height="365" alt="Screenshot 2026-02-10 at 09 10 34" src="https://github.com/user-attachments/assets/1efd613d-16c9-4738-8337-6ccf92e610fc" />

## Changes

* Added `AIBridgeModelIcon` component that infers the model family (Claude, OpenAI) from the model name string and renders the appropriate icon.
* Updated `RequestLogsRow` to use `AIBridgeModelIcon` instead of `AIBridgeProviderIcon` in both the table row and the expanded detail view.
2026-02-11 14:32:13 +00:00
cryptoluks fcf431c1d7 fix(coderd/workspaceapps): prefer app session cookie over Authorization (#22041)
This PR fixes a workspace app authentication bug where requests that
include an `Authorization` header (intended for the upstream app) can
cause Coder to ignore the workspace app session cookie
(`coder_subdomain_app_session_token_*` /
`coder_path_app_session_token`). When that happens, Coder fails to mint
or renew `coder_signed_app_token` and redirects to
`/api/v2/applications/auth-redirect` instead of proxying the request to
the workspace.

This commonly shows up when users run a frontend and backend in the same
workspace and the backend requires `Authorization` (for example, `curl
-H "Authorization: bearer ..."` or browser `fetch()` calls).

Related issues / context:

* Primary bug report and repro:
[https://github.com/coder/coder/issues/21467](https://github.com/coder/coder/issues/21467)
* Related symptoms reported as CORS / redirect failures for workspace
apps:

*
[https://github.com/coder/coder/issues/20667](https://github.com/coder/coder/issues/20667)
*
[https://github.com/coder/coder/issues/19728](https://github.com/coder/coder/issues/19728)

## Root Cause

In `coderd/workspaceapps/cookies.go`, `AppCookies.TokenFromRequest`
checked `httpmw.APITokenFromRequest(r)` first. That helper returns a
token from several places, including `Authorization: Bearer ...`.

As a result, when a request included an upstream `Authorization` header,
that header value was returned as the “session token” for the app proxy,
and `coder_subdomain_app_session_token_*` was never read. Authentication
then failed and the request was treated as signed out.

## Fix

Change the precedence in `AppCookies.TokenFromRequest`:

1. First check the access-method-specific cookie:

   * subdomain apps: `coder_subdomain_app_session_token_{hash}`
   * path apps: `coder_path_app_session_token`
2. If not present, fall back to `httpmw.APITokenFromRequest(r)` (so
non-browser clients can still authenticate via query, header, or bearer
tokens if they really want to).

This ensures that:

* Backend requests that require `Authorization` still reach the
workspace.
* `coder_signed_app_token` can be renewed from the app session cookie
even when `Authorization` is present.
* `Authorization` is still forwarded to the upstream app (the reverse
proxy code does not strip it).

Initially, I attempted workarounds
([https://github.com/coder/coder/issues/20667#issuecomment-3868578388](https://github.com/coder/coder/issues/20667#issuecomment-3868578388),
[https://github.com/coder/coder/issues/19728#issuecomment-3868578093](https://github.com/coder/coder/issues/19728#issuecomment-3868578093)),
but adding `/auth-redirect` to the permissive CORS paths and extending
the validity of workspace app auth tokens from 1 minute to 1 hour only
partially masked the issue. After workspace restarts and token expiry, I
no longer saw CORS errors, but the tokens were still not renewed.

After patching my local Nix-based setup on Coder v1.30.0 with this
change, I can no longer observe this behavior.
2026-02-11 23:18:49 +11:00
dependabot[bot] 0938981ebf chore: bump github.com/go-git/go-git/v5 from 5.16.2 to 5.16.5 (#22016)
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git)
from 5.16.2 to 5.16.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/go-git/go-git/releases">github.com/go-git/go-git/v5's
releases</a>.</em></p>
<blockquote>
<h2>v5.16.5</h2>
<h2>What's Changed</h2>
<ul>
<li>build: Update module golang.org/x/crypto to v0.45.0 [SECURITY]
(releases/v5.x) by <a
href="https://github.com/go-git-renovate"><code>@​go-git-renovate</code></a>[bot]
in <a
href="https://redirect.github.com/go-git/go-git/pull/1744">go-git/go-git#1744</a></li>
<li>build: Bump Go test versions to 1.23-1.25 (v5) by <a
href="https://github.com/pjbgf"><code>@​pjbgf</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1746">go-git/go-git#1746</a></li>
<li>[v5] git: worktree, Don't delete local untracked files when
resetting worktree by <a
href="https://github.com/Ch00k"><code>@​Ch00k</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1800">go-git/go-git#1800</a></li>
<li>Expand packfile checks by <a
href="https://github.com/pjbgf"><code>@​pjbgf</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1836">go-git/go-git#1836</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/go-git/go-git/compare/v5.16.4...v5.16.5">https://github.com/go-git/go-git/compare/v5.16.4...v5.16.5</a></p>
<h2>v5.16.4</h2>
<h2>What's Changed</h2>
<ul>
<li>backport plumbing: format/idxfile, prevent panic by <a
href="https://github.com/swills"><code>@​swills</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1732">go-git/go-git#1732</a></li>
<li>[backport] build: test, Fix build on Windows. by <a
href="https://github.com/pjbgf"><code>@​pjbgf</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1734">go-git/go-git#1734</a></li>
<li>build: Update module golang.org/x/net to v0.38.0 [SECURITY]
(releases/v5.x) by <a
href="https://github.com/go-git-renovate"><code>@​go-git-renovate</code></a>[bot]
in <a
href="https://redirect.github.com/go-git/go-git/pull/1742">go-git/go-git#1742</a></li>
<li>build: Update module github.com/cloudflare/circl to v1.6.1
[SECURITY] (releases/v5.x) by <a
href="https://github.com/go-git-renovate"><code>@​go-git-renovate</code></a>[bot]
in <a
href="https://redirect.github.com/go-git/go-git/pull/1741">go-git/go-git#1741</a></li>
<li>build: Update module github.com/go-git/go-git/v5 to v5.13.0
[SECURITY] (releases/v5.x) by <a
href="https://github.com/go-git-renovate"><code>@​go-git-renovate</code></a>[bot]
in <a
href="https://redirect.github.com/go-git/go-git/pull/1743">go-git/go-git#1743</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/go-git/go-git/compare/v5.16.3...v5.16.4">https://github.com/go-git/go-git/compare/v5.16.3...v5.16.4</a></p>
<h2>v5.16.3</h2>
<h2>What's Changed</h2>
<ul>
<li>internal: Expand regex to fix build [5.x] by <a
href="https://github.com/baloo"><code>@​baloo</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1644">go-git/go-git#1644</a></li>
<li>build: raise timeouts for windows CI tests and disable CIFuzz [5.x]
by <a href="https://github.com/baloo"><code>@​baloo</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1646">go-git/go-git#1646</a></li>
<li>plumbing: support commits extra headers, support jujutsu signed
commit [5.x] by <a
href="https://github.com/baloo"><code>@​baloo</code></a> in <a
href="https://redirect.github.com/go-git/go-git/pull/1633">go-git/go-git#1633</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/go-git/go-git/compare/v5.16.2...v5.16.3">https://github.com/go-git/go-git/compare/v5.16.2...v5.16.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/go-git/go-git/commit/48a1ae05eec4fff4dd0343744a00bf8d6a7a0b4b"><code>48a1ae0</code></a>
Merge pull request <a
href="https://redirect.github.com/go-git/go-git/issues/1836">#1836</a>
from go-git/check-v5</li>
<li><a
href="https://github.com/go-git/go-git/commit/42bdf1f9044e2145acaed6ac4dbf1b8d257da5bd"><code>42bdf1f</code></a>
storage: filesystem, Verify idx matches pack file</li>
<li><a
href="https://github.com/go-git/go-git/commit/4146a5653f186f90057afecb7e0addd9e623cf19"><code>4146a56</code></a>
plumbing: format/idxfile, Verify idxfile's checksum</li>
<li><a
href="https://github.com/go-git/go-git/commit/63d78ec080cb176f8cd7bf46ce14f4ba01c1d2e5"><code>63d78ec</code></a>
plumbing: format/packfile, Add new ErrMalformedPackFile</li>
<li><a
href="https://github.com/go-git/go-git/commit/25f1624754395a0c67839e71b34956c853f2eb3d"><code>25f1624</code></a>
Merge pull request <a
href="https://redirect.github.com/go-git/go-git/issues/1800">#1800</a>
from Ch00k/no-delete-untracked-v5</li>
<li><a
href="https://github.com/go-git/go-git/commit/600fb139079e3c6886fcfeb20021c707e99e29b4"><code>600fb13</code></a>
git: worktree, Don't delete local untracked files when resetting
worktree</li>
<li><a
href="https://github.com/go-git/go-git/commit/390a56941510fdc19276aa298228d61889aad97a"><code>390a569</code></a>
Merge pull request <a
href="https://redirect.github.com/go-git/go-git/issues/1746">#1746</a>
from pjbgf/bump-go</li>
<li><a
href="https://github.com/go-git/go-git/commit/61c8b859ce3366257354695e99d78fc3739b60fb"><code>61c8b85</code></a>
build: Bump Go test versions to 1.23-1.25 (v5)</li>
<li><a
href="https://github.com/go-git/go-git/commit/e5a05ecd4fb91dc5323ec77667346ae94d84c043"><code>e5a05ec</code></a>
Merge pull request <a
href="https://redirect.github.com/go-git/go-git/issues/1744">#1744</a>
from go-git/renovate/releases/v5.x-go-golang.org-x-c...</li>
<li><a
href="https://github.com/go-git/go-git/commit/1495930b098b5e72394ae8ccc2d9396b8aa7e013"><code>1495930</code></a>
plumbing: Remove use of non-constant format strings</li>
<li>Additional commits viewable in <a
href="https://github.com/go-git/go-git/compare/v5.16.2...v5.16.5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/go-git/go-git/v5&package-manager=go_modules&previous-version=5.16.2&new-version=5.16.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/coder/coder/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 08:37:52 +00:00
Jake Howell 87b382cc85 fix: resolve small screen layout for /tasks (#22036)
When discussing the changes needed for #22032 I was complaining about
how the `overflow-hidden` didn't work correctly so we could safely
remove it.

To continue these changes, I've refactored down how we work on mobile
within these triggers and enable full truncating and `max-w-`'s on each
of the content. Everything stemmed from the `<fieldset />` having a
`width: max-content` causing the content to extend past the bounds of
the container with `flex` in-toe.

Furthermore, the `(Default)` on `Preset` has been turned into a badge so
that we get the full truncation effect as we do with `Template Version`.

Follow-up improvements here might be to wrap the content of this input
on smaller displays.

### Preview

Top is the old, bottom is the new.

<img width="924" height="594" alt="preview"
src="https://github.com/user-attachments/assets/c1bbf152-03a6-4cad-b925-aad0549536a7"
/>
2026-02-11 13:26:54 +11:00
George K be94af386c chore(coderd/database): enforce workspace ACL JSON object constraints (#22019)
The constraints prevent faulty code from saving 'null' as JSON and breaking the `workspaces_expanded` view.
2026-02-10 16:17:29 -08:00
ケイラ e27c4dcd92 chore: replace usage of forwardRef with ref as a prop (#21956) 2026-02-10 16:41:20 -07:00
Cian Johnston c2c2b6f16f chore: remove call to taskname.Generate in dbgen (#22040)
I was trying to figure out why `goleak` was complaining about a dangling
http2 connection goroutine in tests. Turns out that `taskname.Generate`
will call out to Anthropic if an API key is set, and we're calling it in
`dbgen`. Modified to use testutil method instead.
2026-02-10 19:16:44 +00:00
Jake Howell 058f8f1f7c feat: remove emojimart external call to jsdelivr (#22034)
This pull-request ensures we don't have any reason to call out
externally to `jsdelivr`. Nobody has complained about this yet, but in
an [air-gapped environment](https://coder.com/docs/install/airgap) I
foresaw an issue where this might try to reach-out and fail to load the
image.

New behaviour for the spritesheet.

```diff
- https://cdn.jsdelivr.net/npm/emoji-datasource-apple@15.0.1/img/apple/sheets-256/64.png
+ /emojis/spritesheet.png
```
2026-02-11 03:43:23 +11:00
Danielle Maywood 0ab54fd63a fix(site): remove overflow-hidden (#22032)
Fixes an issue where the outline on the tasks page preset selector was
obscured by `overflow-hidden`.
2026-02-10 16:24:11 +00:00
Jake Howell 6ac0244960 fix: implement debounce <WorkspaceParametersPageViewExperimental /> (#22029)
Closes #22028

This pull-request simply takes debounces the message sent to our
web-socket backend and debounces it to ensure we're not overwriting the
users input as they type. As an added bonus this will debounce message
spam if people are going crazy on Radio Items or similar.

An extra flavour bit of flavour with resolving a good use-case for
`cn()` in diagnostic errors 🙂
2026-02-11 00:12:57 +11:00
Susana Ferreira 6338be3b30 chore: remove aibridgeproxyd README (#22027)
This README was unintentionally reintroduced during a Graphite stack
merge. It was removed in commit 910edbc2c6
on #21296, but upstack PR #21390 still had the old branch state with the
file, so it got merged back in. This PR removes the file.

The up-to-date documentation for AI Bridge Proxy can be found in
https://github.com/coder/coder/tree/main/docs/ai-coder/ai-bridge/ai-bridge-proxy
2026-02-10 10:45:26 +00:00
Paweł Banaszewski 72d7b6567b chore: update aibridge version (#22024)
Updates AI Bridge library to apply fix:
https://github.com/coder/aibridge/pull/174
2026-02-10 09:34:53 +00:00
Jake Howell 342d2e4bed feat: refactor <AuditLogRow* /> (#22014)
This pull-request takes the MUI based components from `<AuditLogRow />`
and its subsidiaries and updates them to use the correct newer Tailwind
based components.
2026-02-10 16:10:19 +11:00
Jake Howell 8bcfeab500 chore: revert "fix: apply overflow-y: scroll to <html /> (#21988)" (#22017)
This reverts commit 5224387c5a.

This is causing layout shifts to `0,0` when attempting to open
dropdowns. Something more battle-tested is needed unfortunately, Radix +
Scrollgutters is really annoying.
2026-02-10 03:09:13 +00:00
Jake Howell 5224387c5a fix: apply overflow-y: scroll to <html /> (#21988) 2026-02-10 12:16:44 +11:00
ケイラ 52af6eac68 fix: extend app test timeout (#21934) 2026-02-09 15:02:41 -07:00
300 changed files with 10165 additions and 5559 deletions
+4
View File
@@ -0,0 +1,4 @@
# All artifacts of the build processed are dumped here.
# Ignore it for docker context, as all Dockerfiles should build their own
# binaries.
build
+17 -17
View File
@@ -35,7 +35,7 @@ jobs:
tailnet-integration: ${{ steps.filter.outputs.tailnet-integration }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -157,7 +157,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -247,7 +247,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -272,7 +272,7 @@ jobs:
if: ${{ !cancelled() }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -329,7 +329,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -381,7 +381,7 @@ jobs:
- windows-2022
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -578,7 +578,7 @@ jobs:
timeout-minutes: 25
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -640,7 +640,7 @@ jobs:
timeout-minutes: 25
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -712,7 +712,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -739,7 +739,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -772,7 +772,7 @@ jobs:
name: ${{ matrix.variant.name }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -852,7 +852,7 @@ jobs:
if: needs.changes.outputs.site == 'true' || needs.changes.outputs.ci == 'true'
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -933,7 +933,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -1005,7 +1005,7 @@ jobs:
if: always()
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -1120,7 +1120,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -1175,7 +1175,7 @@ jobs:
IMAGE: ghcr.io/coder/coder-preview:${{ steps.build-docker.outputs.tag }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -1572,7 +1572,7 @@ jobs:
if: needs.changes.outputs.db == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+3 -3
View File
@@ -36,7 +36,7 @@ jobs:
verdict: ${{ steps.check.outputs.verdict }} # DEPLOY or NOOP
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -65,7 +65,7 @@ jobs:
packages: write # to retag image as dogfood
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -146,7 +146,7 @@ jobs:
needs: deploy
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+3 -3
View File
@@ -38,7 +38,7 @@ jobs:
if: github.repository_owner == 'coder'
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -58,11 +58,11 @@ jobs:
run: mkdir base-build-context
- name: Install depot.dev CLI
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
# This uses OIDC authentication, so no auth variables are required.
- name: Build base Docker image via depot.dev
uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # v1.16.2
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
with:
project: wl5hnrrkns
context: base-build-context
+4 -4
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-4' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -75,7 +75,7 @@ jobs:
BRANCH_NAME: ${{ steps.branch-name.outputs.current_branch }}
- name: Set up Depot CLI
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
@@ -88,7 +88,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Build and push Non-Nix image
uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # v1.16.2
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
with:
project: b4q6ltmpzh
token: ${{ secrets.DEPOT_TOKEN }}
@@ -125,7 +125,7 @@ jobs:
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- windows-2022
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
packages: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+5 -5
View File
@@ -39,7 +39,7 @@ jobs:
PR_OPEN: ${{ steps.check_pr.outputs.pr_open }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -76,7 +76,7 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -184,7 +184,7 @@ jobs:
pull-requests: write # needed for commenting on PRs
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -228,7 +228,7 @@ jobs:
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -288,7 +288,7 @@ jobs:
PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+6 -6
View File
@@ -158,7 +158,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -386,12 +386,12 @@ jobs:
- name: Install depot.dev CLI
if: steps.image-base-tag.outputs.tag != ''
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
# This uses OIDC authentication, so no auth variables are required.
- name: Build base Docker image via depot.dev
if: steps.image-base-tag.outputs.tag != ''
uses: depot/build-push-action@9785b135c3c76c33db102e45be96a25ab55cd507 # v1.16.2
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
with:
project: wl5hnrrkns
context: base-build-context
@@ -796,7 +796,7 @@ jobs:
# TODO: skip this if it's not a new release (i.e. a backport). This is
# fine right now because it just makes a PR that we can close.
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -872,7 +872,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -965,7 +965,7 @@ jobs:
if: ${{ !inputs.dry_run }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -69,7 +69,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+3 -3
View File
@@ -18,7 +18,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -96,7 +96,7 @@ jobs:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -120,7 +120,7 @@ jobs:
actions: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
pull-requests: write # required to post PR review comments by the action
steps:
- name: Harden Runner
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
+4 -1
View File
@@ -909,7 +909,10 @@ site/src/api/countriesGenerated.ts: site/node_modules/.installed scripts/typegen
(cd site/ && pnpm exec biome format --write src/api/countriesGenerated.ts)
touch "$@"
docs/admin/integrations/prometheus.md: node_modules/.installed scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
scripts/metricsdocgen/generated_metrics: $(GO_SRC_FILES)
go run ./scripts/metricsdocgen/scanner > $@
docs/admin/integrations/prometheus.md: node_modules/.installed scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics scripts/metricsdocgen/generated_metrics
go run scripts/metricsdocgen/main.go
pnpm exec markdownlint-cli2 --fix ./docs/admin/integrations/prometheus.md
pnpm exec markdown-table-formatter ./docs/admin/integrations/prometheus.md
+25 -21
View File
@@ -3,11 +3,11 @@
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
"defaultBranch": "main"
"defaultBranch": "main",
},
"files": {
"includes": ["**", "!**/pnpm-lock.yaml"],
"ignoreUnknown": true
"ignoreUnknown": true,
},
"linter": {
"rules": {
@@ -15,18 +15,18 @@
"noSvgWithoutTitle": "off",
"useButtonType": "off",
"useSemanticElements": "off",
"noStaticElementInteractions": "off"
"noStaticElementInteractions": "off",
},
"correctness": {
"noUnusedImports": "warn",
"correctness": {
"noUnusedImports": "warn",
"useUniqueElementIds": "off", // TODO: This is new but we want to fix it
"noNestedComponentDefinitions": "off", // TODO: Investigate, since it is used by shadcn components
"noUnusedVariables": {
"level": "warn",
"noUnusedVariables": {
"level": "warn",
"options": {
"ignoreRestSiblings": true
}
}
"ignoreRestSiblings": true,
},
},
},
"style": {
"noNonNullAssertion": "off",
@@ -45,6 +45,10 @@
"level": "error",
"options": {
"paths": {
"react": {
"message": "React 19 no longer requires forwardRef. Use ref as a prop instead.",
"importNames": ["forwardRef"],
},
// "@mui/material/Alert": "Use components/Alert/Alert instead.",
// "@mui/material/AlertTitle": "Use components/Alert/Alert instead.",
// "@mui/material/Autocomplete": "Use shadcn/ui Combobox instead.",
@@ -111,10 +115,10 @@
"@emotion/styled": "Use Tailwind CSS instead.",
// "@emotion/cache": "Use Tailwind CSS instead.",
// "components/Stack/Stack": "Use Tailwind flex utilities instead (e.g., <div className='flex flex-col gap-4'>).",
"lodash": "Use lodash/<name> instead."
}
}
}
"lodash": "Use lodash/<name> instead.",
},
},
},
},
"suspicious": {
"noArrayIndexKey": "off",
@@ -125,14 +129,14 @@
"noConsole": {
"level": "error",
"options": {
"allow": ["error", "info", "warn"]
}
}
"allow": ["error", "info", "warn"],
},
},
},
"complexity": {
"noImportantStyles": "off" // TODO: check and fix !important styles
}
}
"noImportantStyles": "off", // TODO: check and fix !important styles
},
},
},
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json"
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
}
+15 -4
View File
@@ -884,16 +884,27 @@ func (o *OrganizationContext) Selected(inv *serpent.Invocation, client *codersdk
index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
return org.Name == o.FlagSelect || org.ID.String() == o.FlagSelect
})
if index >= 0 {
return orgs[index], nil
}
if index < 0 {
// Not in membership list - try direct fetch.
// This allows site-wide admins (e.g., Owners) to use orgs they aren't
// members of.
org, err := client.OrganizationByName(inv.Context(), o.FlagSelect)
if err != nil {
var names []string
for _, org := range orgs {
names = append(names, org.Name)
}
return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization? "+
"Valid options for '--org=' are [%s].", o.FlagSelect, strings.Join(names, ", "))
var sdkErr *codersdk.Error
if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization? "+
"Valid options for '--org=' are [%s].", o.FlagSelect, strings.Join(names, ", "))
}
return codersdk.Organization{}, xerrors.Errorf("get organization %q: %w", o.FlagSelect, err)
}
return orgs[index], nil
return org, nil
}
if len(orgs) == 1 {
+8 -1
View File
@@ -95,6 +95,7 @@ import (
"github.com/coder/coder/v2/coderd/webpush"
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
"github.com/coder/coder/v2/coderd/workspacestats"
"github.com/coder/coder/v2/coderd/wsbuilder"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/drpcsdk"
"github.com/coder/coder/v2/cryptorand"
@@ -935,6 +936,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
options.StatsBatcher = batcher
defer closeBatcher()
wsBuilderMetrics, err := wsbuilder.NewMetrics(options.PrometheusRegistry)
if err != nil {
return xerrors.Errorf("failed to register workspace builder metrics: %w", err)
}
options.WorkspaceBuilderMetrics = wsBuilderMetrics
// Manage notifications.
var (
notificationsCfg = options.DeploymentValues.Notifications
@@ -1118,7 +1125,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
autobuildTicker := time.NewTicker(vals.AutobuildPollInterval.Value())
defer autobuildTicker.Stop()
autobuildExecutor := autobuild.NewExecutor(
ctx, options.Database, options.Pubsub, coderAPI.FileCache, options.PrometheusRegistry, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, coderAPI.BuildUsageChecker, logger, autobuildTicker.C, options.NotificationsEnqueuer, coderAPI.Experiments)
ctx, options.Database, options.Pubsub, coderAPI.FileCache, options.PrometheusRegistry, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, coderAPI.BuildUsageChecker, logger, autobuildTicker.C, options.NotificationsEnqueuer, coderAPI.Experiments, coderAPI.WorkspaceBuilderMetrics)
autobuildExecutor.Run()
jobReaperTicker := time.NewTicker(vals.JobReaperDetectorInterval.Value())
+2
View File
@@ -17,6 +17,8 @@ func (r *RootCmd) tasksCommand() *serpent.Command {
r.taskDelete(),
r.taskList(),
r.taskLogs(),
r.taskPause(),
r.taskResume(),
r.taskSend(),
r.taskStatus(),
},
+5 -10
View File
@@ -41,8 +41,7 @@ func Test_TaskLogs_Golden(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
client, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskLogsOK(testMessages))
userClient := client // user already has access to their own workspace
_, userClient, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskLogsOK(testMessages))
inv, root := clitest.New(t, "task", "logs", task.Name, "--output", "json")
output := clitest.Capture(inv)
@@ -65,8 +64,7 @@ func Test_TaskLogs_Golden(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
client, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskLogsOK(testMessages))
userClient := client
_, userClient, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskLogsOK(testMessages))
inv, root := clitest.New(t, "task", "logs", task.ID.String(), "--output", "json")
output := clitest.Capture(inv)
@@ -89,8 +87,7 @@ func Test_TaskLogs_Golden(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
client, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskLogsOK(testMessages))
userClient := client
_, userClient, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskLogsOK(testMessages))
inv, root := clitest.New(t, "task", "logs", task.ID.String())
output := clitest.Capture(inv)
@@ -144,8 +141,7 @@ func Test_TaskLogs_Golden(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
client, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskLogsErr(assert.AnError))
userClient := client
_, userClient, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskLogsErr(assert.AnError))
inv, root := clitest.New(t, "task", "logs", task.ID.String())
clitest.SetupConfig(t, userClient, root)
@@ -201,8 +197,7 @@ func Test_TaskLogs_Golden(t *testing.T) {
t.Run("SnapshotWithoutLogs_NoSnapshotCaptured", func(t *testing.T) {
t.Parallel()
client, task := setupCLITaskTestWithoutSnapshot(t, codersdk.TaskStatusPaused)
userClient := client
userClient, task := setupCLITaskTestWithoutSnapshot(t, codersdk.TaskStatusPaused)
inv, root := clitest.New(t, "task", "logs", task.Name)
output := clitest.Capture(inv)
+90
View File
@@ -0,0 +1,90 @@
package cli
import (
"fmt"
"time"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) taskPause() *serpent.Command {
cmd := &serpent.Command{
Use: "pause <task>",
Short: "Pause a task",
Long: FormatExamples(
Example{
Description: "Pause a task by name",
Command: "coder task pause my-task",
},
Example{
Description: "Pause another user's task",
Command: "coder task pause alice/my-task",
},
Example{
Description: "Pause a task without confirmation",
Command: "coder task pause my-task --yes",
},
),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
Options: serpent.OptionSet{
cliui.SkipPromptOption(),
},
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
client, err := r.InitClient(inv)
if err != nil {
return err
}
task, err := client.TaskByIdentifier(ctx, inv.Args[0])
if err != nil {
return xerrors.Errorf("resolve task %q: %w", inv.Args[0], err)
}
display := fmt.Sprintf("%s/%s", task.OwnerName, task.Name)
if task.Status == codersdk.TaskStatusPaused {
return xerrors.Errorf("task %q is already paused", display)
}
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: fmt.Sprintf("Pause task %s?", pretty.Sprint(cliui.DefaultStyles.Code, display)),
IsConfirm: true,
Default: cliui.ConfirmNo,
})
if err != nil {
return err
}
resp, err := client.PauseTask(ctx, task.OwnerName, task.ID)
if err != nil {
return xerrors.Errorf("pause task %q: %w", display, err)
}
if resp.WorkspaceBuild == nil {
return xerrors.Errorf("pause task %q: no workspace build returned", display)
}
err = cliui.WorkspaceBuild(ctx, inv.Stdout, client, resp.WorkspaceBuild.ID)
if err != nil {
return xerrors.Errorf("watch pause build for task %q: %w", display, err)
}
_, _ = fmt.Fprintf(
inv.Stdout,
"\nThe %s task has been paused at %s!\n",
cliui.Keyword(task.Name),
cliui.Timestamp(time.Now()),
)
return nil
},
}
return cmd
}
+144
View File
@@ -0,0 +1,144 @@
package cli_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
)
func TestExpTaskPause(t *testing.T) {
t.Parallel()
t.Run("WithYesFlag", func(t *testing.T) {
t.Parallel()
// Given: A running task
setupCtx := testutil.Context(t, testutil.WaitLong)
_, userClient, task := setupCLITaskTest(setupCtx, t, nil)
// When: We attempt to pause the task
inv, root := clitest.New(t, "task", "pause", task.Name, "--yes")
output := clitest.Capture(inv)
clitest.SetupConfig(t, userClient, root)
// Then: Expect the task to be paused
ctx := testutil.Context(t, testutil.WaitMedium)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.Contains(t, output.Stdout(), "has been paused")
updated, err := userClient.TaskByIdentifier(ctx, task.Name)
require.NoError(t, err)
require.Equal(t, codersdk.TaskStatusPaused, updated.Status)
})
// OtherUserTask verifies that an admin can pause a task owned by
// another user using the "owner/name" identifier format.
t.Run("OtherUserTask", func(t *testing.T) {
t.Parallel()
// Given: A different user's running task
setupCtx := testutil.Context(t, testutil.WaitLong)
adminClient, _, task := setupCLITaskTest(setupCtx, t, nil)
// When: We attempt to pause their task
identifier := fmt.Sprintf("%s/%s", task.OwnerName, task.Name)
inv, root := clitest.New(t, "task", "pause", identifier, "--yes")
output := clitest.Capture(inv)
clitest.SetupConfig(t, adminClient, root)
// Then: We expect the task to be paused
ctx := testutil.Context(t, testutil.WaitMedium)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.Contains(t, output.Stdout(), "has been paused")
updated, err := adminClient.TaskByIdentifier(ctx, identifier)
require.NoError(t, err)
require.Equal(t, codersdk.TaskStatusPaused, updated.Status)
})
t.Run("PromptConfirm", func(t *testing.T) {
t.Parallel()
// Given: A running task
setupCtx := testutil.Context(t, testutil.WaitLong)
_, userClient, task := setupCLITaskTest(setupCtx, t, nil)
// When: We attempt to pause the task
inv, root := clitest.New(t, "task", "pause", task.Name)
clitest.SetupConfig(t, userClient, root)
// And: We confirm we want to pause the task
ctx := testutil.Context(t, testutil.WaitMedium)
inv = inv.WithContext(ctx)
pty := ptytest.New(t).Attach(inv)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectMatchContext(ctx, "Pause task")
pty.WriteLine("yes")
// Then: We expect the task to be paused
pty.ExpectMatchContext(ctx, "has been paused")
require.NoError(t, w.Wait())
updated, err := userClient.TaskByIdentifier(ctx, task.Name)
require.NoError(t, err)
require.Equal(t, codersdk.TaskStatusPaused, updated.Status)
})
t.Run("PromptDecline", func(t *testing.T) {
t.Parallel()
// Given: A running task
setupCtx := testutil.Context(t, testutil.WaitLong)
_, userClient, task := setupCLITaskTest(setupCtx, t, nil)
// When: We attempt to pause the task
inv, root := clitest.New(t, "task", "pause", task.Name)
clitest.SetupConfig(t, userClient, root)
// But: We say no at the confirmation screen
ctx := testutil.Context(t, testutil.WaitMedium)
inv = inv.WithContext(ctx)
pty := ptytest.New(t).Attach(inv)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectMatchContext(ctx, "Pause task")
pty.WriteLine("no")
require.Error(t, w.Wait())
// Then: We expect the task to not be paused
updated, err := userClient.TaskByIdentifier(ctx, task.Name)
require.NoError(t, err)
require.NotEqual(t, codersdk.TaskStatusPaused, updated.Status)
})
t.Run("TaskAlreadyPaused", func(t *testing.T) {
t.Parallel()
// Given: A running task
setupCtx := testutil.Context(t, testutil.WaitLong)
_, userClient, task := setupCLITaskTest(setupCtx, t, nil)
// And: We paused the running task
ctx := testutil.Context(t, testutil.WaitMedium)
resp, err := userClient.PauseTask(ctx, task.OwnerName, task.ID)
require.NoError(t, err)
require.NotNil(t, resp.WorkspaceBuild)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, resp.WorkspaceBuild.ID)
// When: We attempt to pause the task again
inv, root := clitest.New(t, "task", "pause", task.Name, "--yes")
clitest.SetupConfig(t, userClient, root)
// Then: We expect to get an error that the task is already paused
err = inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "is already paused")
})
}
+95
View File
@@ -0,0 +1,95 @@
package cli
import (
"fmt"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) taskResume() *serpent.Command {
var noWait bool
cmd := &serpent.Command{
Use: "resume <task>",
Short: "Resume a task",
Long: FormatExamples(
Example{
Description: "Resume a task by name",
Command: "coder task resume my-task",
},
Example{
Description: "Resume another user's task",
Command: "coder task resume alice/my-task",
},
Example{
Description: "Resume a task without confirmation",
Command: "coder task resume my-task --yes",
},
),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
Options: serpent.OptionSet{
{
Flag: "no-wait",
Description: "Return immediately after resuming the task.",
Value: serpent.BoolOf(&noWait),
},
cliui.SkipPromptOption(),
},
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
client, err := r.InitClient(inv)
if err != nil {
return err
}
task, err := client.TaskByIdentifier(ctx, inv.Args[0])
if err != nil {
return xerrors.Errorf("resolve task %q: %w", inv.Args[0], err)
}
display := fmt.Sprintf("%s/%s", task.OwnerName, task.Name)
if task.Status == codersdk.TaskStatusError || task.Status == codersdk.TaskStatusUnknown {
return xerrors.Errorf("task %q is in %s state and cannot be resumed; check the workspace build logs and agent status for details", display, task.Status)
} else if task.Status != codersdk.TaskStatusPaused {
return xerrors.Errorf("task %q cannot be resumed (current status: %s)", display, task.Status)
}
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: fmt.Sprintf("Resume task %s?", pretty.Sprint(cliui.DefaultStyles.Code, display)),
IsConfirm: true,
Default: cliui.ConfirmNo,
})
if err != nil {
return err
}
resp, err := client.ResumeTask(ctx, task.OwnerName, task.ID)
if err != nil {
return xerrors.Errorf("resume task %q: %w", display, err)
} else if resp.WorkspaceBuild == nil {
return xerrors.Errorf("resume task %q: no workspace build returned", display)
}
if noWait {
_, _ = fmt.Fprintf(inv.Stdout, "Resuming task %q in the background.\n", cliui.Keyword(display))
return nil
}
if err = cliui.WorkspaceBuild(ctx, inv.Stdout, client, resp.WorkspaceBuild.ID); err != nil {
return xerrors.Errorf("watch resume build for task %q: %w", display, err)
}
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s task has been resumed.\n", cliui.Keyword(display))
return nil
},
}
return cmd
}
+183
View File
@@ -0,0 +1,183 @@
package cli_test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
)
func TestExpTaskResume(t *testing.T) {
t.Parallel()
// pauseTask is a helper that pauses a task and waits for the stop
// build to complete.
pauseTask := func(ctx context.Context, t *testing.T, client *codersdk.Client, task codersdk.Task) {
t.Helper()
pauseResp, err := client.PauseTask(ctx, task.OwnerName, task.ID)
require.NoError(t, err)
require.NotNil(t, pauseResp.WorkspaceBuild)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, pauseResp.WorkspaceBuild.ID)
}
t.Run("WithYesFlag", func(t *testing.T) {
t.Parallel()
// Given: A paused task
setupCtx := testutil.Context(t, testutil.WaitLong)
_, userClient, task := setupCLITaskTest(setupCtx, t, nil)
pauseTask(setupCtx, t, userClient, task)
// When: We attempt to resume the task
inv, root := clitest.New(t, "task", "resume", task.Name, "--yes")
output := clitest.Capture(inv)
clitest.SetupConfig(t, userClient, root)
// Then: We expect the task to be resumed
ctx := testutil.Context(t, testutil.WaitMedium)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.Contains(t, output.Stdout(), "has been resumed")
updated, err := userClient.TaskByIdentifier(ctx, task.Name)
require.NoError(t, err)
require.Equal(t, codersdk.TaskStatusInitializing, updated.Status)
})
// OtherUserTask verifies that an admin can resume a task owned by
// another user using the "owner/name" identifier format.
t.Run("OtherUserTask", func(t *testing.T) {
t.Parallel()
// Given: A different user's paused task
setupCtx := testutil.Context(t, testutil.WaitLong)
adminClient, userClient, task := setupCLITaskTest(setupCtx, t, nil)
pauseTask(setupCtx, t, userClient, task)
// When: We attempt to resume their task
identifier := fmt.Sprintf("%s/%s", task.OwnerName, task.Name)
inv, root := clitest.New(t, "task", "resume", identifier, "--yes")
output := clitest.Capture(inv)
clitest.SetupConfig(t, adminClient, root)
// Then: We expect the task to be resumed
ctx := testutil.Context(t, testutil.WaitMedium)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.Contains(t, output.Stdout(), "has been resumed")
updated, err := adminClient.TaskByIdentifier(ctx, identifier)
require.NoError(t, err)
require.Equal(t, codersdk.TaskStatusInitializing, updated.Status)
})
t.Run("NoWait", func(t *testing.T) {
t.Parallel()
// Given: A paused task
setupCtx := testutil.Context(t, testutil.WaitLong)
_, userClient, task := setupCLITaskTest(setupCtx, t, nil)
pauseTask(setupCtx, t, userClient, task)
// When: We attempt to resume the task (and specify no wait)
inv, root := clitest.New(t, "task", "resume", task.Name, "--yes", "--no-wait")
output := clitest.Capture(inv)
clitest.SetupConfig(t, userClient, root)
// Then: We expect the task to be resumed in the background
ctx := testutil.Context(t, testutil.WaitMedium)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.Contains(t, output.Stdout(), "in the background")
// And: The task to eventually be resumed
require.True(t, task.WorkspaceID.Valid, "task should have a workspace ID")
ws := coderdtest.MustWorkspace(t, userClient, task.WorkspaceID.UUID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, ws.LatestBuild.ID)
updated, err := userClient.TaskByIdentifier(ctx, task.Name)
require.NoError(t, err)
require.Equal(t, codersdk.TaskStatusInitializing, updated.Status)
})
t.Run("PromptConfirm", func(t *testing.T) {
t.Parallel()
// Given: A paused task
setupCtx := testutil.Context(t, testutil.WaitLong)
_, userClient, task := setupCLITaskTest(setupCtx, t, nil)
pauseTask(setupCtx, t, userClient, task)
// When: We attempt to resume the task
inv, root := clitest.New(t, "task", "resume", task.Name)
clitest.SetupConfig(t, userClient, root)
// And: We confirm we want to resume the task
ctx := testutil.Context(t, testutil.WaitMedium)
inv = inv.WithContext(ctx)
pty := ptytest.New(t).Attach(inv)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectMatchContext(ctx, "Resume task")
pty.WriteLine("yes")
// Then: We expect the task to be resumed
pty.ExpectMatchContext(ctx, "has been resumed")
require.NoError(t, w.Wait())
updated, err := userClient.TaskByIdentifier(ctx, task.Name)
require.NoError(t, err)
require.Equal(t, codersdk.TaskStatusInitializing, updated.Status)
})
t.Run("PromptDecline", func(t *testing.T) {
t.Parallel()
// Given: A paused task
setupCtx := testutil.Context(t, testutil.WaitLong)
_, userClient, task := setupCLITaskTest(setupCtx, t, nil)
pauseTask(setupCtx, t, userClient, task)
// When: We attempt to resume the task
inv, root := clitest.New(t, "task", "resume", task.Name)
clitest.SetupConfig(t, userClient, root)
// But: Say no at the confirmation screen
ctx := testutil.Context(t, testutil.WaitMedium)
inv = inv.WithContext(ctx)
pty := ptytest.New(t).Attach(inv)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectMatchContext(ctx, "Resume task")
pty.WriteLine("no")
require.Error(t, w.Wait())
// Then: We expect the task to still be paused
updated, err := userClient.TaskByIdentifier(ctx, task.Name)
require.NoError(t, err)
require.Equal(t, codersdk.TaskStatusPaused, updated.Status)
})
t.Run("TaskNotPaused", func(t *testing.T) {
t.Parallel()
// Given: A running task
setupCtx := testutil.Context(t, testutil.WaitLong)
_, userClient, task := setupCLITaskTest(setupCtx, t, nil)
// When: We attempt to resume the task that is not paused
inv, root := clitest.New(t, "task", "resume", task.Name, "--yes")
clitest.SetupConfig(t, userClient, root)
// Then: We expect to get an error that the task is not paused
ctx := testutil.Context(t, testutil.WaitMedium)
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "cannot be resumed")
})
}
+4 -7
View File
@@ -25,8 +25,7 @@ func Test_TaskSend(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
client, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskSendOK(t, "carry on with the task", "you got it"))
userClient := client
_, userClient, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskSendOK(t, "carry on with the task", "you got it"))
var stdout strings.Builder
inv, root := clitest.New(t, "task", "send", task.Name, "carry on with the task")
@@ -42,8 +41,7 @@ func Test_TaskSend(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
client, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskSendOK(t, "carry on with the task", "you got it"))
userClient := client
_, userClient, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskSendOK(t, "carry on with the task", "you got it"))
var stdout strings.Builder
inv, root := clitest.New(t, "task", "send", task.ID.String(), "carry on with the task")
@@ -59,8 +57,7 @@ func Test_TaskSend(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
client, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskSendOK(t, "carry on with the task", "you got it"))
userClient := client
_, userClient, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskSendOK(t, "carry on with the task", "you got it"))
var stdout strings.Builder
inv, root := clitest.New(t, "task", "send", task.Name, "--stdin")
@@ -113,7 +110,7 @@ func Test_TaskSend(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
userClient, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskSendErr(t, assert.AnError))
_, userClient, task := setupCLITaskTest(setupCtx, t, fakeAgentAPITaskSendErr(t, assert.AnError))
var stdout strings.Builder
inv, root := clitest.New(t, "task", "send", task.Name, "some task input")
+44 -10
View File
@@ -120,6 +120,40 @@ func Test_Tasks(t *testing.T) {
require.Equal(t, logs[2].Type, codersdk.TaskLogTypeOutput, "third message should be an output")
},
},
{
name: "pause task",
cmdArgs: []string{"task", "pause", taskName, "--yes"},
assertFn: func(stdout string, userClient *codersdk.Client) {
require.Contains(t, stdout, "has been paused", "pause output should confirm task was paused")
},
},
{
name: "get task status after pause",
cmdArgs: []string{"task", "status", taskName, "--output", "json"},
assertFn: func(stdout string, userClient *codersdk.Client) {
var task codersdk.Task
require.NoError(t, json.NewDecoder(strings.NewReader(stdout)).Decode(&task), "should unmarshal task status")
require.Equal(t, taskName, task.Name, "task name should match")
require.Equal(t, codersdk.TaskStatusPaused, task.Status, "task should be paused")
},
},
{
name: "resume task",
cmdArgs: []string{"task", "resume", taskName, "--yes"},
assertFn: func(stdout string, userClient *codersdk.Client) {
require.Contains(t, stdout, "has been resumed", "resume output should confirm task was resumed")
},
},
{
name: "get task status after resume",
cmdArgs: []string{"task", "status", taskName, "--output", "json"},
assertFn: func(stdout string, userClient *codersdk.Client) {
var task codersdk.Task
require.NoError(t, json.NewDecoder(strings.NewReader(stdout)).Decode(&task), "should unmarshal task status")
require.Equal(t, taskName, task.Name, "task name should match")
require.Equal(t, codersdk.TaskStatusInitializing, task.Status, "task should be initializing after resume")
},
},
{
name: "delete task",
cmdArgs: []string{"task", "delete", taskName, "--yes"},
@@ -238,17 +272,17 @@ func fakeAgentAPIEcho(ctx context.Context, t testing.TB, initMsg agentapisdk.Mes
// setupCLITaskTest creates a test workspace with an AI task template and agent,
// with a fake agent API configured with the provided set of handlers.
// Returns the user client and workspace.
func setupCLITaskTest(ctx context.Context, t *testing.T, agentAPIHandlers map[string]http.HandlerFunc) (*codersdk.Client, codersdk.Task) {
func setupCLITaskTest(ctx context.Context, t *testing.T, agentAPIHandlers map[string]http.HandlerFunc) (ownerClient *codersdk.Client, memberClient *codersdk.Client, task codersdk.Task) {
t.Helper()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
userClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
ownerClient = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, ownerClient)
userClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
fakeAPI := startFakeAgentAPI(t, agentAPIHandlers)
authToken := uuid.NewString()
template := createAITaskTemplate(t, client, owner.OrganizationID, withSidebarURL(fakeAPI.URL()), withAgentToken(authToken))
template := createAITaskTemplate(t, ownerClient, owner.OrganizationID, withSidebarURL(fakeAPI.URL()), withAgentToken(authToken))
wantPrompt := "test prompt"
task, err := userClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
@@ -262,17 +296,17 @@ func setupCLITaskTest(ctx context.Context, t *testing.T, agentAPIHandlers map[st
require.True(t, task.WorkspaceID.Valid, "task should have a workspace ID")
workspace, err := userClient.Workspace(ctx, task.WorkspaceID.UUID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(authToken))
_ = agenttest.New(t, client.URL, authToken, func(o *agent.Options) {
agentClient := agentsdk.New(userClient.URL, agentsdk.WithFixedToken(authToken))
_ = agenttest.New(t, userClient.URL, authToken, func(o *agent.Options) {
o.Client = agentClient
})
coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).
coderdtest.NewWorkspaceAgentWaiter(t, userClient, workspace.ID).
WaitFor(coderdtest.AgentsReady)
return userClient, task
return ownerClient, userClient, task
}
// setupCLITaskTestWithSnapshot creates a task in the specified status with a log snapshot.
+2
View File
@@ -12,6 +12,8 @@ SUBCOMMANDS:
delete Delete tasks
list List tasks
logs Show a task's logs
pause Pause a task
resume Resume a task
send Send input to a task
status Show the status of a task.
+25
View File
@@ -0,0 +1,25 @@
coder v0.0.0-devel
USAGE:
coder task pause [flags] <task>
Pause a task
- Pause a task by name:
$ coder task pause my-task
- Pause another user's task:
$ coder task pause alice/my-task
- Pause a task without confirmation:
$ coder task pause my-task --yes
OPTIONS:
-y, --yes bool
Bypass confirmation prompts.
———
Run `coder --help` for a list of global options.
+28
View File
@@ -0,0 +1,28 @@
coder v0.0.0-devel
USAGE:
coder task resume [flags] <task>
Resume a task
- Resume a task by name:
$ coder task resume my-task
- Resume another user's task:
$ coder task resume alice/my-task
- Resume a task without confirmation:
$ coder task resume my-task --yes
OPTIONS:
--no-wait bool
Return immediately after resuming the task.
-y, --yes bool
Bypass confirmation prompts.
———
Run `coder --help` for a list of global options.
+1 -1
View File
@@ -27,7 +27,7 @@ USAGE:
SUBCOMMANDS:
create Create a token
list List tokens
remove Delete a token
remove Expire or delete a token
view Display detailed information about a token
———
+4
View File
@@ -15,6 +15,10 @@ OPTIONS:
-c, --column [id|name|scopes|allow list|last used|expires at|created at|owner] (default: id,name,scopes,allow list,last used,expires at,created at)
Columns to display in table output.
--include-expired bool
Include expired tokens in the output. By default, expired tokens are
hidden.
-o, --output table|json (default: table)
Output format.
+10 -2
View File
@@ -1,11 +1,19 @@
coder v0.0.0-devel
USAGE:
coder tokens remove <name|id|token>
coder tokens remove [flags] <name|id|token>
Delete a token
Expire or delete a token
Aliases: delete, rm
Remove a token by expiring it. Use --delete to permanently hard-delete the
token instead.
OPTIONS:
--delete bool
Permanently delete the token instead of expiring it. This removes the
audit trail.
———
Run `coder --help` for a list of global options.
+49 -13
View File
@@ -218,9 +218,10 @@ func (r *RootCmd) listTokens() *serpent.Command {
}
var (
all bool
displayTokens []tokenListRow
formatter = cliui.NewOutputFormatter(
all bool
includeExpired bool
displayTokens []tokenListRow
formatter = cliui.NewOutputFormatter(
cliui.TableFormat([]tokenListRow{}, defaultCols),
cliui.JSONFormat(),
)
@@ -246,6 +247,20 @@ func (r *RootCmd) listTokens() *serpent.Command {
return xerrors.Errorf("list tokens: %w", err)
}
// Filter out expired tokens unless --include-expired is set
// TODO(Cian): This _could_ get too big for client-side filtering.
// If it causes issues, we can filter server-side.
if !includeExpired {
now := time.Now()
filtered := make([]codersdk.APIKeyWithOwner, 0, len(tokens))
for _, token := range tokens {
if token.ExpiresAt.After(now) {
filtered = append(filtered, token)
}
}
tokens = filtered
}
displayTokens = make([]tokenListRow, len(tokens))
for i, token := range tokens {
@@ -274,6 +289,12 @@ func (r *RootCmd) listTokens() *serpent.Command {
Description: "Specifies whether all users' tokens will be listed or not (must have Owner role to see all tokens).",
Value: serpent.BoolOf(&all),
},
{
Name: "include-expired",
Flag: "include-expired",
Description: "Include expired tokens in the output. By default, expired tokens are hidden.",
Value: serpent.BoolOf(&includeExpired),
},
}
formatter.AttachOptions(&cmd.Options)
@@ -323,10 +344,13 @@ func (r *RootCmd) viewToken() *serpent.Command {
}
func (r *RootCmd) removeToken() *serpent.Command {
var deleteToken bool
cmd := &serpent.Command{
Use: "remove <name|id|token>",
Aliases: []string{"delete"},
Short: "Delete a token",
Short: "Expire or delete a token",
Long: "Remove a token by expiring it. Use --delete to permanently hard-" +
"delete the token instead.",
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
@@ -338,7 +362,7 @@ func (r *RootCmd) removeToken() *serpent.Command {
token, err := client.APIKeyByName(inv.Context(), codersdk.Me, inv.Args[0])
if err != nil {
// If it's a token, we need to extract the ID
// If it's a token, we need to extract the ID.
maybeID := strings.Split(inv.Args[0], "-")[0]
token, err = client.APIKeyByID(inv.Context(), codersdk.Me, maybeID)
if err != nil {
@@ -346,19 +370,31 @@ func (r *RootCmd) removeToken() *serpent.Command {
}
}
err = client.DeleteAPIKey(inv.Context(), codersdk.Me, token.ID)
if err != nil {
return xerrors.Errorf("delete api key: %w", err)
if deleteToken {
err = client.DeleteAPIKey(inv.Context(), codersdk.Me, token.ID)
if err != nil {
return xerrors.Errorf("delete api key: %w", err)
}
cliui.Infof(inv.Stdout, "Token has been deleted.")
return nil
}
cliui.Infof(
inv.Stdout,
"Token has been deleted.",
)
err = client.ExpireAPIKey(inv.Context(), codersdk.Me, token.ID)
if err != nil {
return xerrors.Errorf("expire api key: %w", err)
}
cliui.Infof(inv.Stdout, "Token has been expired.")
return nil
},
}
cmd.Options = serpent.OptionSet{
{
Flag: "delete",
Description: "Permanently delete the token instead of expiring it. This removes the audit trail.",
Value: serpent.BoolOf(&deleteToken),
},
}
return cmd
}
+144 -8
View File
@@ -6,12 +6,16 @@ import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"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/dbgen"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
)
@@ -22,7 +26,7 @@ func TestTokens(t *testing.T) {
adminUser := coderdtest.CreateFirstUser(t, client)
secondUserClient, secondUser := coderdtest.CreateAnotherUser(t, client, adminUser.OrganizationID)
_, thirdUser := coderdtest.CreateAnotherUser(t, client, adminUser.OrganizationID)
thirdUserClient, thirdUser := coderdtest.CreateAnotherUser(t, client, adminUser.OrganizationID)
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
@@ -155,7 +159,7 @@ func TestTokens(t *testing.T) {
require.Len(t, scopedToken.AllowList, 1)
require.Equal(t, allowSpec, scopedToken.AllowList[0].String())
// Delete by name
// Delete by name (default behavior is now expire)
inv, root = clitest.New(t, "tokens", "rm", "token-one")
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
@@ -164,10 +168,31 @@ func TestTokens(t *testing.T) {
require.NoError(t, err)
res = buf.String()
require.NotEmpty(t, res)
require.Contains(t, res, "deleted")
require.Contains(t, res, "expired")
// Delete by ID
// Regular users cannot expire other users' tokens (expire is default now).
inv, root = clitest.New(t, "tokens", "rm", secondTokenID)
clitest.SetupConfig(t, thirdUserClient, root)
buf = new(bytes.Buffer)
inv.Stdout = buf
err = inv.WithContext(ctx).Run()
require.Error(t, err)
require.Contains(t, err.Error(), "not found")
// Only admin users can expire other users' tokens (expire is default now).
inv, root = clitest.New(t, "tokens", "rm", secondTokenID)
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
inv.Stdout = buf
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
// Validate that token was expired
if token, err := client.APIKeyByName(ctx, secondUser.ID.String(), "token-two"); assert.NoError(t, err) {
require.True(t, token.ExpiresAt.Before(time.Now()))
}
// Delete by ID (explicit delete flag)
inv, root = clitest.New(t, "tokens", "rm", "--delete", secondTokenID)
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
inv.Stdout = buf
@@ -177,8 +202,8 @@ func TestTokens(t *testing.T) {
require.NotEmpty(t, res)
require.Contains(t, res, "deleted")
// Delete scoped token by ID
inv, root = clitest.New(t, "tokens", "rm", scopedTokenID)
// Delete scoped token by ID (explicit delete flag)
inv, root = clitest.New(t, "tokens", "rm", "--delete", scopedTokenID)
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
inv.Stdout = buf
@@ -199,8 +224,8 @@ func TestTokens(t *testing.T) {
require.NotEmpty(t, res)
fourthToken := res
// Delete by token
inv, root = clitest.New(t, "tokens", "rm", fourthToken)
// Delete by token (explicit delete flag)
inv, root = clitest.New(t, "tokens", "rm", "--delete", fourthToken)
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
inv.Stdout = buf
@@ -210,3 +235,114 @@ func TestTokens(t *testing.T) {
require.NotEmpty(t, res)
require.Contains(t, res, "deleted")
}
func TestTokensListExpiredFiltering(t *testing.T) {
t.Parallel()
client, _, api := coderdtest.NewWithAPI(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
// Create a valid (non-expired) token
validToken, _ := dbgen.APIKey(t, api.Database, database.APIKey{
UserID: owner.UserID,
ExpiresAt: time.Now().Add(24 * time.Hour),
LoginType: database.LoginTypeToken,
TokenName: "valid-token",
})
// Create an expired token
expiredToken, _ := dbgen.APIKey(t, api.Database, database.APIKey{
UserID: owner.UserID,
ExpiresAt: time.Now().Add(-24 * time.Hour),
LoginType: database.LoginTypeToken,
TokenName: "expired-token",
})
t.Run("HidesExpiredByDefault", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
inv, root := clitest.New(t, "tokens", "ls")
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
res := buf.String()
require.Contains(t, res, validToken.ID)
require.Contains(t, res, "valid-token")
require.NotContains(t, res, expiredToken.ID)
require.NotContains(t, res, "expired-token")
})
t.Run("ShowsExpiredWithFlag", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
inv, root := clitest.New(t, "tokens", "ls", "--include-expired")
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
res := buf.String()
require.Contains(t, res, validToken.ID)
require.Contains(t, res, "valid-token")
require.Contains(t, res, expiredToken.ID)
require.Contains(t, res, "expired-token")
})
t.Run("JSONOutputRespectsFilter", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Default (no expired)
inv, root := clitest.New(t, "tokens", "ls", "--output=json")
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
res := buf.String()
require.Contains(t, res, "valid-token")
require.NotContains(t, res, "expired-token")
// With --include-expired
inv, root = clitest.New(t, "tokens", "ls", "--output=json", "--include-expired")
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
inv.Stdout = buf
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
res = buf.String()
require.Contains(t, res, "valid-token")
require.Contains(t, res, "expired-token")
})
t.Run("AllUsersWithIncludeExpired", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
inv, root := clitest.New(t, "tokens", "ls", "--all", "--include-expired")
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
res := buf.String()
// Should show both valid and expired tokens
require.Contains(t, res, validToken.ID)
require.Contains(t, res, "valid-token")
require.Contains(t, res, expiredToken.ID)
require.Contains(t, res, "expired-token")
})
}
-24
View File
@@ -1,24 +0,0 @@
//go:build !windows && !darwin
package cli
import (
"golang.org/x/xerrors"
"github.com/coder/serpent"
)
func (*RootCmd) vpnDaemonRun() *serpent.Command {
cmd := &serpent.Command{
Use: "run",
Short: "Run the VPN daemon on Windows.",
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
),
Handler: func(_ *serpent.Invocation) error {
return xerrors.New("vpn-daemon subcommand is not supported on this platform")
},
}
return cmd
}
@@ -1,4 +1,4 @@
//go:build windows
//go:build windows || linux
package cli
@@ -11,7 +11,7 @@ import (
"github.com/coder/serpent"
)
func (r *RootCmd) vpnDaemonRun() *serpent.Command {
func (*RootCmd) vpnDaemonRun() *serpent.Command {
var (
rpcReadHandleInt int64
rpcWriteHandleInt int64
@@ -19,7 +19,7 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command {
cmd := &serpent.Command{
Use: "run",
Short: "Run the VPN daemon on Windows.",
Short: "Run the VPN daemon on Windows and Linux.",
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
),
@@ -53,8 +53,8 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command {
return xerrors.Errorf("rpc-read-handle (%v) and rpc-write-handle (%v) must be different", rpcReadHandleInt, rpcWriteHandleInt)
}
// We don't need to worry about duplicating the handles on Windows,
// which is different from Unix.
// The manager passes the read and write descriptors directly to the
// daemon, so we can open the RPC pipe from the raw values.
logger.Info(ctx, "opening bidirectional RPC pipe", slog.F("rpc_read_handle", rpcReadHandleInt), slog.F("rpc_write_handle", rpcWriteHandleInt))
pipe, err := vpn.NewBidirectionalPipe(uintptr(rpcReadHandleInt), uintptr(rpcWriteHandleInt))
if err != nil {
@@ -62,7 +62,7 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command {
}
defer pipe.Close()
logger.Info(ctx, "starting tunnel")
logger.Info(ctx, "starting VPN tunnel")
tunnel, err := vpn.NewTunnel(ctx, logger, pipe, vpn.NewClient(), vpn.UseOSNetworkingStack())
if err != nil {
return xerrors.Errorf("create new tunnel for client: %w", err)
@@ -1,4 +1,4 @@
//go:build windows
//go:build windows || linux
package cli_test
@@ -67,22 +67,35 @@ func TestVPNDaemonRun(t *testing.T) {
r1, w1, err := os.Pipe()
require.NoError(t, err)
defer r1.Close()
defer w1.Close()
r2, w2, err := os.Pipe()
require.NoError(t, err)
defer r2.Close()
defer w2.Close()
// The daemon closes the handles passed via NewBidirectionalPipe. Since our
// CLI tests run in-process, pass duplicated handles so we can close the
// originals without risking a double-close on FD reuse.
rpcReadHandle := dupHandle(t, r1)
rpcWriteHandle := dupHandle(t, w2)
require.NoError(t, r1.Close())
require.NoError(t, w2.Close())
ctx := testutil.Context(t, testutil.WaitLong)
inv, _ := clitest.New(t, "vpn-daemon", "run", "--rpc-read-handle", fmt.Sprint(r1.Fd()), "--rpc-write-handle", fmt.Sprint(w2.Fd()))
inv, _ := clitest.New(t,
"vpn-daemon",
"run",
"--rpc-read-handle",
fmt.Sprint(rpcReadHandle),
"--rpc-write-handle",
fmt.Sprint(rpcWriteHandle),
)
waiter := clitest.StartWithWaiter(t, inv.WithContext(ctx))
// Send garbage which should cause the handshake to fail and the daemon
// to exit.
_, err = w1.Write([]byte("garbage"))
// Send an invalid header, including a newline delimiter, so the handshake
// fails without requiring context cancellation.
_, err = w1.Write([]byte("garbage\n"))
require.NoError(t, err)
waiter.Cancel()
err = waiter.Wait()
require.ErrorContains(t, err, "handshake failed")
})
@@ -0,0 +1,19 @@
//go:build linux
package cli_test
import (
"os"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
func dupHandle(t *testing.T, f *os.File) uintptr {
t.Helper()
dupFD, err := unix.Dup(int(f.Fd()))
require.NoError(t, err)
return uintptr(dupFD)
}
@@ -0,0 +1,33 @@
//go:build windows
package cli_test
import (
"os"
"syscall"
"testing"
"github.com/stretchr/testify/require"
)
func dupHandle(t *testing.T, f *os.File) uintptr {
t.Helper()
src := syscall.Handle(f.Fd())
var dup syscall.Handle
proc, err := syscall.GetCurrentProcess()
require.NoError(t, err)
err = syscall.DuplicateHandle(
proc,
src,
proc,
&dup,
0,
false,
syscall.DUPLICATE_SAME_ACCESS,
)
require.NoError(t, err)
return uintptr(dup)
}
+87
View File
@@ -1304,3 +1304,90 @@ func (api *API) pauseTask(rw http.ResponseWriter, r *http.Request) {
WorkspaceBuild: &build,
})
}
// @Summary Resume task
// @ID resume-task
// @Security CoderSessionToken
// @Accept json
// @Tags Tasks
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param task path string true "Task ID" format(uuid)
// @Success 202 {object} codersdk.ResumeTaskResponse
// @Router /tasks/{user}/{task}/resume [post]
func (api *API) resumeTask(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
apiKey = httpmw.APIKey(r)
task = httpmw.TaskParam(r)
)
if !task.WorkspaceID.Valid {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Task does not have a workspace.",
})
return
}
workspace, err := api.Database.GetWorkspaceByID(ctx, task.WorkspaceID.UUID)
if err != nil {
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching task workspace.",
Detail: err.Error(),
})
return
}
latestBuild, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching task workspace build.",
Detail: err.Error(),
})
return
}
job, err := api.Database.GetProvisionerJobByID(ctx, latestBuild.JobID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching task workspace build job.",
Detail: err.Error(),
})
return
}
workspaceStatus := codersdk.ConvertWorkspaceStatus(
codersdk.ProvisionerJobStatus(job.JobStatus),
codersdk.WorkspaceTransition(latestBuild.Transition),
)
if workspaceStatus == codersdk.WorkspaceStatusRunning {
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: "Task workspace is already running.",
Detail: fmt.Sprintf("Workspace status is %q.", workspaceStatus),
})
return
}
buildReq := codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
Reason: codersdk.CreateWorkspaceBuildReasonTaskResume,
}
build, err := api.postWorkspaceBuildsInternal(
ctx,
apiKey,
workspace,
buildReq,
func(action policy.Action, object rbac.Objecter) bool {
return api.Authorize(r, action, object)
},
audit.WorkspaceBuildBaggageFromRequest(r),
)
if err != nil {
httperror.WriteWorkspaceBuildError(ctx, rw, err)
return
}
httpapi.Write(ctx, rw, http.StatusAccepted, codersdk.ResumeTaskResponse{
WorkspaceBuild: &build,
})
}
+337 -1
View File
@@ -2512,13 +2512,20 @@ func TestPauseTask(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
resp, err := client.PauseTask(ctx, codersdk.Me, task.ID)
// Verify that the request was accepted correctly:
require.NoError(t, err)
build := *resp.WorkspaceBuild
require.NotNil(t, build)
require.Equal(t, codersdk.WorkspaceTransitionStop, build.Transition)
require.Equal(t, task.WorkspaceID.UUID, build.WorkspaceID)
require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber)
require.Equal(t, string(codersdk.CreateWorkspaceBuildReasonTaskManualPause), string(build.Reason))
// Verify that the accepted request was processed correctly:
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
workspace, err = client.Workspace(ctx, task.WorkspaceID.UUID)
require.NoError(t, err)
require.Equal(t, codersdk.WorkspaceStatusStopped, workspace.LatestBuild.Status)
})
t.Run("Non-owner role access", func(t *testing.T) {
@@ -2781,3 +2788,332 @@ func TestPauseTask(t *testing.T) {
require.Equal(t, http.StatusInternalServerError, apiErr.StatusCode())
})
}
func TestResumeTask(t *testing.T) {
t.Parallel()
setupClient := func(t *testing.T, db database.Store, ps pubsub.Pubsub, authorizer rbac.Authorizer) *codersdk.Client {
t.Helper()
client, _, _ := coderdtest.NewWithAPI(t, &coderdtest.Options{
Database: db,
Pubsub: ps,
Authorizer: authorizer,
IncludeProvisionerDaemon: true,
})
return client
}
setupWorkspaceTask := func(t *testing.T, db database.Store, user codersdk.CreateFirstUserResponse) (database.Task, uuid.UUID) {
t.Helper()
workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
}).WithTask(database.TaskTable{
Prompt: "resume me",
}, nil).Do()
return workspaceBuild.Task, workspaceBuild.Workspace.ID
}
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
task, err := client.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
TemplateVersionID: template.ActiveVersionID,
Input: "resume me",
})
require.NoError(t, err)
workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
pauseResp, err := client.PauseTask(ctx, codersdk.Me, task.ID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, pauseResp.WorkspaceBuild.ID)
resumeResp, err := client.ResumeTask(ctx, codersdk.Me, task.ID)
require.NoError(t, err)
build := *resumeResp.WorkspaceBuild
require.Equal(t, codersdk.WorkspaceTransitionStart, build.Transition)
require.Equal(t, task.WorkspaceID.UUID, build.WorkspaceID)
require.Equal(t, workspace.LatestBuild.BuildNumber+2, build.BuildNumber)
require.Equal(t, string(codersdk.CreateWorkspaceBuildReasonTaskResume), string(build.Reason))
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
workspace, err = client.Workspace(ctx, task.WorkspaceID.UUID)
require.NoError(t, err)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspace.LatestBuild.Status)
})
t.Run("Resume a task that is not paused", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
db, ps := dbtestutil.NewDB(t)
client := setupClient(t, db, ps, nil)
user := coderdtest.CreateFirstUser(t, client)
workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
}).
WithTask(database.TaskTable{
Prompt: "pause me",
}, nil).
Succeeded().
Do()
_, err := client.ResumeTask(ctx, codersdk.Me, workspaceBuild.Task.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("Task not found", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.ResumeTask(ctx, codersdk.Me, uuid.New())
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Task lookup forbidden", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, ps := dbtestutil.NewDB(t)
auth := &coderdtest.FakeAuthorizer{
ConditionalReturn: func(_ context.Context, _ rbac.Subject, action policy.Action, object rbac.Object) error {
if action == policy.ActionRead && object.Type == rbac.ResourceTask.Type {
return rbac.UnauthorizedError{}
}
return nil
},
}
client := setupClient(t, db, ps, auth)
user := coderdtest.CreateFirstUser(t, client)
task, _ := setupWorkspaceTask(t, db, user)
_, err := client.ResumeTask(ctx, codersdk.Me, task.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Workspace lookup forbidden", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, ps := dbtestutil.NewDB(t)
auth := &coderdtest.FakeAuthorizer{
ConditionalReturn: func(_ context.Context, _ rbac.Subject, action policy.Action, object rbac.Object) error {
if action == policy.ActionRead && object.Type == rbac.ResourceWorkspace.Type {
return rbac.UnauthorizedError{}
}
return nil
},
}
client := setupClient(t, db, ps, auth)
user := coderdtest.CreateFirstUser(t, client)
task, _ := setupWorkspaceTask(t, db, user)
_, err := client.ResumeTask(ctx, codersdk.Me, task.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("No Workspace for Task", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, ps := dbtestutil.NewDB(t)
client := setupClient(t, db, ps, nil)
user := coderdtest.CreateFirstUser(t, client)
workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
}).Do()
task := dbgen.Task(t, db, database.TaskTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
TemplateVersionID: workspaceBuild.Build.TemplateVersionID,
Prompt: "no workspace",
})
_, err := client.ResumeTask(ctx, codersdk.Me, task.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusInternalServerError, apiErr.StatusCode())
require.Equal(t, "Task does not have a workspace.", apiErr.Message)
})
t.Run("Workspace not found", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, ps := dbtestutil.NewDB(t)
var workspaceID uuid.UUID
wrapped := aiTaskStoreWrapper{
Store: db,
getWorkspaceByID: func(ctx context.Context, id uuid.UUID) (database.Workspace, error) {
if id == workspaceID && id != uuid.Nil {
return database.Workspace{}, sql.ErrNoRows
}
return db.GetWorkspaceByID(ctx, id)
},
}
client := setupClient(t, wrapped, ps, nil)
user := coderdtest.CreateFirstUser(t, client)
task, workspaceIDValue := setupWorkspaceTask(t, db, user)
workspaceID = workspaceIDValue
_, err := client.ResumeTask(ctx, codersdk.Me, task.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("Workspace lookup internal error", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, ps := dbtestutil.NewDB(t)
var workspaceID uuid.UUID
wrapped := aiTaskStoreWrapper{
Store: db,
getWorkspaceByID: func(ctx context.Context, id uuid.UUID) (database.Workspace, error) {
if id == workspaceID && id != uuid.Nil {
return database.Workspace{}, xerrors.New("boom")
}
return db.GetWorkspaceByID(ctx, id)
},
}
client := setupClient(t, wrapped, ps, nil)
user := coderdtest.CreateFirstUser(t, client)
task, workspaceIDValue := setupWorkspaceTask(t, db, user)
workspaceID = workspaceIDValue
_, err := client.ResumeTask(ctx, codersdk.Me, task.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusInternalServerError, apiErr.StatusCode())
require.Equal(t, "Internal error fetching task workspace.", apiErr.Message)
})
t.Run("Build Forbidden", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, ps := dbtestutil.NewDB(t)
auth := &coderdtest.FakeAuthorizer{
ConditionalReturn: func(_ context.Context, _ rbac.Subject, action policy.Action, object rbac.Object) error {
if action == policy.ActionWorkspaceStart && object.Type == rbac.ResourceWorkspace.Type {
return rbac.UnauthorizedError{}
}
return nil
},
}
client := setupClient(t, db, ps, auth)
user := coderdtest.CreateFirstUser(t, client)
task, _ := setupWorkspaceTask(t, db, user)
pauseResp, err := client.PauseTask(ctx, codersdk.Me, task.ID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, pauseResp.WorkspaceBuild.ID)
_, err = client.ResumeTask(ctx, codersdk.Me, task.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
})
t.Run("Job already in progress", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, ps := dbtestutil.NewDB(t)
client := setupClient(t, db, ps, nil)
user := coderdtest.CreateFirstUser(t, client)
workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
}).
WithTask(database.TaskTable{
Prompt: "resume me",
}, nil).
Starting().
Do()
_, err := client.ResumeTask(ctx, codersdk.Me, workspaceBuild.Task.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
t.Run("Build Internal Error", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, ps := dbtestutil.NewDB(t)
wrapped := aiTaskStoreWrapper{
Store: db,
}
client := setupClient(t, &wrapped, ps, nil)
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionGraph: []*proto.Response{
{Type: &proto.Response_Graph{Graph: &proto.GraphComplete{
HasAiTasks: true,
}}},
},
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
task, err := client.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
TemplateVersionID: template.ActiveVersionID,
Input: "resume me",
})
require.NoError(t, err)
workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
pauseResp, err := client.PauseTask(ctx, codersdk.Me, task.ID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, pauseResp.WorkspaceBuild.ID)
// Induce a transient failure in the database after the task has been paused.
wrapped.insertWorkspaceBuild = func(ctx context.Context, arg database.InsertWorkspaceBuildParams) error {
return xerrors.New("insert failed")
}
_, err = client.ResumeTask(ctx, codersdk.Me, task.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusInternalServerError, apiErr.StatusCode())
})
}
+116 -4
View File
@@ -5866,6 +5866,48 @@ const docTemplate = `{
}
}
},
"/tasks/{user}/{task}/resume": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"tags": [
"Tasks"
],
"summary": "Resume task",
"operationId": "resume-task",
"parameters": [
{
"type": "string",
"description": "Username, user ID, or 'me' for the authenticated user",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "task",
"in": "path",
"required": true
}
],
"responses": {
"202": {
"description": "Accepted",
"schema": {
"$ref": "#/definitions/codersdk.ResumeTaskResponse"
}
}
}
}
},
"/tasks/{user}/{task}/send": {
"post": {
"security": [
@@ -8344,6 +8386,54 @@ const docTemplate = `{
}
}
},
"/users/{user}/keys/{keyid}/expire": {
"put": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": [
"Users"
],
"summary": "Expire API key",
"operationId": "expire-api-key",
"parameters": [
{
"type": "string",
"description": "User ID, name, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "string",
"description": "Key ID",
"name": "keyid",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
}
}
}
},
"/users/{user}/login-type": {
"get": {
"security": [
@@ -12261,6 +12351,9 @@ const docTemplate = `{
"api_key_id": {
"type": "string"
},
"client": {
"type": "string"
},
"ended_at": {
"type": "string",
"format": "date-time"
@@ -13460,7 +13553,10 @@ const docTemplate = `{
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
"jetbrains_connection",
"task_auto_pause",
"task_manual_pause",
"task_resume"
],
"x-enum-varnames": [
"BuildReasonInitiator",
@@ -13471,7 +13567,10 @@ const docTemplate = `{
"BuildReasonCLI",
"BuildReasonSSHConnection",
"BuildReasonVSCodeConnection",
"BuildReasonJetbrainsConnection"
"BuildReasonJetbrainsConnection",
"BuildReasonTaskAutoPause",
"BuildReasonTaskManualPause",
"BuildReasonTaskResume"
]
},
"codersdk.CORSBehavior": {
@@ -14145,7 +14244,8 @@ const docTemplate = `{
"ssh_connection",
"vscode_connection",
"jetbrains_connection",
"task_manual_pause"
"task_manual_pause",
"task_resume"
],
"x-enum-varnames": [
"CreateWorkspaceBuildReasonDashboard",
@@ -14153,7 +14253,8 @@ const docTemplate = `{
"CreateWorkspaceBuildReasonSSHConnection",
"CreateWorkspaceBuildReasonVSCodeConnection",
"CreateWorkspaceBuildReasonJetbrainsConnection",
"CreateWorkspaceBuildReasonTaskManualPause"
"CreateWorkspaceBuildReasonTaskManualPause",
"CreateWorkspaceBuildReasonTaskResume"
]
},
"codersdk.CreateWorkspaceBuildRequest": {
@@ -18235,6 +18336,14 @@ const docTemplate = `{
}
}
},
"codersdk.ResumeTaskResponse": {
"type": "object",
"properties": {
"workspace_build": {
"$ref": "#/definitions/codersdk.WorkspaceBuild"
}
}
},
"codersdk.RetentionConfig": {
"type": "object",
"properties": {
@@ -18867,6 +18976,9 @@ const docTemplate = `{
"default_ttl_ms": {
"type": "integer"
},
"deleted": {
"type": "boolean"
},
"deprecated": {
"type": "boolean"
},
+110 -4
View File
@@ -5185,6 +5185,44 @@
}
}
},
"/tasks/{user}/{task}/resume": {
"post": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"tags": ["Tasks"],
"summary": "Resume task",
"operationId": "resume-task",
"parameters": [
{
"type": "string",
"description": "Username, user ID, or 'me' for the authenticated user",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"name": "task",
"in": "path",
"required": true
}
],
"responses": {
"202": {
"description": "Accepted",
"schema": {
"$ref": "#/definitions/codersdk.ResumeTaskResponse"
}
}
}
}
},
"/tasks/{user}/{task}/send": {
"post": {
"security": [
@@ -7379,6 +7417,52 @@
}
}
},
"/users/{user}/keys/{keyid}/expire": {
"put": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": ["Users"],
"summary": "Expire API key",
"operationId": "expire-api-key",
"parameters": [
{
"type": "string",
"description": "User ID, name, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "string",
"description": "Key ID",
"name": "keyid",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/codersdk.Response"
}
}
}
}
},
"/users/{user}/login-type": {
"get": {
"security": [
@@ -10887,6 +10971,9 @@
"api_key_id": {
"type": "string"
},
"client": {
"type": "string"
},
"ended_at": {
"type": "string",
"format": "date-time"
@@ -12061,7 +12148,10 @@
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
"jetbrains_connection",
"task_auto_pause",
"task_manual_pause",
"task_resume"
],
"x-enum-varnames": [
"BuildReasonInitiator",
@@ -12072,7 +12162,10 @@
"BuildReasonCLI",
"BuildReasonSSHConnection",
"BuildReasonVSCodeConnection",
"BuildReasonJetbrainsConnection"
"BuildReasonJetbrainsConnection",
"BuildReasonTaskAutoPause",
"BuildReasonTaskManualPause",
"BuildReasonTaskResume"
]
},
"codersdk.CORSBehavior": {
@@ -12701,7 +12794,8 @@
"ssh_connection",
"vscode_connection",
"jetbrains_connection",
"task_manual_pause"
"task_manual_pause",
"task_resume"
],
"x-enum-varnames": [
"CreateWorkspaceBuildReasonDashboard",
@@ -12709,7 +12803,8 @@
"CreateWorkspaceBuildReasonSSHConnection",
"CreateWorkspaceBuildReasonVSCodeConnection",
"CreateWorkspaceBuildReasonJetbrainsConnection",
"CreateWorkspaceBuildReasonTaskManualPause"
"CreateWorkspaceBuildReasonTaskManualPause",
"CreateWorkspaceBuildReasonTaskResume"
]
},
"codersdk.CreateWorkspaceBuildRequest": {
@@ -16647,6 +16742,14 @@
}
}
},
"codersdk.ResumeTaskResponse": {
"type": "object",
"properties": {
"workspace_build": {
"$ref": "#/definitions/codersdk.WorkspaceBuild"
}
}
},
"codersdk.RetentionConfig": {
"type": "object",
"properties": {
@@ -17258,6 +17361,9 @@
"default_ttl_ms": {
"type": "integer"
},
"deleted": {
"type": "boolean"
},
"deprecated": {
"type": "boolean"
},
+63
View File
@@ -421,6 +421,69 @@ func (api *API) deleteAPIKey(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent)
}
// @Summary Expire API key
// @ID expire-api-key
// @Security CoderSessionToken
// @Tags Users
// @Param user path string true "User ID, name, or me"
// @Param keyid path string true "Key ID" format(string)
// @Success 204
// @Failure 404 {object} codersdk.Response
// @Failure 500 {object} codersdk.Response
// @Router /users/{user}/keys/{keyid}/expire [put]
func (api *API) expireAPIKey(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
keyID = chi.URLParam(r, "keyid")
auditor = api.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.APIKey](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
)
defer commitAudit()
if err := api.Database.InTx(func(db database.Store) error {
key, err := db.GetAPIKeyByID(ctx, keyID)
if err != nil {
return xerrors.Errorf("fetch API key: %w", err)
}
if !key.ExpiresAt.After(api.Clock.Now()) {
return nil // Already expired
}
aReq.Old = key
if err := db.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
ID: key.ID,
LastUsed: key.LastUsed,
ExpiresAt: dbtime.Now(),
IPAddress: key.IPAddress,
}); err != nil {
return xerrors.Errorf("expire API key: %w", err)
}
// Fetch the updated key for audit log.
newKey, err := db.GetAPIKeyByID(ctx, keyID)
if err != nil {
api.Logger.Warn(ctx, "failed to fetch updated API key for audit log", slog.Error(err))
} else {
aReq.New = newKey
}
return nil
}, nil); httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
} else if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error expiring API key.",
Detail: err.Error(),
})
return
}
rw.WriteHeader(http.StatusNoContent)
}
// @Summary Get token config
// @ID get-token-config
// @Security CoderSessionToken
+159 -4
View File
@@ -400,7 +400,7 @@ func TestAPIKey_Deleted(t *testing.T) {
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
}
func TestAPIKey_SetDefault(t *testing.T) {
@@ -439,7 +439,7 @@ func TestAPIKey_PrebuildsNotAllowed(t *testing.T) {
DeploymentValues: dc,
})
ctx := testutil.Context(t, testutil.WaitLong)
setupCtx := testutil.Context(t, testutil.WaitLong)
// Given: an existing api token for the prebuilds user
_, prebuildsToken := dbgen.APIKey(t, db, database.APIKey{
@@ -448,12 +448,167 @@ func TestAPIKey_PrebuildsNotAllowed(t *testing.T) {
client.SetSessionToken(prebuildsToken)
// When: the prebuilds user tries to create an API key
_, err := client.CreateAPIKey(ctx, database.PrebuildsSystemUserID.String())
_, err := client.CreateAPIKey(setupCtx, database.PrebuildsSystemUserID.String())
// Then: denied.
require.ErrorContains(t, err, httpapi.ResourceForbiddenResponse.Message)
// When: the prebuilds user tries to create a token
_, err = client.CreateToken(ctx, database.PrebuildsSystemUserID.String(), codersdk.CreateTokenRequest{})
_, err = client.CreateToken(setupCtx, database.PrebuildsSystemUserID.String(), codersdk.CreateTokenRequest{})
// Then: also denied.
require.ErrorContains(t, err, httpapi.ResourceForbiddenResponse.Message)
}
//nolint:tparallel,paralleltest // Subtests share the same coderdtest instance and auditor.
func TestExpireAPIKey(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
adminClient := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
admin := coderdtest.CreateFirstUser(t, adminClient)
memberClient, member := coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID)
t.Run("OwnerCanExpireOwnToken", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
// Create a token.
res, err := adminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 7,
})
require.NoError(t, err)
keyID := strings.Split(res.Key, "-")[0]
// Verify the token is not expired.
key, err := adminClient.APIKeyByID(ctx, codersdk.Me, keyID)
require.NoError(t, err)
require.True(t, key.ExpiresAt.After(time.Now()))
auditor.ResetLogs()
// Expire the token.
err = adminClient.ExpireAPIKey(ctx, codersdk.Me, keyID)
require.NoError(t, err)
// Verify the token is expired.
key, err = adminClient.APIKeyByID(ctx, codersdk.Me, keyID)
require.NoError(t, err)
require.True(t, key.ExpiresAt.Before(time.Now()))
// Verify audit log.
als := auditor.AuditLogs()
require.Len(t, als, 1)
require.Equal(t, database.AuditActionWrite, als[0].Action)
require.Equal(t, database.ResourceTypeApiKey, als[0].ResourceType)
require.Equal(t, admin.UserID.String(), als[0].UserID.String())
})
t.Run("AdminCanExpireOtherUsersToken", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
// Create a token for the member.
res, err := memberClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 7,
})
require.NoError(t, err)
keyID := strings.Split(res.Key, "-")[0]
// Admin expires the member's token.
err = adminClient.ExpireAPIKey(ctx, member.ID.String(), keyID)
require.NoError(t, err)
// Verify the token is expired.
key, err := memberClient.APIKeyByID(ctx, codersdk.Me, keyID)
require.NoError(t, err)
require.True(t, key.ExpiresAt.Before(time.Now()))
})
t.Run("MemberCannotExpireOtherUsersToken", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
// Create a token for the admin.
res, err := adminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 7,
})
require.NoError(t, err)
keyID := strings.Split(res.Key, "-")[0]
// Member attempts to expire admin's token.
err = memberClient.ExpireAPIKey(ctx, admin.UserID.String(), keyID)
require.Error(t, err)
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
// Members cannot read other users, so they get a 404 Not Found
// from the authorization layer.
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
})
t.Run("NotFound", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
// Try to expire a non-existent token.
err := adminClient.ExpireAPIKey(ctx, codersdk.Me, "nonexistent")
require.Error(t, err)
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
})
t.Run("ExpiringAlreadyExpiredTokenSucceeds", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
// Create and expire a token.
res, err := adminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 7,
})
require.NoError(t, err)
keyID := strings.Split(res.Key, "-")[0]
// Expire it once.
err = adminClient.ExpireAPIKey(ctx, codersdk.Me, keyID)
require.NoError(t, err)
// Invariant: make sure it's actually expired
key, err := adminClient.APIKeyByID(ctx, codersdk.Me, keyID)
require.NoError(t, err)
require.LessOrEqual(t, key.ExpiresAt, time.Now(), "key should be expired")
// Expire it again - should succeed (idempotent).
err = adminClient.ExpireAPIKey(ctx, codersdk.Me, keyID)
require.NoError(t, err)
// Token should still be just as expired as before. No more, no less.
keyAgain, err := adminClient.APIKeyByID(ctx, codersdk.Me, keyID)
require.NoError(t, err)
require.Equal(t, key.ExpiresAt, keyAgain.ExpiresAt, "expiration should be idempotent")
})
t.Run("DeletingExpiredTokenSucceeds", func(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
// Create a token.
res, err := adminClient.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: time.Hour * 24 * 7,
})
require.NoError(t, err)
keyID := strings.Split(res.Key, "-")[0]
// Expire it first.
err = adminClient.ExpireAPIKey(ctx, codersdk.Me, keyID)
require.NoError(t, err)
// Verify it's expired.
key, err := adminClient.APIKeyByID(ctx, codersdk.Me, keyID)
require.NoError(t, err)
require.True(t, key.ExpiresAt.Before(time.Now()))
// Delete the expired token - should succeed.
err = adminClient.DeleteAPIKey(ctx, codersdk.Me, keyID)
require.NoError(t, err)
// Verify it's gone.
_, err = adminClient.APIKeyByID(ctx, codersdk.Me, keyID)
require.Error(t, err)
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
})
}
+29 -18
View File
@@ -48,9 +48,10 @@ type Executor struct {
tick <-chan time.Time
statsCh chan<- Stats
// NotificationsEnqueuer handles enqueueing notifications for delivery by SMTP, webhook, etc.
notificationsEnqueuer notifications.Enqueuer
reg prometheus.Registerer
experiments codersdk.Experiments
notificationsEnqueuer notifications.Enqueuer
reg prometheus.Registerer
experiments codersdk.Experiments
workspaceBuilderMetrics *wsbuilder.Metrics
metrics executorMetrics
}
@@ -67,23 +68,24 @@ type Stats struct {
}
// New returns a new wsactions executor.
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, fc *files.Cache, reg prometheus.Registerer, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], buildUsageChecker *atomic.Pointer[wsbuilder.UsageChecker], log slog.Logger, tick <-chan time.Time, enqueuer notifications.Enqueuer, exp codersdk.Experiments) *Executor {
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, fc *files.Cache, reg prometheus.Registerer, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], buildUsageChecker *atomic.Pointer[wsbuilder.UsageChecker], log slog.Logger, tick <-chan time.Time, enqueuer notifications.Enqueuer, exp codersdk.Experiments, workspaceBuilderMetrics *wsbuilder.Metrics) *Executor {
factory := promauto.With(reg)
le := &Executor{
//nolint:gocritic // Autostart has a limited set of permissions.
ctx: dbauthz.AsAutostart(ctx),
db: db,
ps: ps,
fileCache: fc,
templateScheduleStore: tss,
tick: tick,
log: log.Named("autobuild"),
auditor: auditor,
accessControlStore: acs,
buildUsageChecker: buildUsageChecker,
notificationsEnqueuer: enqueuer,
reg: reg,
experiments: exp,
ctx: dbauthz.AsAutostart(ctx),
db: db,
ps: ps,
fileCache: fc,
templateScheduleStore: tss,
tick: tick,
log: log.Named("autobuild"),
auditor: auditor,
accessControlStore: acs,
buildUsageChecker: buildUsageChecker,
notificationsEnqueuer: enqueuer,
reg: reg,
experiments: exp,
workspaceBuilderMetrics: workspaceBuilderMetrics,
metrics: executorMetrics{
autobuildExecutionDuration: factory.NewHistogram(prometheus.HistogramOpts{
Namespace: "coderd",
@@ -335,7 +337,8 @@ func (e *Executor) runOnce(t time.Time) Stats {
SetLastWorkspaceBuildInTx(&latestBuild).
SetLastWorkspaceBuildJobInTx(&latestJob).
Experiments(e.experiments).
Reason(reason)
Reason(reason).
BuildMetrics(e.workspaceBuilderMetrics)
log.Debug(e.ctx, "auto building workspace", slog.F("transition", nextTransition))
if nextTransition == database.WorkspaceTransitionStart &&
useActiveVersion(accessControl, ws) {
@@ -522,10 +525,18 @@ func getNextTransition(
) {
switch {
case isEligibleForAutostop(user, ws, latestBuild, latestJob, currentTick):
// Use task-specific reason for AI task workspaces.
if ws.TaskID.Valid {
return database.WorkspaceTransitionStop, database.BuildReasonTaskAutoPause, nil
}
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
case isEligibleForAutostart(user, ws, latestBuild, latestJob, templateSchedule, currentTick):
return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil
case isEligibleForFailedStop(latestBuild, latestJob, templateSchedule, currentTick):
// Use task-specific reason for AI task workspaces.
if ws.TaskID.Valid {
return database.WorkspaceTransitionStop, database.BuildReasonTaskAutoPause, nil
}
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
case isEligibleForDormantStop(ws, templateSchedule, currentTick):
// Only stop started workspaces.
@@ -5,12 +5,113 @@ import (
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/schedule"
)
func Test_getNextTransition_TaskAutoPause(t *testing.T) {
t.Parallel()
// Set up a workspace that is eligible for autostop (past deadline).
now := time.Now()
pastDeadline := now.Add(-time.Hour)
okUser := database.User{Status: database.UserStatusActive}
okBuild := database.WorkspaceBuild{
Transition: database.WorkspaceTransitionStart,
Deadline: pastDeadline,
}
okJob := database.ProvisionerJob{
JobStatus: database.ProvisionerJobStatusSucceeded,
}
okTemplateSchedule := schedule.TemplateScheduleOptions{}
// Failed build setup for failedstop tests.
failedBuild := database.WorkspaceBuild{
Transition: database.WorkspaceTransitionStart,
}
failedJob := database.ProvisionerJob{
JobStatus: database.ProvisionerJobStatusFailed,
CompletedAt: sql.NullTime{Time: now.Add(-time.Hour), Valid: true},
}
failedTemplateSchedule := schedule.TemplateScheduleOptions{
FailureTTL: time.Minute, // TTL already elapsed since job completed an hour ago.
}
testCases := []struct {
Name string
Workspace database.Workspace
Build database.WorkspaceBuild
Job database.ProvisionerJob
TemplateSchedule schedule.TemplateScheduleOptions
ExpectedReason database.BuildReason
}{
{
Name: "RegularWorkspace_Autostop",
Workspace: database.Workspace{
DormantAt: sql.NullTime{Valid: false},
},
Build: okBuild,
Job: okJob,
TemplateSchedule: okTemplateSchedule,
ExpectedReason: database.BuildReasonAutostop,
},
{
Name: "TaskWorkspace_Autostop_UsesTaskAutoPause",
Workspace: database.Workspace{
DormantAt: sql.NullTime{Valid: false},
TaskID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
},
Build: okBuild,
Job: okJob,
TemplateSchedule: okTemplateSchedule,
ExpectedReason: database.BuildReasonTaskAutoPause,
},
{
Name: "RegularWorkspace_FailedStop",
Workspace: database.Workspace{
DormantAt: sql.NullTime{Valid: false},
},
Build: failedBuild,
Job: failedJob,
TemplateSchedule: failedTemplateSchedule,
ExpectedReason: database.BuildReasonAutostop,
},
{
Name: "TaskWorkspace_FailedStop_UsesTaskAutoPause",
Workspace: database.Workspace{
DormantAt: sql.NullTime{Valid: false},
TaskID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
},
Build: failedBuild,
Job: failedJob,
TemplateSchedule: failedTemplateSchedule,
ExpectedReason: database.BuildReasonTaskAutoPause,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
transition, reason, err := getNextTransition(
okUser,
tc.Workspace,
tc.Build,
tc.Job,
tc.TemplateSchedule,
now,
)
require.NoError(t, err)
require.Equal(t, database.WorkspaceTransitionStop, transition)
require.Equal(t, tc.ExpectedReason, reason)
})
}
}
func Test_isEligibleForAutostart(t *testing.T) {
t.Parallel()
@@ -2019,5 +2019,11 @@ func TestExecutorTaskWorkspace(t *testing.T) {
assert.Contains(t, stats.Transitions, workspace.ID, "task workspace should be in transitions")
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID], "should autostop the workspace")
require.Empty(t, stats.Errors, "should have no errors when managing task workspaces")
// Then: The build reason should be TaskAutoPause (not regular Autostop)
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
assert.Equal(t, codersdk.BuildReasonTaskAutoPause, workspace.LatestBuild.Reason, "task workspace should use TaskAutoPause build reason")
})
}
+3
View File
@@ -245,6 +245,7 @@ type Options struct {
MetadataBatcherOptions []metadatabatcher.Option
ProvisionerdServerMetrics *provisionerdserver.Metrics
WorkspaceBuilderMetrics *wsbuilder.Metrics
// WorkspaceAppAuditSessionTimeout allows changing the timeout for audit
// sessions. Raising or lowering this value will directly affect the write
@@ -1079,6 +1080,7 @@ func New(options *Options) *API {
r.Post("/send", api.taskSend)
r.Get("/logs", api.taskLogs)
r.Post("/pause", api.pauseTask)
r.Post("/resume", api.resumeTask)
})
})
})
@@ -1397,6 +1399,7 @@ func New(options *Options) *API {
r.Route("/{keyid}", func(r chi.Router) {
r.Get("/", api.apiKeyByID)
r.Delete("/", api.deleteAPIKey)
r.Put("/expire", api.expireAPIKey)
})
})
+3
View File
@@ -191,6 +191,7 @@ type Options struct {
TelemetryReporter telemetry.Reporter
ProvisionerdServerMetrics *provisionerdserver.Metrics
WorkspaceBuilderMetrics *wsbuilder.Metrics
UsageInserter usage.Inserter
}
@@ -399,6 +400,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
options.AutobuildTicker,
options.NotificationsEnqueuer,
experiments,
options.WorkspaceBuilderMetrics,
).WithStatsChannel(options.AutobuildStats)
lifecycleExecutor.Run()
@@ -620,6 +622,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
AppEncryptionKeyCache: options.APIKeyEncryptionCache,
OIDCConvertKeyCache: options.OIDCConvertKeyCache,
ProvisionerdServerMetrics: options.ProvisionerdServerMetrics,
WorkspaceBuilderMetrics: options.WorkspaceBuilderMetrics,
}
}
+2
View File
@@ -17,4 +17,6 @@ const (
CheckTelemetryLockEventTypeConstraint CheckConstraint = "telemetry_lock_event_type_constraint" // telemetry_locks
CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters
CheckUsageEventTypeCheck CheckConstraint = "usage_event_type_check" // usage_events
CheckGroupAclIsObject CheckConstraint = "group_acl_is_object" // workspaces
CheckUserAclIsObject CheckConstraint = "user_acl_is_object" // workspaces
)
-1
View File
@@ -93,7 +93,6 @@ type TxOptions struct {
// IncrementExecutionCount is a helper function for external packages
// to increment the unexported count.
// Mainly for `dbmem`.
func IncrementExecutionCount(opts *TxOptions) {
opts.executionCount++
}
+3
View File
@@ -981,6 +981,9 @@ func AIBridgeInterception(interception database.AIBridgeInterception, initiator
if interception.EndedAt.Valid {
intc.EndedAt = &interception.EndedAt.Time
}
if interception.Client.Valid {
intc.Client = &interception.Client.String
}
return intc
}
+229
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
@@ -206,3 +207,231 @@ func TestTemplateVersionParameter_BadDescription(t *testing.T) {
req.NoError(err)
req.NotEmpty(sdk.DescriptionPlaintext, "broke the markdown parser with %v", desc)
}
func TestAIBridgeInterception(t *testing.T) {
t.Parallel()
now := dbtime.Now()
interceptionID := uuid.New()
initiatorID := uuid.New()
cases := []struct {
name string
interception database.AIBridgeInterception
initiator database.VisibleUser
tokenUsages []database.AIBridgeTokenUsage
userPrompts []database.AIBridgeUserPrompt
toolUsages []database.AIBridgeToolUsage
expected codersdk.AIBridgeInterception
}{
{
name: "all_optional_values_set",
interception: database.AIBridgeInterception{
ID: interceptionID,
InitiatorID: initiatorID,
Provider: "anthropic",
Model: "claude-3-opus",
StartedAt: now,
Metadata: pqtype.NullRawMessage{
RawMessage: json.RawMessage(`{"key":"value"}`),
Valid: true,
},
EndedAt: sql.NullTime{
Time: now.Add(time.Minute),
Valid: true,
},
APIKeyID: sql.NullString{
String: "api-key-123",
Valid: true,
},
Client: sql.NullString{
String: "claude-code/1.0.0",
Valid: true,
},
},
initiator: database.VisibleUser{
ID: initiatorID,
Username: "testuser",
Name: "Test User",
AvatarURL: "https://example.com/avatar.png",
},
tokenUsages: []database.AIBridgeTokenUsage{
{
ID: uuid.New(),
InterceptionID: interceptionID,
ProviderResponseID: "resp-123",
InputTokens: 100,
OutputTokens: 200,
Metadata: pqtype.NullRawMessage{
RawMessage: json.RawMessage(`{"cache":"hit"}`),
Valid: true,
},
CreatedAt: now.Add(10 * time.Second),
},
},
userPrompts: []database.AIBridgeUserPrompt{
{
ID: uuid.New(),
InterceptionID: interceptionID,
ProviderResponseID: "resp-123",
Prompt: "Hello, world!",
Metadata: pqtype.NullRawMessage{
RawMessage: json.RawMessage(`{"role":"user"}`),
Valid: true,
},
CreatedAt: now.Add(5 * time.Second),
},
},
toolUsages: []database.AIBridgeToolUsage{
{
ID: uuid.New(),
InterceptionID: interceptionID,
ProviderResponseID: "resp-123",
ServerUrl: sql.NullString{
String: "https://mcp.example.com",
Valid: true,
},
Tool: "read_file",
Input: `{"path":"/tmp/test.txt"}`,
Injected: true,
InvocationError: sql.NullString{
String: "file not found",
Valid: true,
},
Metadata: pqtype.NullRawMessage{
RawMessage: json.RawMessage(`{"duration_ms":50}`),
Valid: true,
},
CreatedAt: now.Add(15 * time.Second),
},
},
expected: codersdk.AIBridgeInterception{
ID: interceptionID,
Initiator: codersdk.MinimalUser{
ID: initiatorID,
Username: "testuser",
Name: "Test User",
AvatarURL: "https://example.com/avatar.png",
},
Provider: "anthropic",
Model: "claude-3-opus",
Metadata: map[string]any{"key": "value"},
StartedAt: now,
},
},
{
name: "no_optional_values_set",
interception: database.AIBridgeInterception{
ID: interceptionID,
InitiatorID: initiatorID,
Provider: "openai",
Model: "gpt-4",
StartedAt: now,
Metadata: pqtype.NullRawMessage{Valid: false},
EndedAt: sql.NullTime{Valid: false},
APIKeyID: sql.NullString{Valid: false},
Client: sql.NullString{Valid: false},
},
initiator: database.VisibleUser{
ID: initiatorID,
Username: "minimaluser",
Name: "",
AvatarURL: "",
},
tokenUsages: nil,
userPrompts: nil,
toolUsages: nil,
expected: codersdk.AIBridgeInterception{
ID: interceptionID,
Initiator: codersdk.MinimalUser{
ID: initiatorID,
Username: "minimaluser",
Name: "",
AvatarURL: "",
},
Provider: "openai",
Model: "gpt-4",
Metadata: nil,
StartedAt: now,
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := db2sdk.AIBridgeInterception(
tc.interception,
tc.initiator,
tc.tokenUsages,
tc.userPrompts,
tc.toolUsages,
)
// Check basic fields.
require.Equal(t, tc.expected.ID, result.ID)
require.Equal(t, tc.expected.Initiator, result.Initiator)
require.Equal(t, tc.expected.Provider, result.Provider)
require.Equal(t, tc.expected.Model, result.Model)
require.Equal(t, tc.expected.StartedAt.UTC(), result.StartedAt.UTC())
require.Equal(t, tc.expected.Metadata, result.Metadata)
// Check optional pointer fields.
if tc.interception.APIKeyID.Valid {
require.NotNil(t, result.APIKeyID)
require.Equal(t, tc.interception.APIKeyID.String, *result.APIKeyID)
} else {
require.Nil(t, result.APIKeyID)
}
if tc.interception.EndedAt.Valid {
require.NotNil(t, result.EndedAt)
require.Equal(t, tc.interception.EndedAt.Time.UTC(), result.EndedAt.UTC())
} else {
require.Nil(t, result.EndedAt)
}
if tc.interception.Client.Valid {
require.NotNil(t, result.Client)
require.Equal(t, tc.interception.Client.String, *result.Client)
} else {
require.Nil(t, result.Client)
}
// Check slices.
require.Len(t, result.TokenUsages, len(tc.tokenUsages))
require.Len(t, result.UserPrompts, len(tc.userPrompts))
require.Len(t, result.ToolUsages, len(tc.toolUsages))
// Verify token usages are converted correctly.
for i, tu := range tc.tokenUsages {
require.Equal(t, tu.ID, result.TokenUsages[i].ID)
require.Equal(t, tu.InterceptionID, result.TokenUsages[i].InterceptionID)
require.Equal(t, tu.ProviderResponseID, result.TokenUsages[i].ProviderResponseID)
require.Equal(t, tu.InputTokens, result.TokenUsages[i].InputTokens)
require.Equal(t, tu.OutputTokens, result.TokenUsages[i].OutputTokens)
}
// Verify user prompts are converted correctly.
for i, up := range tc.userPrompts {
require.Equal(t, up.ID, result.UserPrompts[i].ID)
require.Equal(t, up.InterceptionID, result.UserPrompts[i].InterceptionID)
require.Equal(t, up.ProviderResponseID, result.UserPrompts[i].ProviderResponseID)
require.Equal(t, up.Prompt, result.UserPrompts[i].Prompt)
}
// Verify tool usages are converted correctly.
for i, toolUsage := range tc.toolUsages {
require.Equal(t, toolUsage.ID, result.ToolUsages[i].ID)
require.Equal(t, toolUsage.InterceptionID, result.ToolUsages[i].InterceptionID)
require.Equal(t, toolUsage.ProviderResponseID, result.ToolUsages[i].ProviderResponseID)
require.Equal(t, toolUsage.ServerUrl.String, result.ToolUsages[i].ServerURL)
require.Equal(t, toolUsage.Tool, result.ToolUsages[i].Tool)
require.Equal(t, toolUsage.Input, result.ToolUsages[i].Input)
require.Equal(t, toolUsage.Injected, result.ToolUsages[i].Injected)
require.Equal(t, toolUsage.InvocationError.String, result.ToolUsages[i].InvocationError)
}
})
}
}
+3 -5
View File
@@ -19,7 +19,6 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/apikey"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
@@ -30,7 +29,6 @@ import (
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/rbac/rolestore"
"github.com/coder/coder/v2/coderd/taskname"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/provisionerd/proto"
@@ -1592,6 +1590,7 @@ func AIBridgeInterception(t testing.TB, db database.Store, seed database.InsertA
Model: takeFirst(seed.Model, "model"),
Metadata: takeFirstSlice(seed.Metadata, json.RawMessage("{}")),
StartedAt: takeFirst(seed.StartedAt, dbtime.Now()),
Client: seed.Client,
})
if endedAt != nil {
interception, err = db.UpdateAIBridgeInterceptionEnded(genCtx, database.UpdateAIBridgeInterceptionEndedParams{
@@ -1664,13 +1663,12 @@ func Task(t testing.TB, db database.Store, orig database.TaskTable) database.Tas
parameters = json.RawMessage([]byte("{}"))
}
taskName := taskname.Generate(genCtx, slog.Make(), orig.Prompt)
task, err := db.InsertTask(genCtx, database.InsertTaskParams{
ID: takeFirst(orig.ID, uuid.New()),
OrganizationID: orig.OrganizationID,
OwnerID: orig.OwnerID,
Name: takeFirst(orig.Name, taskName.Name),
DisplayName: takeFirst(orig.DisplayName, taskName.DisplayName),
Name: takeFirst(orig.Name, testutil.GetRandomNameHyphenated(t)),
DisplayName: takeFirst(orig.DisplayName, testutil.GetRandomNameHyphenated(t)),
WorkspaceID: orig.WorkspaceID,
TemplateVersionID: orig.TemplateVersionID,
TemplateParameters: parameters,
+7 -2
View File
@@ -1023,7 +1023,8 @@ CREATE TABLE aibridge_interceptions (
started_at timestamp with time zone NOT NULL,
metadata jsonb,
ended_at timestamp with time zone,
api_key_id text
api_key_id text,
client character varying(64) DEFAULT 'Unknown'::character varying
);
COMMENT ON TABLE aibridge_interceptions IS 'Audit log of requests intercepted by AI Bridge';
@@ -2736,7 +2737,9 @@ CREATE TABLE workspaces (
favorite boolean DEFAULT false NOT NULL,
next_start_at timestamp with time zone,
group_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
user_acl jsonb DEFAULT '{}'::jsonb NOT NULL
user_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
CONSTRAINT group_acl_is_object CHECK ((jsonb_typeof(group_acl) = 'object'::text)),
CONSTRAINT user_acl_is_object CHECK ((jsonb_typeof(user_acl) = 'object'::text))
);
COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.';
@@ -3272,6 +3275,8 @@ CREATE INDEX idx_agent_stats_created_at ON workspace_agent_stats USING btree (cr
CREATE INDEX idx_agent_stats_user_id ON workspace_agent_stats USING btree (user_id);
CREATE INDEX idx_aibridge_interceptions_client ON aibridge_interceptions USING btree (client);
CREATE INDEX idx_aibridge_interceptions_initiator_id ON aibridge_interceptions USING btree (initiator_id);
CREATE INDEX idx_aibridge_interceptions_model ON aibridge_interceptions USING btree (model);
@@ -0,0 +1,3 @@
ALTER TABLE workspaces
DROP CONSTRAINT IF EXISTS group_acl_is_object,
DROP CONSTRAINT IF EXISTS user_acl_is_object;
@@ -0,0 +1,9 @@
-- Add constraints that reject 'null'::jsonb for group and user ACLs
-- because they would break the new workspace_expanded view.
UPDATE workspaces SET group_acl = '{}'::jsonb WHERE group_acl = 'null'::jsonb;
UPDATE workspaces SET user_acl = '{}'::jsonb WHERE user_acl = 'null'::jsonb;
ALTER TABLE workspaces
ADD CONSTRAINT group_acl_is_object CHECK (jsonb_typeof(group_acl) = 'object'),
ADD CONSTRAINT user_acl_is_object CHECK (jsonb_typeof(user_acl) = 'object');
@@ -0,0 +1,2 @@
ALTER TABLE aibridge_interceptions
DROP COLUMN client;
@@ -0,0 +1,5 @@
ALTER TABLE aibridge_interceptions
ADD COLUMN client VARCHAR(64)
DEFAULT 'Unknown';
CREATE INDEX idx_aibridge_interceptions_client ON aibridge_interceptions (client);
@@ -0,0 +1,35 @@
-- Fixture for migration 000417_workspace_acl_object_constraint.
-- Inserts a workspace with 'null'::json ACLs to ensure the migration
-- correctly normalizes such values.
INSERT INTO workspaces (
id,
created_at,
updated_at,
owner_id,
organization_id,
template_id,
deleted,
name,
last_used_at,
automatic_updates,
favorite,
group_acl,
user_acl
)
VALUES (
'6f6fdbee-4c18-4a5c-8a8d-9b811c9f0a28',
'2024-02-10 00:00:00+00',
'2024-02-10 00:00:00+00',
'30095c71-380b-457a-8995-97b8ee6e5307',
'bb640d07-ca8a-4869-b6bc-ae61ebb2fda1',
'4cc1f466-f326-477e-8762-9d0c6781fc56',
false,
'acl-null-workspace',
'0001-01-01 00:00:00+00',
'never',
false,
'null'::jsonb,
'null'::jsonb
)
ON CONFLICT DO NOTHING;
+3
View File
@@ -790,6 +790,7 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, ar
arg.InitiatorID,
arg.Provider,
arg.Model,
arg.Client,
arg.AfterID,
arg.Offset,
arg.Limit,
@@ -810,6 +811,7 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, ar
&i.AIBridgeInterception.Metadata,
&i.AIBridgeInterception.EndedAt,
&i.AIBridgeInterception.APIKeyID,
&i.AIBridgeInterception.Client,
&i.VisibleUser.ID,
&i.VisibleUser.Username,
&i.VisibleUser.Name,
@@ -847,6 +849,7 @@ func (q *sqlQuerier) CountAuthorizedAIBridgeInterceptions(ctx context.Context, a
arg.InitiatorID,
arg.Provider,
arg.Model,
arg.Client,
)
if err != nil {
return 0, err
+1
View File
@@ -3642,6 +3642,7 @@ type AIBridgeInterception struct {
Metadata pqtype.NullRawMessage `db:"metadata" json:"metadata"`
EndedAt sql.NullTime `db:"ended_at" json:"ended_at"`
APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"`
Client sql.NullString `db:"client" json:"client"`
}
// Audit log of tokens used by intercepted requests in AI Bridge
+62
View File
@@ -6765,6 +6765,65 @@ func TestWorkspaceBuildDeadlineConstraint(t *testing.T) {
}
}
func TestWorkspaceACLObjectConstraint(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
template := dbgen.Template(t, db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
TemplateID: template.ID,
Deleted: false,
})
t.Run("GroupACLNull", func(t *testing.T) {
t.Parallel()
var nilACL database.WorkspaceACL
ctx := testutil.Context(t, testutil.WaitLong)
err := db.UpdateWorkspaceACLByID(ctx, database.UpdateWorkspaceACLByIDParams{
ID: workspace.ID,
GroupACL: nilACL,
UserACL: database.WorkspaceACL{},
})
require.Error(t, err)
require.True(t, database.IsCheckViolation(err, database.CheckGroupAclIsObject))
})
t.Run("UserACLNull", func(t *testing.T) {
t.Parallel()
var nilACL database.WorkspaceACL
ctx := testutil.Context(t, testutil.WaitLong)
err := db.UpdateWorkspaceACLByID(ctx, database.UpdateWorkspaceACLByIDParams{
ID: workspace.ID,
GroupACL: database.WorkspaceACL{},
UserACL: nilACL,
})
require.Error(t, err)
require.True(t, database.IsCheckViolation(err, database.CheckUserAclIsObject))
})
t.Run("ValidEmptyObjects", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
err := db.UpdateWorkspaceACLByID(ctx, database.UpdateWorkspaceACLByIDParams{
ID: workspace.ID,
GroupACL: database.WorkspaceACL{},
UserACL: database.WorkspaceACL{},
})
require.NoError(t, err)
})
}
// TestGetLatestWorkspaceBuildsByWorkspaceIDs populates the database with
// workspaces and builds. It then tests that
// GetLatestWorkspaceBuildsByWorkspaceIDs returns the latest build for some
@@ -8011,12 +8070,15 @@ func TestUpdateAIBridgeInterceptionEnded(t *testing.T) {
ID: uid,
InitiatorID: user.ID,
Metadata: json.RawMessage("{}"),
Client: sql.NullString{String: "client", Valid: true},
}
intc, err := db.InsertAIBridgeInterception(ctx, insertParams)
require.NoError(t, err)
require.Equal(t, uid, intc.ID)
require.False(t, intc.EndedAt.Valid)
require.True(t, intc.Client.Valid)
require.Equal(t, "client", intc.Client.String)
interceptions = append(interceptions, intc)
}
+35 -16
View File
@@ -123,8 +123,7 @@ WITH interceptions_in_range AS (
WHERE
provider = $1::text
AND model = $2::text
-- TODO: use the client value once we have it (see https://github.com/coder/aibridge/issues/31)
AND 'unknown' = $3::text
AND COALESCE(client, 'Unknown') = $3::text
AND ended_at IS NOT NULL -- incomplete interceptions are not included in summaries
AND ended_at >= $4::timestamptz
AND ended_at < $5::timestamptz
@@ -301,6 +300,11 @@ WHERE
WHEN $5::text != '' THEN aibridge_interceptions.model = $5::text
ELSE true
END
-- Filter client
AND CASE
WHEN $6::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $6::text
ELSE true
END
-- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeInterceptions
-- @authorize_filter
`
@@ -311,6 +315,7 @@ type CountAIBridgeInterceptionsParams struct {
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
Client string `db:"client" json:"client"`
}
func (q *sqlQuerier) CountAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams) (int64, error) {
@@ -320,6 +325,7 @@ func (q *sqlQuerier) CountAIBridgeInterceptions(ctx context.Context, arg CountAI
arg.InitiatorID,
arg.Provider,
arg.Model,
arg.Client,
)
var count int64
err := row.Scan(&count)
@@ -372,7 +378,7 @@ func (q *sqlQuerier) DeleteOldAIBridgeRecords(ctx context.Context, beforeTime ti
const getAIBridgeInterceptionByID = `-- name: GetAIBridgeInterceptionByID :one
SELECT
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client
FROM
aibridge_interceptions
WHERE
@@ -391,13 +397,14 @@ func (q *sqlQuerier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UU
&i.Metadata,
&i.EndedAt,
&i.APIKeyID,
&i.Client,
)
return i, err
}
const getAIBridgeInterceptions = `-- name: GetAIBridgeInterceptions :many
SELECT
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id
id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client
FROM
aibridge_interceptions
`
@@ -420,6 +427,7 @@ func (q *sqlQuerier) GetAIBridgeInterceptions(ctx context.Context) ([]AIBridgeIn
&i.Metadata,
&i.EndedAt,
&i.APIKeyID,
&i.Client,
); err != nil {
return nil, err
}
@@ -565,11 +573,11 @@ func (q *sqlQuerier) GetAIBridgeUserPromptsByInterceptionID(ctx context.Context,
const insertAIBridgeInterception = `-- name: InsertAIBridgeInterception :one
INSERT INTO aibridge_interceptions (
id, api_key_id, initiator_id, provider, model, metadata, started_at
id, api_key_id, initiator_id, provider, model, metadata, started_at, client
) VALUES (
$1, $2, $3, $4, $5, COALESCE($6::jsonb, '{}'::jsonb), $7
$1, $2, $3, $4, $5, COALESCE($6::jsonb, '{}'::jsonb), $7, $8
)
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client
`
type InsertAIBridgeInterceptionParams struct {
@@ -580,6 +588,7 @@ type InsertAIBridgeInterceptionParams struct {
Model string `db:"model" json:"model"`
Metadata json.RawMessage `db:"metadata" json:"metadata"`
StartedAt time.Time `db:"started_at" json:"started_at"`
Client sql.NullString `db:"client" json:"client"`
}
func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error) {
@@ -591,6 +600,7 @@ func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertA
arg.Model,
arg.Metadata,
arg.StartedAt,
arg.Client,
)
var i AIBridgeInterception
err := row.Scan(
@@ -602,6 +612,7 @@ func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertA
&i.Metadata,
&i.EndedAt,
&i.APIKeyID,
&i.Client,
)
return i, err
}
@@ -740,7 +751,7 @@ func (q *sqlQuerier) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIB
const listAIBridgeInterceptions = `-- name: ListAIBridgeInterceptions :many
SELECT
aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, aibridge_interceptions.ended_at, aibridge_interceptions.api_key_id,
aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, aibridge_interceptions.ended_at, aibridge_interceptions.api_key_id, aibridge_interceptions.client,
visible_users.id, visible_users.username, visible_users.name, visible_users.avatar_url
FROM
aibridge_interceptions
@@ -773,9 +784,14 @@ WHERE
WHEN $5::text != '' THEN aibridge_interceptions.model = $5::text
ELSE true
END
-- Filter client
AND CASE
WHEN $6::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $6::text
ELSE true
END
-- Cursor pagination
AND CASE
WHEN $6::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
WHEN $7::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
-- The pagination cursor is the last ID of the previous page.
-- The query is ordered by the started_at field, so select all
-- rows before the cursor and before the after_id UUID.
@@ -783,8 +799,8 @@ WHERE
-- "after_id" terminology comes from our pagination parser in
-- coderd.
(aibridge_interceptions.started_at, aibridge_interceptions.id) < (
(SELECT started_at FROM aibridge_interceptions WHERE id = $6),
$6::uuid
(SELECT started_at FROM aibridge_interceptions WHERE id = $7),
$7::uuid
)
)
ELSE true
@@ -794,8 +810,8 @@ WHERE
ORDER BY
aibridge_interceptions.started_at DESC,
aibridge_interceptions.id DESC
LIMIT COALESCE(NULLIF($8::integer, 0), 100)
OFFSET $7
LIMIT COALESCE(NULLIF($9::integer, 0), 100)
OFFSET $8
`
type ListAIBridgeInterceptionsParams struct {
@@ -804,6 +820,7 @@ type ListAIBridgeInterceptionsParams struct {
InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
Client string `db:"client" json:"client"`
AfterID uuid.UUID `db:"after_id" json:"after_id"`
Offset int32 `db:"offset_" json:"offset_"`
Limit int32 `db:"limit_" json:"limit_"`
@@ -821,6 +838,7 @@ func (q *sqlQuerier) ListAIBridgeInterceptions(ctx context.Context, arg ListAIBr
arg.InitiatorID,
arg.Provider,
arg.Model,
arg.Client,
arg.AfterID,
arg.Offset,
arg.Limit,
@@ -841,6 +859,7 @@ func (q *sqlQuerier) ListAIBridgeInterceptions(ctx context.Context, arg ListAIBr
&i.AIBridgeInterception.Metadata,
&i.AIBridgeInterception.EndedAt,
&i.AIBridgeInterception.APIKeyID,
&i.AIBridgeInterception.Client,
&i.VisibleUser.ID,
&i.VisibleUser.Username,
&i.VisibleUser.Name,
@@ -864,8 +883,7 @@ SELECT
DISTINCT ON (provider, model, client)
provider,
model,
-- TODO: use the client value once we have it (see https://github.com/coder/aibridge/issues/31)
'unknown' AS client
COALESCE(client, 'Unknown') AS client
FROM
aibridge_interceptions
WHERE
@@ -1047,7 +1065,7 @@ UPDATE aibridge_interceptions
WHERE
id = $2::uuid
AND ended_at IS NULL
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client
`
type UpdateAIBridgeInterceptionEndedParams struct {
@@ -1067,6 +1085,7 @@ func (q *sqlQuerier) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg Up
&i.Metadata,
&i.EndedAt,
&i.APIKeyID,
&i.Client,
)
return i, err
}
+14 -6
View File
@@ -1,8 +1,8 @@
-- name: InsertAIBridgeInterception :one
INSERT INTO aibridge_interceptions (
id, api_key_id, initiator_id, provider, model, metadata, started_at
id, api_key_id, initiator_id, provider, model, metadata, started_at, client
) VALUES (
@id, @api_key_id, @initiator_id, @provider, @model, COALESCE(@metadata::jsonb, '{}'::jsonb), @started_at
@id, @api_key_id, @initiator_id, @provider, @model, COALESCE(@metadata::jsonb, '{}'::jsonb), @started_at, @client
)
RETURNING *;
@@ -115,6 +115,11 @@ WHERE
WHEN @model::text != '' THEN aibridge_interceptions.model = @model::text
ELSE true
END
-- Filter client
AND CASE
WHEN @client::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = @client::text
ELSE true
END
-- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeInterceptions
-- @authorize_filter
;
@@ -154,6 +159,11 @@ WHERE
WHEN @model::text != '' THEN aibridge_interceptions.model = @model::text
ELSE true
END
-- Filter client
AND CASE
WHEN @client::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = @client::text
ELSE true
END
-- Cursor pagination
AND CASE
WHEN @after_id::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
@@ -219,8 +229,7 @@ SELECT
DISTINCT ON (provider, model, client)
provider,
model,
-- TODO: use the client value once we have it (see https://github.com/coder/aibridge/issues/31)
'unknown' AS client
COALESCE(client, 'Unknown') AS client
FROM
aibridge_interceptions
WHERE
@@ -242,8 +251,7 @@ WITH interceptions_in_range AS (
WHERE
provider = @provider::text
AND model = @model::text
-- TODO: use the client value once we have it (see https://github.com/coder/aibridge/issues/31)
AND 'unknown' = @client::text
AND COALESCE(client, 'Unknown') = @client::text
AND ended_at IS NOT NULL -- incomplete interceptions are not included in summaries
AND ended_at >= @ended_at_after::timestamptz
AND ended_at < @ended_at_before::timestamptz
+8
View File
@@ -106,6 +106,10 @@ func ExtractUserContext(ctx context.Context, db database.Store, rw http.Response
if userID, err := uuid.Parse(userQuery); err == nil {
user, err = db.GetUserByID(ctx, userID)
if err != nil {
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return database.User{}, false
}
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: userErrorMessage,
Detail: fmt.Sprintf("queried user=%q", userQuery),
@@ -120,6 +124,10 @@ func ExtractUserContext(ctx context.Context, db database.Store, rw http.Response
Username: userQuery,
})
if err != nil {
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return database.User{}, false
}
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: userErrorMessage,
Detail: fmt.Sprintf("queried user=%q", userQuery),
+47 -1
View File
@@ -71,7 +71,53 @@ func TestUserParam(t *testing.T) {
})).ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)
// User "ben" doesn't exist, so expect 404.
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("NotFoundByUsername", func(t *testing.T) {
t.Parallel()
db, rw, r := setup(t)
httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
DB: db,
RedirectToLogin: false,
})(http.HandlerFunc(func(rw http.ResponseWriter, returnedRequest *http.Request) {
r = returnedRequest
})).ServeHTTP(rw, r)
routeContext := chi.NewRouteContext()
routeContext.URLParams.Add("user", "nonexistent-user")
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
httpmw.ExtractUserParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
})).ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("NotFoundByUUID", func(t *testing.T) {
t.Parallel()
db, rw, r := setup(t)
httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
DB: db,
RedirectToLogin: false,
})(http.HandlerFunc(func(rw http.ResponseWriter, returnedRequest *http.Request) {
r = returnedRequest
})).ServeHTTP(rw, r)
routeContext := chi.NewRouteContext()
// Use a valid UUID that doesn't exist in the database.
routeContext.URLParams.Add("user", "88888888-4444-4444-4444-121212121212")
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
httpmw.ExtractUserParam(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
})).ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("me", func(t *testing.T) {
@@ -262,8 +262,6 @@ func TestWebhookDispatch(t *testing.T) {
// This is not strictly necessary for this test, but it's testing some side logic which is too small for its own test.
require.Equal(t, payload.Payload.UserName, name)
require.Equal(t, payload.Payload.UserUsername, username)
// Right now we don't have a way to query notification templates by ID in dbmem, and it's not necessary to add this
// just to satisfy this test. We can safely assume that as long as this value is not empty that the given value was delivered.
require.NotEmpty(t, payload.Payload.NotificationName)
}
+1 -1
View File
@@ -150,7 +150,7 @@ func TestNotificationPreferences(t *testing.T) {
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
// NOTE: ExtractUserParam gets in the way here, and returns a 400 Bad Request instead of a 403 Forbidden.
// This is not ideal, and we should probably change this behavior.
require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
require.Equal(t, http.StatusNotFound, sdkError.StatusCode())
})
t.Run("Admin may read any users' preferences", func(t *testing.T) {
+41 -1
View File
@@ -13,6 +13,7 @@ type Metrics struct {
logger slog.Logger
workspaceCreationTimings *prometheus.HistogramVec
workspaceClaimTimings *prometheus.HistogramVec
jobQueueWait *prometheus.HistogramVec
}
type WorkspaceTimingType int
@@ -29,6 +30,12 @@ const (
workspaceTypePrebuild = "prebuild"
)
// BuildReasonPrebuild is the build_reason metric label value for prebuild
// operations. This is distinct from database.BuildReason values since prebuilds
// use BuildReasonInitiator in the database but we want to track them separately
// in metrics. This is also used as a label value by the metrics in wsbuilder.
const BuildReasonPrebuild = workspaceTypePrebuild
type WorkspaceTimingFlags struct {
IsPrebuild bool
IsClaim bool
@@ -90,6 +97,30 @@ func NewMetrics(logger slog.Logger) *Metrics {
NativeHistogramZeroThreshold: 0,
NativeHistogramMaxZeroThreshold: 0,
}, []string{"organization_name", "template_name", "preset_name"}),
jobQueueWait: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "coderd",
Name: "provisioner_job_queue_wait_seconds",
Help: "Time from job creation to acquisition by a provisioner daemon.",
Buckets: []float64{
0.1, // 100ms
0.5, // 500ms
1, // 1s
5, // 5s
10, // 10s
30, // 30s
60, // 1m
120, // 2m
300, // 5m
600, // 10m
900, // 15m
1800, // 30m
},
NativeHistogramBucketFactor: 1.1,
NativeHistogramMaxBucketNumber: 100,
NativeHistogramMinResetDuration: time.Hour,
NativeHistogramZeroThreshold: 0,
NativeHistogramMaxZeroThreshold: 0,
}, []string{"provisioner_type", "job_type", "transition", "build_reason"}),
}
}
@@ -97,7 +128,10 @@ func (m *Metrics) Register(reg prometheus.Registerer) error {
if err := reg.Register(m.workspaceCreationTimings); err != nil {
return err
}
return reg.Register(m.workspaceClaimTimings)
if err := reg.Register(m.workspaceClaimTimings); err != nil {
return err
}
return reg.Register(m.jobQueueWait)
}
// IsTrackable returns true if the workspace build should be tracked in metrics.
@@ -162,3 +196,9 @@ func (m *Metrics) UpdateWorkspaceTimingsMetrics(
// Not a trackable build type (e.g. restart, stop, subsequent builds)
}
}
// ObserveJobQueueWait records the time a provisioner job spent waiting in the queue.
// For non-workspace-build jobs, transition and buildReason should be empty strings.
func (m *Metrics) ObserveJobQueueWait(provisionerType, jobType, transition, buildReason string, waitSeconds float64) {
m.jobQueueWait.WithLabelValues(provisionerType, jobType, transition, buildReason).Observe(waitSeconds)
}
@@ -478,6 +478,10 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo
TraceMetadata: jobTraceMetadata,
}
// jobTransition and jobBuildReason are used for metrics; only set for workspace builds.
var jobTransition string
var jobBuildReason string
switch job.Type {
case database.ProvisionerJobTypeWorkspaceBuild:
var input WorkspaceProvisionJob
@@ -584,6 +588,15 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo
if err != nil {
return nil, failJob(fmt.Sprintf("convert workspace transition: %s", err))
}
jobTransition = string(workspaceBuild.Transition)
// Prebuilds use BuildReasonInitiator in the database but we want to
// track them separately in metrics. Check the initiator ID to detect
// prebuild jobs.
if job.InitiatorID == database.PrebuildsSystemUserID {
jobBuildReason = BuildReasonPrebuild
} else {
jobBuildReason = string(workspaceBuild.Reason)
}
// A previous workspace build exists
var lastWorkspaceBuildParameters []database.WorkspaceBuildParameter
@@ -825,6 +838,12 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo
return nil, failJob(fmt.Sprintf("payload was too big: %d > %d", protobuf.Size(protoJob), drpcsdk.MaxMessageSize))
}
// Record the time the job spent waiting in the queue.
if s.metrics != nil && job.StartedAt.Valid && job.Provisioner.Valid() {
queueWaitSeconds := job.StartedAt.Time.Sub(job.CreatedAt).Seconds()
s.metrics.ObserveJobQueueWait(string(job.Provisioner), string(job.Type), jobTransition, jobBuildReason, queueWaitSeconds)
}
return protoJob, err
}
+1
View File
@@ -385,6 +385,7 @@ func AIBridgeInterceptions(ctx context.Context, db database.Store, query string,
filter.InitiatorID = parseUser(ctx, db, parser, values, "initiator", actorID)
filter.Provider = parser.String(values, "", "provider")
filter.Model = parser.String(values, "", "model")
filter.Client = parser.String(values, "", "client")
// Time must be between started_after and started_before.
filter.StartedAfter = parser.Time3339Nano(values, time.Time{}, "started_after")
+2 -2
View File
@@ -376,7 +376,7 @@ func TestTelemetry(t *testing.T) {
require.Equal(t, snapshot1.Provider, aiBridgeInterception1.Provider)
require.Equal(t, snapshot1.Model, aiBridgeInterception1.Model)
require.Equal(t, snapshot1.Client, "unknown") // no client info yet
require.Equal(t, snapshot1.Client, "Unknown") // no client info yet
require.EqualValues(t, snapshot1.InterceptionCount, 2)
require.EqualValues(t, snapshot1.InterceptionsByRoute, map[string]int64{}) // no route info yet
require.EqualValues(t, snapshot1.InterceptionDurationMillis.P50, 90_000)
@@ -396,7 +396,7 @@ func TestTelemetry(t *testing.T) {
require.Equal(t, snapshot2.Provider, aiBridgeInterception3.Provider)
require.Equal(t, snapshot2.Model, aiBridgeInterception3.Model)
require.Equal(t, snapshot2.Client, "unknown") // no client info yet
require.Equal(t, snapshot2.Client, "Unknown") // no client info yet
require.EqualValues(t, snapshot2.InterceptionCount, 1)
require.EqualValues(t, snapshot2.InterceptionsByRoute, map[string]int64{}) // no route info yet
require.EqualValues(t, snapshot2.InterceptionDurationMillis.P50, 180_000)
+1
View File
@@ -1131,6 +1131,7 @@ func (api *API) convertTemplate(
RequireActiveVersion: templateAccessControl.RequireActiveVersion,
Deprecated: templateAccessControl.IsDeprecated(),
DeprecationMessage: templateAccessControl.Deprecated,
Deleted: template.Deleted,
MaxPortShareLevel: maxPortShareLevel,
UseClassicParameterFlow: template.UseClassicParameterFlow,
CORSBehavior: codersdk.CORSBehavior(template.CorsBehavior),
+43
View File
@@ -1801,6 +1801,49 @@ func TestDeleteTemplate(t *testing.T) {
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
})
t.Run("DeletedIsSet", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
ctx := testutil.Context(t, testutil.WaitLong)
// Verify the deleted field is exposed in the SDK and set to false for active templates
got, err := client.Template(ctx, template.ID)
require.NoError(t, err)
require.False(t, got.Deleted)
})
t.Run("DeletedIsTrue", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
ctx := testutil.Context(t, testutil.WaitLong)
err := client.DeleteTemplate(ctx, template.ID)
require.NoError(t, err)
// Verify the deleted field is set to true by listing templates with
// deleted:true filter.
templates, err := client.Templates(ctx, codersdk.TemplateFilter{
OrganizationID: user.OrganizationID,
SearchQuery: "deleted:true",
})
require.NoError(t, err)
require.Len(t, templates, 1)
require.Equal(t, template.ID, templates[0].ID)
require.True(t, templates[0].Deleted)
})
}
func TestTemplateMetrics(t *testing.T) {
+3 -3
View File
@@ -349,7 +349,7 @@ func TestDeleteUser(t *testing.T) {
err := client.DeleteUser(context.Background(), firstUser.UserID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("HasWorkspaces", func(t *testing.T) {
t.Parallel()
@@ -1010,7 +1010,7 @@ func TestUpdateUserProfile(t *testing.T) {
require.ErrorAs(t, err, &apiErr)
// Right now, we are raising a BAD request error because we don't support a
// user accessing other users info
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("ConflictingUsername", func(t *testing.T) {
@@ -2602,7 +2602,7 @@ func TestUserAutofillParameters(t *testing.T) {
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
// u1 should be able to read u2's parameters as u1 is site admin.
_, err = client1.UserAutofillParameters(
+17 -14
View File
@@ -68,27 +68,30 @@ func SubdomainAppSessionTokenCookie(hostname string) string {
// the wrong value.
//
// We use different cookie names for:
// - path apps on primary access URL: coder_session_token
// - path apps on proxies: coder_path_app_session_token
// - path apps: coder_path_app_session_token
// - subdomain apps: coder_subdomain_app_session_token_{unique_hash}
//
// First we try the default function to get a token from request, which supports
// query parameters, the Coder-Session-Token header and the coder_session_token
// cookie.
//
// Then we try the specific cookie name for the access method.
// We prefer the access-method-specific cookie first, then fall back to standard
// Coder token extraction (query parameters, Coder-Session-Token header, etc.).
func (c AppCookies) TokenFromRequest(r *http.Request, accessMethod AccessMethod) string {
// Try the default function first.
token := httpmw.APITokenFromRequest(r)
if token != "" {
return token
}
// Then try the specific cookie name for the access method.
// Prefer the access-method-specific cookie first.
//
// Workspace app requests commonly include an `Authorization` header intended
// for the upstream app (e.g. API calls). `httpmw.APITokenFromRequest` supports
// RFC 6750 bearer tokens, so if we consult it first we'd incorrectly treat
// that upstream header as a Coder session token and ignore the app session
// cookie, breaking token renewal for subdomain apps.
cookie, err := r.Cookie(c.CookieNameForAccessMethod(accessMethod))
if err == nil && cookie.Value != "" {
return cookie.Value
}
// Fall back to standard Coder token extraction (session cookie, query param,
// Coder-Session-Token header, and then Authorization: Bearer).
token := httpmw.APITokenFromRequest(r)
if token != "" {
return token
}
return ""
}
+18
View File
@@ -1,6 +1,8 @@
package workspaceapps_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
@@ -32,3 +34,19 @@ func TestAppCookies(t *testing.T) {
newCookies := workspaceapps.NewAppCookies("different.com")
require.NotEqual(t, cookies.SubdomainAppSessionToken, newCookies.SubdomainAppSessionToken)
}
func TestAppCookies_TokenFromRequest_PrefersAppCookieOverAuthorizationBearer(t *testing.T) {
t.Parallel()
cookies := workspaceapps.NewAppCookies("apps.example.com")
req := httptest.NewRequest("GET", "https://8081--agent--workspace--user.apps.example.com/", nil)
req.Header.Set("Authorization", "Bearer whatever")
req.AddCookie(&http.Cookie{
Name: cookies.CookieNameForAccessMethod(workspaceapps.AccessMethodSubdomain),
Value: "subdomain-session-token",
})
got := cookies.TokenFromRequest(req, workspaceapps.AccessMethodSubdomain)
require.Equal(t, "subdomain-session-token", got)
}
+2 -1
View File
@@ -382,7 +382,8 @@ func (api *API) postWorkspaceBuildsInternal(
LogLevel(string(createBuild.LogLevel)).
DeploymentValues(api.Options.DeploymentValues).
Experiments(api.Experiments).
TemplateVersionPresetID(createBuild.TemplateVersionPresetID)
TemplateVersionPresetID(createBuild.TemplateVersionPresetID).
BuildMetrics(api.WorkspaceBuilderMetrics)
if (transition == database.WorkspaceTransitionStart || transition == database.WorkspaceTransitionStop) && createBuild.Reason != "" {
builder = builder.Reason(database.BuildReason(createBuild.Reason))
+2 -1
View File
@@ -787,7 +787,8 @@ func createWorkspace(
ActiveVersion().
Experiments(api.Experiments).
DeploymentValues(api.DeploymentValues).
RichParameterValues(req.RichParameterValues)
RichParameterValues(req.RichParameterValues).
BuildMetrics(api.WorkspaceBuilderMetrics)
if req.TemplateVersionID != uuid.Nil {
builder = builder.VersionID(req.TemplateVersionID)
}
+137
View File
@@ -14,6 +14,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -21,7 +22,9 @@ import (
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/autobuild"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/coderdtest/promhelp"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbfake"
@@ -30,6 +33,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/render"
@@ -37,6 +41,7 @@ import (
"github.com/coder/coder/v2/coderd/schedule/cron"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/coderd/wsbuilder"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/provisioner/echo"
@@ -5901,3 +5906,135 @@ func TestWorkspaceCreateWithImplicitPreset(t *testing.T) {
require.Equal(t, preset2ID, *ws2.LatestBuild.TemplateVersionPresetID)
})
}
func TestProvisionerJobQueueWaitMetric(t *testing.T) {
t.Parallel()
logger := testutil.Logger(t)
reg := prometheus.NewRegistry()
metrics := provisionerdserver.NewMetrics(logger)
err := metrics.Register(reg)
require.NoError(t, err)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
ProvisionerdServerMetrics: metrics,
})
user := coderdtest.CreateFirstUser(t, client)
// Create a template version - this triggers a template_version_import job.
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
// Check that the queue wait metric was recorded for the template_version_import job.
importMetric := promhelp.MetricValue(t, reg, "coderd_provisioner_job_queue_wait_seconds", prometheus.Labels{
"provisioner_type": string(database.ProvisionerTypeEcho),
"job_type": string(database.ProvisionerJobTypeTemplateVersionImport),
"transition": "",
"build_reason": "",
})
require.NotNil(t, importMetric, "import job metric should be recorded")
importHistogram := importMetric.GetHistogram()
require.NotNil(t, importHistogram)
require.Equal(t, uint64(1), importHistogram.GetSampleCount(), "import job should have 1 sample")
require.Greater(t, importHistogram.GetSampleSum(), 0.0, "import job queue wait should be non-zero")
// Create a template and workspace - this triggers a workspace_build job.
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Check that the queue wait metric was recorded for the workspace_build job.
buildMetric := promhelp.MetricValue(t, reg, "coderd_provisioner_job_queue_wait_seconds", prometheus.Labels{
"provisioner_type": string(database.ProvisionerTypeEcho),
"job_type": string(database.ProvisionerJobTypeWorkspaceBuild),
"transition": string(database.WorkspaceTransitionStart),
"build_reason": string(database.BuildReasonInitiator),
})
require.NotNil(t, buildMetric, "workspace build job metric should be recorded")
buildHistogram := buildMetric.GetHistogram()
require.NotNil(t, buildHistogram)
require.Equal(t, uint64(1), buildHistogram.GetSampleCount(), "workspace build job should have 1 sample")
require.Greater(t, buildHistogram.GetSampleSum(), 0.0, "workspace build job queue wait should be non-zero")
}
func TestWorkspaceBuildsEnqueuedMetric(t *testing.T) {
t.Parallel()
var (
logger = testutil.Logger(t)
reg = prometheus.NewRegistry()
metrics = provisionerdserver.NewMetrics(logger)
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
tickCh = make(chan time.Time)
statsCh = make(chan autobuild.Stats)
)
err := metrics.Register(reg)
require.NoError(t, err)
wsBuilderMetrics, err := wsbuilder.NewMetrics(reg)
require.NoError(t, err)
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
ProvisionerdServerMetrics: metrics,
WorkspaceBuilderMetrics: wsBuilderMetrics,
AutobuildTicker: tickCh,
AutobuildStats: statsCh,
})
user := coderdtest.CreateFirstUser(t, client)
// Create a template and workspace with autostart schedule.
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = ptr.Ref(sched.String())
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Stop the workspace to prepare for autostart.
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Trigger an autostart build via the autobuild ticker. This verifies that
// autostart builds are recorded with build_reason="autostart".
p, err := coderdtest.GetProvisionerForTags(db, time.Now(), workspace.OrganizationID, map[string]string{})
require.NoError(t, err)
go func() {
tickTime := sched.Next(workspace.LatestBuild.CreatedAt)
coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime)
tickCh <- tickTime
close(tickCh)
}()
// Wait for the autostart to complete.
stats := <-statsCh
require.Len(t, stats.Errors, 0)
require.Len(t, stats.Transitions, 1)
require.Contains(t, stats.Transitions, workspace.ID)
require.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
// Verify the workspace was autostarted.
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.BuildReasonAutostart, workspace.LatestBuild.Reason)
// Now check the autostart metric was recorded.
autostartCount := promhelp.CounterValue(t, reg, "coderd_workspace_builds_enqueued_total", prometheus.Labels{
"provisioner_type": string(database.ProvisionerTypeEcho),
"build_reason": string(database.BuildReasonAutostart),
"transition": string(database.WorkspaceTransitionStart),
"status": wsbuilder.BuildStatusSuccess,
})
require.Equal(t, 1, autostartCount, "autostart should record 1 enqueue with build_reason=autostart")
}
func mustSchedule(t *testing.T, s string) *cron.Schedule {
t.Helper()
sched, err := cron.Weekly(s)
require.NoError(t, err)
return sched
}
+42
View File
@@ -0,0 +1,42 @@
package wsbuilder
import "github.com/prometheus/client_golang/prometheus"
// Metrics holds metrics related to workspace build creation.
type Metrics struct {
workspaceBuildsEnqueued *prometheus.CounterVec
}
// Metric label values for build status.
const (
BuildStatusSuccess = "success"
BuildStatusFailed = "failed"
)
func NewMetrics(reg prometheus.Registerer) (*Metrics, error) {
m := &Metrics{
workspaceBuildsEnqueued: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "coderd",
Name: "workspace_builds_enqueued_total",
Help: "Total number of workspace build enqueue attempts.",
}, []string{"provisioner_type", "build_reason", "transition", "status"}),
}
if reg != nil {
if err := reg.Register(m.workspaceBuildsEnqueued); err != nil {
return nil, err
}
}
return m, nil
}
// RecordBuildEnqueued records a workspace build enqueue attempt. It determines
// the status based on whether an error occurred and increments the counter.
func (m *Metrics) RecordBuildEnqueued(provisionerType, buildReason, transition string, err error) {
status := BuildStatusSuccess
if err != nil {
status = BuildStatusFailed
}
m.workspaceBuildsEnqueued.WithLabelValues(provisionerType, buildReason, transition, status).Inc()
}
+31
View File
@@ -90,6 +90,8 @@ type Builder struct {
prebuiltWorkspaceBuildStage sdkproto.PrebuiltWorkspaceBuildStage
verifyNoLegacyParametersOnce bool
buildMetrics *Metrics
}
type UsageChecker interface {
@@ -253,6 +255,12 @@ func (b Builder) TemplateVersionPresetID(id uuid.UUID) Builder {
return b
}
func (b Builder) BuildMetrics(m *Metrics) Builder {
// nolint: revive
b.buildMetrics = m
return b
}
type BuildError struct {
// Status is a suitable HTTP status code
Status int
@@ -313,11 +321,34 @@ func (b *Builder) Build(
return err
})
if err != nil {
b.recordBuildMetrics(provisionerJob, err)
return nil, nil, nil, xerrors.Errorf("build tx: %w", err)
}
b.recordBuildMetrics(provisionerJob, nil)
return workspaceBuild, provisionerJob, provisionerDaemons, nil
}
// recordBuildMetrics records the workspace build enqueue metric if metrics are
// configured. It determines the appropriate build reason label, using "prebuild"
// for prebuild operations instead of the database reason.
func (b *Builder) recordBuildMetrics(job *database.ProvisionerJob, err error) {
if b.buildMetrics == nil {
return
}
if job == nil || !job.Provisioner.Valid() {
return
}
// Determine the build reason for metrics. Prebuilds use BuildReasonInitiator
// in the database but we want to track them separately in metrics.
buildReason := string(b.reason)
if b.prebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CREATE {
buildReason = provisionerdserver.BuildReasonPrebuild
}
b.buildMetrics.RecordBuildEnqueued(string(job.Provisioner), buildReason, string(b.trans), err)
}
// buildTx contains the business logic of computing a new build. Attributes of the new database objects are computed
// in a functional style, rather than imperative, to emphasize the logic of how they are defined. A simple cache
// of database-fetched objects is stored on the struct to ensure we only fetch things once, even if they are used in
+5
View File
@@ -17,6 +17,7 @@ type AIBridgeInterception struct {
Initiator MinimalUser `json:"initiator"`
Provider string `json:"provider"`
Model string `json:"model"`
Client *string `json:"client"`
Metadata map[string]any `json:"metadata"`
StartedAt time.Time `json:"started_at" format:"date-time"`
EndedAt *time.Time `json:"ended_at" format:"date-time"`
@@ -75,6 +76,7 @@ type AIBridgeListInterceptionsFilter struct {
StartedAfter time.Time `json:"started_after,omitempty" format:"date-time"`
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
Client string `json:"client,omitempty"`
FilterQuery string `json:"q,omitempty"`
}
@@ -101,6 +103,9 @@ func (f AIBridgeListInterceptionsFilter) asRequestOption() RequestOption {
if f.Model != "" {
params = append(params, fmt.Sprintf("model:%q", f.Model))
}
if f.Client != "" {
params = append(params, fmt.Sprintf("client:%q", f.Client))
}
if f.FilterQuery != "" {
// If custom stuff is added, just add it on here.
params = append(params, f.FilterQuery)
+23
View File
@@ -354,6 +354,29 @@ func (c *Client) PauseTask(ctx context.Context, user string, id uuid.UUID) (Paus
return resp, nil
}
// ResumeTaskResponse represents the response from resuming a task.
type ResumeTaskResponse struct {
WorkspaceBuild *WorkspaceBuild `json:"workspace_build"`
}
func (c *Client) ResumeTask(ctx context.Context, user string, id uuid.UUID) (ResumeTaskResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/tasks/%s/%s/resume", user, id.String()), nil)
if err != nil {
return ResumeTaskResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusAccepted {
return ResumeTaskResponse{}, ReadBodyAsError(res)
}
var resp ResumeTaskResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return ResumeTaskResponse{}, err
}
return resp, nil
}
// TaskLogType indicates the source of a task log entry.
type TaskLogType string
+14
View File
@@ -171,6 +171,20 @@ func (c *Client) DeleteAPIKey(ctx context.Context, userID string, id string) err
return nil
}
// ExpireAPIKey expires an API key by id, setting its expiry to now.
// This preserves the API key record for audit purposes rather than deleting it.
func (c *Client) ExpireAPIKey(ctx context.Context, userID string, id string) error {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/keys/%s/expire", userID, id), nil)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode > http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// GetTokenConfig returns deployment options related to token management
func (c *Client) GetTokenConfig(ctx context.Context, userID string) (TokenConfig, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens/tokenconfig", userID), nil)
+48 -48
View File
@@ -1431,7 +1431,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
}
emailHello := serpent.Option{
Name: "Email: Hello",
Description: "The hostname identifying this client to the SMTP server.",
Description: "The hostname identifying the SMTP server.",
Flag: "email-hello",
Env: "CODER_EMAIL_HELLO",
Default: "localhost",
@@ -1523,7 +1523,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
}
emailTLSCertFile := serpent.Option{
Name: "Email TLS: Certificate File",
Description: "Client certificate file for mutual TLS authentication.",
Description: "Certificate file to use.",
Flag: "email-tls-cert-file",
Env: "CODER_EMAIL_TLS_CERTFILE",
Value: &c.Notifications.SMTP.TLS.CertFile,
@@ -1532,7 +1532,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
}
emailTLSCertKeyFile := serpent.Option{
Name: "Email TLS: Certificate Key File",
Description: "Private key file for the client certificate.",
Description: "Certificate key file to use.",
Flag: "email-tls-cert-key-file",
Env: "CODER_EMAIL_TLS_CERTKEYFILE",
Value: &c.Notifications.SMTP.TLS.KeyFile,
@@ -1551,7 +1551,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
}
workspaceHostnameSuffix := serpent.Option{
Name: "Workspace Hostname Suffix",
Description: "Workspace hostnames use this suffix for SSH connections and Coder Connect. By default it is coder, resulting in hostnames like agent.workspace.owner.coder.",
Description: "Workspace hostnames use this suffix in SSH config and Coder Connect on Coder Desktop. By default it is coder, resulting in names like myworkspace.coder.",
Flag: "workspace-hostname-suffix",
Env: "CODER_WORKSPACE_HOSTNAME_SUFFIX",
YAML: "workspaceHostnameSuffix",
@@ -1680,7 +1680,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "TLS Client CA Files",
Description: "PEM-encoded Certificate Authority file used for checking the authenticity of the client.",
Description: "PEM-encoded Certificate Authority file used for checking the authenticity of client.",
Flag: "tls-client-ca-file",
Env: "CODER_TLS_CLIENT_CA_FILE",
Value: &c.TLS.ClientCAFile,
@@ -1742,7 +1742,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "TLS Ciphers",
Description: "Specify specific TLS ciphers that are allowed to be used. See https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53-L75.",
Description: "Specify specific TLS ciphers that allowed to be used. See https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53-L75.",
Flag: "tls-ciphers",
Env: "CODER_TLS_CIPHERS",
Default: "",
@@ -1800,7 +1800,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "DERP Server Region Name",
Description: "Region name to use for the embedded DERP server.",
Description: "Region name that for the embedded DERP server.",
Flag: "derp-server-region-name",
Env: "CODER_DERP_SERVER_REGION_NAME",
Default: "Coder Embedded Relay",
@@ -1811,7 +1811,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "DERP Server STUN Addresses",
Description: "Addresses for STUN servers to establish P2P connections. It's recommended to have at least two STUN servers to give users the best chance of connecting P2P to workspaces. Each STUN server will get its own DERP region, with region IDs starting at `--derp-server-region-id + 1`. Use special value 'disable' to turn off STUN completely.",
Description: "Addresses for STUN servers to establish P2P connections. It's recommended to have at least two STUN servers to give users the best chance of connecting P2P to workspaces. Each STUN server will get it's own DERP region, with region IDs starting at `--derp-server-region-id + 1`. Use special value 'disable' to turn off STUN completely.",
Flag: "derp-server-stun-addresses",
Env: "CODER_DERP_SERVER_STUN_ADDRESSES",
Default: "stun.l.google.com:19302,stun1.l.google.com:19302,stun2.l.google.com:19302,stun3.l.google.com:19302,stun4.l.google.com:19302",
@@ -1833,7 +1833,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "Block Direct Connections",
Description: "Block peer-to-peer (aka. direct) workspace connections. All workspace connections from the CLI will be proxied through Coder (or custom configured DERP servers) and will never be peer-to-peer when enabled. Workspace agents may still reach out to STUN servers to discover their address until they are restarted, but all new connections will be proxied regardless.",
Description: "Block peer-to-peer (aka. direct) workspace connections. All workspace connections from the CLI will be proxied through Coder (or custom configured DERP servers) and will never be peer-to-peer when enabled. Workspaces may still reach out to STUN servers to get their address until they are restarted after this change has been made, but new connections will still be proxied regardless.",
// This cannot be called `disable-direct-connections` because that's
// already a global CLI flag for CLI connections. This is a
// deployment-wide flag.
@@ -1884,7 +1884,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
// Prometheus settings
{
Name: "Prometheus Enable",
Description: "Serve Prometheus metrics on the address defined by prometheus address.",
Description: "Serve prometheus metrics on the address defined by prometheus address.",
Flag: "prometheus-enable",
Env: "CODER_PROMETHEUS_ENABLE",
Value: &c.Prometheus.Enable,
@@ -1894,7 +1894,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "Prometheus Address",
Description: "The bind address to serve Prometheus metrics.",
Description: "The bind address to serve prometheus metrics.",
Flag: "prometheus-address",
Env: "CODER_PROMETHEUS_ADDRESS",
Default: "127.0.0.1:2112",
@@ -1945,7 +1945,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
// Pprof settings
{
Name: "pprof Enable",
Description: "Serve pprof profiling endpoints on the address defined by pprof address.",
Description: "Serve pprof metrics on the address defined by pprof address.",
Flag: "pprof-enable",
Env: "CODER_PPROF_ENABLE",
Value: &c.Pprof.Enable,
@@ -2032,7 +2032,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OAuth2 GitHub Allow Everyone",
Description: "Allow all GitHub users to authenticate. When enabled, allowed orgs and teams must be empty.",
Description: "Allow all logins, setting this option means allowed orgs and teams must be empty.",
Flag: "oauth2-github-allow-everyone",
Env: "CODER_OAUTH2_GITHUB_ALLOW_EVERYONE",
Value: &c.OAuth2.Github.AllowEveryone,
@@ -2079,8 +2079,8 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OIDC Client Key File",
Description: "PEM encoded RSA private key to use for OAuth2 PKI/JWT authorization. " +
"This can be used instead of oidc-client-secret if your IdP supports it.",
Description: "Pem encoded RSA private key to use for oauth2 PKI/JWT authorization. " +
"This can be used instead of oidc-client-secret if your IDP supports it.",
Flag: "oidc-client-key-file",
Env: "CODER_OIDC_CLIENT_KEY_FILE",
YAML: "oidcClientKeyFile",
@@ -2089,8 +2089,8 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OIDC Client Cert File",
Description: "PEM encoded certificate file to use for OAuth2 PKI/JWT authorization. " +
"The public certificate that accompanies oidc-client-key-file. A standard X.509 certificate is expected.",
Description: "Pem encoded certificate file to use for oauth2 PKI/JWT authorization. " +
"The public certificate that accompanies oidc-client-key-file. A standard x509 certificate is expected.",
Flag: "oidc-client-cert-file",
Env: "CODER_OIDC_CLIENT_CERT_FILE",
YAML: "oidcClientCertFile",
@@ -2242,7 +2242,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OIDC Group Field",
Description: "OIDC claim field to use as the user's groups. This field must be set if using the group sync feature and the scope name is not 'groups'.",
Description: "This field must be set if using the group sync feature and the scope name is not 'groups'. Set to the claim to be used for groups.",
Flag: "oidc-group-field",
Env: "CODER_OIDC_GROUP_FIELD",
// This value is intentionally blank. If this is empty, then OIDC group
@@ -2257,7 +2257,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OIDC Group Mapping",
Description: "A map of OIDC group IDs and the groups in Coder they should map to. This is useful when OIDC providers only return group IDs.",
Description: "A map of OIDC group IDs and the group in Coder it should map to. This is useful for when OIDC providers only return group IDs.",
Flag: "oidc-group-mapping",
Env: "CODER_OIDC_GROUP_MAPPING",
Default: "{}",
@@ -2277,7 +2277,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OIDC Regex Group Filter",
Description: "If provided, any group name not matching the regex is ignored. This allows filtering out groups that are not needed. This filter is applied after the OIDC Group Mapping step.",
Description: "If provided any group name not matching the regex is ignored. This allows for filtering out groups that are not needed. This filter is applied after the group mapping.",
Flag: "oidc-group-regex-filter",
Env: "CODER_OIDC_GROUP_REGEX_FILTER",
Default: ".*",
@@ -2287,7 +2287,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OIDC Allowed Groups",
Description: "If provided, only users with at least one group in this list will be allowed to authenticate. This restricts access to a specific set of groups. This check is applied before any group mapping or filtering.",
Description: "If provided any group name not in the list will not be allowed to authenticate. This allows for restricting access to a specific set of groups. This filter is applied after the group mapping and before the regex filter.",
Flag: "oidc-allowed-groups",
Env: "CODER_OIDC_ALLOWED_GROUPS",
Default: "",
@@ -2309,7 +2309,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OIDC User Role Mapping",
Description: "A map of OIDC user role names to Coder role names. This is useful if the role names do not match between systems. If mapped to the empty string, the role will be ignored.",
Description: "A map of the OIDC passed in user roles and the groups in Coder it should map to. This is useful if the group names do not match. If mapped to the empty string, the role will ignored.",
Flag: "oidc-user-role-mapping",
Env: "CODER_OIDC_USER_ROLE_MAPPING",
Default: "{}",
@@ -2319,7 +2319,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OIDC User Role Default",
Description: "If user role sync is enabled, these roles are always included for all authenticated users in addition to synced roles. The 'member' role is always assigned regardless of this setting.",
Description: "If user role sync is enabled, these roles are always included for all authenticated users. The 'member' role is always assigned.",
Flag: "oidc-user-role-default",
Env: "CODER_OIDC_USER_ROLE_DEFAULT",
Default: "",
@@ -2339,7 +2339,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "OpenID connect icon URL",
Description: "URL of the icon to use on the OpenID Connect login button.",
Description: "URL pointing to the icon to use on the OpenID Connect login button.",
Flag: "oidc-icon-url",
Env: "CODER_OIDC_ICON_URL",
Value: &c.OIDC.IconURL,
@@ -2348,7 +2348,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "Signups disabled text",
Description: "Custom text to show on the error page when OIDC signups are disabled. Markdown format is supported.",
Description: "The custom text to show on the error page informing about disabled OIDC signups. Markdown format is supported.",
Flag: "oidc-signups-disabled-text",
Env: "CODER_OIDC_SIGNUPS_DISABLED_TEXT",
Value: &c.OIDC.SignupsDisabledText,
@@ -2807,7 +2807,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
},
{
Name: "SameSite Auth Cookie",
Description: "Controls if the 'SameSite' property is set on browser session cookies.",
Description: "Controls the 'SameSite' property is set on browser session cookies.",
Flag: "samesite-auth-cookie",
Env: "CODER_SAMESITE_AUTH_COOKIE",
// Do not allow "strict" same-site cookies. That would potentially break workspace apps.
@@ -3000,7 +3000,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
{
Name: "SSH Config Options",
Description: "These SSH config options will override the default SSH config options. " +
"Provide options in \"key=value\" or \"key value\" format separated by commas. " +
"Provide options in \"key=value\" or \"key value\" format separated by commas." +
"Using this incorrectly can break SSH to your deployment, use cautiously.",
Flag: "ssh-config-options",
Env: "CODER_SSH_CONFIG_OPTIONS",
@@ -3041,7 +3041,7 @@ Write out the current server config as YAML to stdout.`,
{
// Env handling is done in cli.ReadGitAuthFromEnvironment
Name: "External Auth Providers",
Description: "Configure external authentication providers for Git and other services.",
Description: "External Authentication providers.",
YAML: "externalAuthProviders",
Flag: "external-auth-providers",
Value: &c.ExternalAuthConfigs,
@@ -3059,7 +3059,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "Proxy Health Check Interval",
Description: "The interval at which coderd checks the status of workspace proxies.",
Description: "The interval in which coderd should be checking the status of workspace proxies.",
Flag: "proxy-health-interval",
Env: "CODER_PROXY_HEALTH_INTERVAL",
Default: (time.Minute).String(),
@@ -3080,7 +3080,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "Allow Custom Quiet Hours",
Description: "Allow users to set their own quiet hours schedule for when workspaces are stopped (depending on template autostop requirement settings). If false, users can't change their quiet hours schedule and the site default is always used.",
Description: "Allow users to set their own quiet hours schedule for workspaces to stop in (depending on template autostop requirement settings). If false, users can't change their quiet hours schedule and the site default is always used.",
Flag: "allow-custom-quiet-hours",
Env: "CODER_ALLOW_CUSTOM_QUIET_HOURS",
Default: "true",
@@ -3192,7 +3192,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "Notifications: Email: Hello",
Description: "The hostname identifying this client to the SMTP server.",
Description: "The hostname identifying the SMTP server.",
Flag: "notifications-email-hello",
Env: "CODER_NOTIFICATIONS_EMAIL_HELLO",
Value: &c.Notifications.SMTP.Hello,
@@ -3355,7 +3355,7 @@ Write out the current server config as YAML to stdout.`,
Name: "Notifications: Store Sync Interval",
Description: "The notifications system buffers message updates in memory to ease pressure on the database. " +
"This option controls how often it synchronizes its state with the database. The shorter this value the " +
"lower the chance of state inconsistency in a non-graceful shutdown - but it also increases load on the " +
"lower the change of state inconsistency in a non-graceful shutdown - but it also increases load on the " +
"database. It is recommended to keep this option at its default value.",
Flag: "notifications-store-sync-interval",
Env: "CODER_NOTIFICATIONS_STORE_SYNC_INTERVAL",
@@ -3370,7 +3370,7 @@ Write out the current server config as YAML to stdout.`,
Name: "Notifications: Store Sync Buffer Size",
Description: "The notifications system buffers message updates in memory to ease pressure on the database. " +
"This option controls how many updates are kept in memory. The lower this value the " +
"lower the chance of state inconsistency in a non-graceful shutdown - but it also increases load on the " +
"lower the change of state inconsistency in a non-graceful shutdown - but it also increases load on the " +
"database. It is recommended to keep this option at its default value.",
Flag: "notifications-store-sync-buffer-size",
Env: "CODER_NOTIFICATIONS_STORE_SYNC_BUFFER_SIZE",
@@ -3434,7 +3434,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "Reconciliation Backoff Interval",
Description: "Amount of time to add to the reconciliation backoff delay after each prebuild failure, before the next retry attempt is made.",
Description: "Interval to increase reconciliation backoff by when prebuilds fail, after which a retry attempt is made.",
Flag: "workspace-prebuilds-reconciliation-backoff-interval",
Env: "CODER_WORKSPACE_PREBUILDS_RECONCILIATION_BACKOFF_INTERVAL",
Value: &c.Prebuilds.ReconciliationBackoffInterval,
@@ -3446,7 +3446,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "Reconciliation Backoff Lookback Period",
Description: "Time period to look back when counting failed prebuilds to calculate the backoff delay.",
Description: "Interval to look back to determine number of failed prebuilds, which influences backoff.",
Flag: "workspace-prebuilds-reconciliation-backoff-lookback-period",
Env: "CODER_WORKSPACE_PREBUILDS_RECONCILIATION_BACKOFF_LOOKBACK_PERIOD",
Value: &c.Prebuilds.ReconciliationBackoffLookback,
@@ -3458,7 +3458,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "Failure Hard Limit",
Description: "Maximum number of consecutive failed prebuilds before a preset is considered hard-limited and stops automatic prebuild creation. Disabled when set to zero.",
Description: "Maximum number of consecutive failed prebuilds before a preset hits the hard limit; disabled when set to zero.",
Flag: "workspace-prebuilds-failure-hard-limit",
Env: "CODER_WORKSPACE_PREBUILDS_FAILURE_HARD_LIMIT",
Value: &c.Prebuilds.FailureHardLimit,
@@ -3481,7 +3481,7 @@ Write out the current server config as YAML to stdout.`,
// AI Bridge Options
{
Name: "AI Bridge Enabled",
Description: "Enable the embedded AI Bridge service to intercept and record AI provider requests.",
Description: "Whether to start an in-memory aibridged instance.",
Flag: "aibridge-enabled",
Env: "CODER_AIBRIDGE_ENABLED",
Value: &c.AI.BridgeConfig.Enabled,
@@ -3501,7 +3501,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge OpenAI Key",
Description: "API key for authenticating with the OpenAI API.",
Description: "The key to authenticate against the OpenAI API.",
Flag: "aibridge-openai-key",
Env: "CODER_AIBRIDGE_OPENAI_KEY",
Value: &c.AI.BridgeConfig.OpenAI.Key,
@@ -3521,7 +3521,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge Anthropic Key",
Description: "API key for authenticating with the Anthropic API.",
Description: "The key to authenticate against the Anthropic API.",
Flag: "aibridge-anthropic-key",
Env: "CODER_AIBRIDGE_ANTHROPIC_KEY",
Value: &c.AI.BridgeConfig.Anthropic.Key,
@@ -3553,7 +3553,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge Bedrock Access Key",
Description: "AWS access key for authenticating with the AWS Bedrock API.",
Description: "The access key to authenticate against the AWS Bedrock API.",
Flag: "aibridge-bedrock-access-key",
Env: "CODER_AIBRIDGE_BEDROCK_ACCESS_KEY",
Value: &c.AI.BridgeConfig.Bedrock.AccessKey,
@@ -3563,7 +3563,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge Bedrock Access Key Secret",
Description: "AWS secret access key for authenticating with the AWS Bedrock API.",
Description: "The access key secret to use with the access key to authenticate against the AWS Bedrock API.",
Flag: "aibridge-bedrock-access-key-secret",
Env: "CODER_AIBRIDGE_BEDROCK_ACCESS_KEY_SECRET",
Value: &c.AI.BridgeConfig.Bedrock.AccessKeySecret,
@@ -3593,7 +3593,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge Inject Coder MCP tools",
Description: "Enable injection of Coder's MCP tools into intercepted AI Bridge requests. Requires the 'oauth2' and 'mcp-server-http' experiments.",
Description: "Whether to inject Coder's MCP tools into intercepted AI Bridge requests (requires the \"oauth2\" and \"mcp-server-http\" experiments to be enabled).",
Flag: "aibridge-inject-coder-mcp-tools",
Env: "CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS",
Value: &c.AI.BridgeConfig.InjectCoderMCPTools,
@@ -3603,7 +3603,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge Data Retention Duration",
Description: "How long to retain AI Bridge data including interceptions, tokens, prompts, and tool usage records.",
Description: "Length of time to retain data such as interceptions and all related records (token, prompt, tool use).",
Flag: "aibridge-retention",
Env: "CODER_AIBRIDGE_RETENTION",
Value: &c.AI.BridgeConfig.Retention,
@@ -3656,7 +3656,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge Circuit Breaker Enabled",
Description: "Enable the circuit breaker to protect against cascading failures from upstream AI provider rate limits and overload errors (HTTP 429, 503, 529).",
Description: "Enable the circuit breaker to protect against cascading failures from upstream AI provider rate limits (429, 503, 529 overloaded).",
Flag: "aibridge-circuit-breaker-enabled",
Env: "CODER_AIBRIDGE_CIRCUIT_BREAKER_ENABLED",
Value: &c.AI.BridgeConfig.CircuitBreakerEnabled,
@@ -3666,7 +3666,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge Circuit Breaker Failure Threshold",
Description: "Number of consecutive failures that trigger the circuit breaker to open.",
Description: "Number of consecutive failures that triggers the circuit breaker to open.",
Flag: "aibridge-circuit-breaker-failure-threshold",
Env: "CODER_AIBRIDGE_CIRCUIT_BREAKER_FAILURE_THRESHOLD",
Value: serpent.Validate(&c.AI.BridgeConfig.CircuitBreakerFailureThreshold, func(value *serpent.Int64) error {
@@ -3682,7 +3682,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "AI Bridge Circuit Breaker Interval",
Description: "Time window for counting failures before resetting the failure count in the closed state.",
Description: "Cyclic period of the closed state for clearing internal failure counts.",
Flag: "aibridge-circuit-breaker-interval",
Env: "CODER_AIBRIDGE_CIRCUIT_BREAKER_INTERVAL",
Value: &c.AI.BridgeConfig.CircuitBreakerInterval,
@@ -3830,7 +3830,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "Workspace Agent Logs Retention",
Description: "How long workspace agent logs are retained. Logs from non-latest builds are deleted if the agent hasn't connected within this period. Logs from the latest build for each workspace are always retained. Set to 0 to disable automatic deletion.",
Description: "How long workspace agent logs are retained. Logs from non-latest builds are deleted if the agent hasn't connected within this period. Logs from the latest build are always retained. Set to 0 to disable automatic deletion.",
Flag: "workspace-agent-logs-retention",
Env: "CODER_WORKSPACE_AGENT_LOGS_RETENTION",
Value: &c.Retention.WorkspaceAgentLogs,
@@ -3841,7 +3841,7 @@ Write out the current server config as YAML to stdout.`,
},
{
Name: "Enable Authorization Recordings",
Description: "All API requests will have a header including all authorization calls made during the request. " +
Description: "All api requests will have a header including all authorization calls made during the request. " +
"This is used for debugging purposes and only available for dev builds.",
Required: false,
Flag: "enable-authz-recordings",
+1
View File
@@ -32,6 +32,7 @@ type Template struct {
Description string `json:"description"`
Deprecated bool `json:"deprecated"`
DeprecationMessage string `json:"deprecation_message"`
Deleted bool `json:"deleted"`
Icon string `json:"icon"`
DefaultTTLMillis int64 `json:"default_ttl_ms"`
ActivityBumpMillis int64 `json:"activity_bump_ms"`
+9
View File
@@ -59,6 +59,15 @@ const (
BuildReasonVSCodeConnection BuildReason = "vscode_connection"
// BuildReasonJetbrainsConnection "jetbrains_connection" is used when a build to start a workspace is triggered by a JetBrains connection.
BuildReasonJetbrainsConnection BuildReason = "jetbrains_connection"
// BuildReasonTaskAutoPause "task_auto_pause" is used when a build to stop
// a task workspace is triggered by the lifecycle executor.
BuildReasonTaskAutoPause BuildReason = "task_auto_pause"
// BuildReasonTaskManualPause "task_manual_pause" is used when a build to
// stop a task workspace is triggered by a user.
BuildReasonTaskManualPause BuildReason = "task_manual_pause"
// BuildReasonTaskResume "task_resume" is used when a build to
// start a task workspace is triggered by a user.
BuildReasonTaskResume BuildReason = "task_resume"
)
// WorkspaceBuild is an at-point representation of a workspace state.
+1
View File
@@ -110,6 +110,7 @@ const (
CreateWorkspaceBuildReasonVSCodeConnection CreateWorkspaceBuildReason = "vscode_connection"
CreateWorkspaceBuildReasonJetbrainsConnection CreateWorkspaceBuildReason = "jetbrains_connection"
CreateWorkspaceBuildReasonTaskManualPause CreateWorkspaceBuildReason = "task_manual_pause"
CreateWorkspaceBuildReasonTaskResume CreateWorkspaceBuildReason = "task_resume"
)
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
+112 -19
View File
@@ -1,11 +1,11 @@
# Dev Containers
Dev containers allow developers to define their development environment
Dev Containers allow developers to define their development environment
as code using the [Dev Container specification](https://containers.dev/).
Configuration lives in a `devcontainer.json` file alongside source code,
enabling consistent, reproducible environments.
By adopting dev containers, organizations can:
By adopting Dev Containers, organizations can:
- **Standardize environments**: Eliminate "works on my machine" issues while
still allowing developers to customize their tools within approved boundaries.
@@ -14,36 +14,129 @@ By adopting dev containers, organizations can:
- **Improve security**: Use hardened base images and controlled package
registries to enforce security policies while enabling developer self-service.
Coder supports two approaches for running dev containers. Choose based on your
infrastructure and workflow requirements.
Coder supports multiple approaches for running Dev Containers. Choose based on
your infrastructure and workflow requirements.
## Dev Containers Integration
## Comparison
The Dev Containers Integration uses the standard `@devcontainers/cli` and Docker
to run containers inside your workspace. This is the recommended approach for
most use cases.
| Method | Dev Container CLI | Envbuilder | CI/CD Pre-built |
|-------------------------------------------|--------------------------------------------------------|---------------------------------------|-----------------------------------------------------------|
| **Standard Dev Container implementation** | ✅ Yes | ❌ No | ✅ Yes |
| **Full Dev Container Spec Support** | ✅ All options | ❌ Limited options | \~ Most options |
| **Startup Time** | Build at runtime, faster with caching | Build at runtime, faster with caching | Fast (pre-built) |
| **Docker Required** | ❌ Yes | ✅ No | ✅ No |
| **Caching** | More difficult | ✅ Yes | ✅ Yes |
| **Repo Discovery** | ✅ Yes | ❌ No | ❌ No |
| **Custom Apps in-spec** | ✅ Via spec args | ❌ No | ❌ No |
| **Debugging** | Easy | Very difficult | Moderate |
| **Versioning** | \~ Via spec, or template | \~ Via spec, or template | ✅ Image tags |
| **Testing Pipeline** | \~ Via CLI in CI/CD | \~ Via CLI in CI/CD | ✅ Yes, via the same pipeline |
| **Feedback Loop** | ✅ Fast | ✅ Fast | Slow (build, and then test) |
| **Maintenance Status** | ✅ Active | ⚠️ Maintenance mode | ✅ Active |
| **Best For** | Dev flexibility, rapid iteration, feature completeness | Restricted environments | Controlled and centralized releases, less dev flexibility |
## Dev Container CLI
The Dev Container CLI integration uses the standard `@devcontainers/cli` and Docker to run
Dev Containers inside your workspace. This is the recommended approach for most use
cases and provides the most complete Dev Container experience.
Uses the
[devcontainers-cli module](https://registry.coder.com/modules/devcontainers-cli),
the `coder_devcontainer` Terraform resource, and
`CODER_AGENT_DEVCONTAINERS_ENABLE=true`.
**Pros:**
- Standard Dev Container implementation via Microsoft's official `@devcontainers/cli` package.
- Supports all Dev Container configuration options.
- Supports custom arguments in the Dev Container spec for defining custom apps
without needing template changes.
- Supports discovery of repos with Dev Containers in them.
- Easier to debug, since you have access to the outer container.
**Cons / Requirements:**
- Requires Docker in workspaces. This does not necessarily mean Docker-in-Docker
or a specific Kubernetes runtime — you could use Rootless Podman or a
privileged sidecar.
- Caching is more difficult than with Envbuilder or CI/CD pre-built approaches.
**Best for:**
- Workspaces with Docker available (Docker-in-Docker or mounted socket)
- Dev container management in the Coder dashboard (discovery, status, rebuild)
- Multiple dev containers per workspace
- Dev flexibility, rapid iteration, and feature completeness.
- Workspaces with Docker available (Docker-in-Docker or mounted socket).
- Dev Container management in the Coder dashboard (discovery, status, rebuild).
- Multiple Dev Containers per workspace.
[Configure Dev Containers Integration](./integration.md)
See the [Dev Containers Integration](./integration.md) page for instructions.
For user documentation, see the
[Dev Containers user guide](../../../user-guides/devcontainers/index.md).
## Envbuilder
Envbuilder transforms the workspace image itself from a `devcontainer.json`,
rather than running containers inside the workspace. It does not require
a Docker daemon.
Envbuilder transforms the workspace environment itself from a Dev Container spec (i.e `devcontainer.json`),
rather than running containers inside the workspace. It does not require a Docker
daemon.
> [!NOTE]
> Envbuilder is in **maintenance mode**. No new features are planned to be
> implemented. For most use cases, the
> [Dev Container CLI](#dev-container-cli) or [CI/CD Pre-built](#cicd-pre-built)
> approaches are recommended.
**Pros:**
- Does not require Docker in workspaces.
- Easier caching.
**Cons:**
- Very complicated to debug, since Envbuilder replaces the filesystem of the
container. You can't access that environment within Coder easily if it fails,
and you won't have many debug tools.
- Does not support all of the Dev Container configuration options.
- Does not support discovery of repos with Dev Containers in them.
- Less flexible and more complex in general.
**Best for:**
- Environments where Docker is unavailable or restricted
- Infrastructure-level control over image builds, caching, and security scanning
- Kubernetes-native deployments without privileged containers
- Environments where Docker is unavailable or restricted.
- Infrastructure-level control over image builds, caching, and security scanning.
- Kubernetes-native deployments without privileged containers.
[Configure Envbuilder](./envbuilder/index.md)
See the [Envbuilder](./envbuilder/index.md) page for instructions.
## CI/CD Pre-built
Build the Dev Container image from CI/CD and pull it from within Terraform. This
approach separates the image build step from the workspace startup, resulting in
fast startup times and a generic template that doesn't have any
Dev Container-specific configuration items.
**Pros:**
- Standard Dev Container implementation via Microsoft's official `@devcontainers/cli` package.
- Faster startup time — no need for a specific caching setup.
- The template is generic and doesn't have any Dev Container-specific
configuration items.
- Versioned via image tags.
- Testable pipeline.
**Cons:**
- Adds a build step.
- Does not support all of the runtime options, but still supports more options
than Envbuilder.
- Does not support discovery of repos with Dev Containers.
- Slow feedback loop (build, then test).
**Best for:**
- Controlled and centralized releases with less dev flexibility.
- Teams that already have CI/CD pipelines for building images.
- Environments that need fast, predictable startup times.
For an example workflow, see the
[uwu/basic-env CI/CD workflow](https://github.com/uwu/basic-env/blob/main/.github/workflows/_build-and-push.yml).
@@ -1,13 +1,14 @@
# Configure a template for Dev Containers
This guide covers the Dev Containers Integration, which uses Docker. For
This guide covers the Dev Containers CLI Integration, which uses Docker. For
environments without Docker, see [Envbuilder](./envbuilder/index.md) as an
alternative.
To enable Dev Containers in workspaces, configure your template with the Dev Containers
modules and configurations outlined in this doc.
To enable Dev Containers in workspaces, configure your template with the [`devcontainers-cli`](https://registry.coder.com/modules/coder/devcontainers-cli)
module and configurations outlined in this doc.
Dev Containers are currently not supported in Windows or macOS workspaces.
> [!WARNING]
> Dev Containers are currently not supported in Windows or macOS workspaces.
## Configuration Modes
@@ -16,7 +17,7 @@ There are two approaches to configuring Dev Containers in Coder:
### Manual Configuration
Use the [`coder_devcontainer`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/devcontainer) Terraform resource to explicitly define which Dev
Containers should be started in your workspace. This approach provides:
Container(s) should be started in your workspace. This approach provides:
- Predictable behavior and explicit control
- Clear template configuration
@@ -38,7 +39,7 @@ or work with many projects, as it reduces template maintenance overhead.
Use the
[devcontainers-cli](https://registry.coder.com/modules/devcontainers-cli) module
to ensure the `@devcontainers/cli` is installed in your workspace:
to ensure that the `@devcontainers/cli` NPM package is installed in your workspace:
```terraform
module "devcontainers-cli" {
@@ -54,7 +55,7 @@ Alternatively, install the devcontainer CLI manually in your base image.
The
[`coder_devcontainer`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/devcontainer)
resource automatically starts a Dev Container in your workspace, ensuring it's
resource automatically starts a specific Dev Container in your workspace, ensuring it's
ready when you access the workspace:
```terraform
@@ -74,9 +75,9 @@ For multi-repo workspaces, define multiple `coder_devcontainer` resources, each
pointing to a different repository. Each one runs as a separate sub-agent with
its own terminal and apps in the dashboard.
## Enable Dev Containers Integration
## Enable Dev Containers CLI Integration
Dev Containers integration is **enabled by default** in Coder 2.24.0 and later.
The Dev Containers CLI Integration is **enabled by default** in Coder 2.24.0 and later.
You don't need to set any environment variables unless you want to change the
default behavior.
@@ -110,7 +111,7 @@ the feature.
**Default: `true`** • **Added in: v2.24.0**
Enables the Dev Containers integration in the Coder agent.
Enables the Dev Containers CLI Integration in the Coder agent.
The Dev Containers feature is enabled by default. You can explicitly disable it
by setting this to `false`.
@@ -160,6 +161,8 @@ always auto-start regardless of this setting.
> [dev container features](../../../user-guides/devcontainers/working-with-dev-containers.md#dev-container-features).
> For Coder-specific apps, use the
> [`apps` customization](../../../user-guides/devcontainers/customizing-dev-containers.md#custom-apps).
>
> If you really need modules, look into [other Dev Container options](./index.md#comparison)
Developers can customize individual dev containers using the `customizations.coder`
block in their `devcontainer.json` file. Available options include:
@@ -174,7 +177,7 @@ block in their `devcontainer.json` file. Available options include:
For the full reference, see
[Customizing dev containers](../../../user-guides/devcontainers/customizing-dev-containers.md).
## Complete Template Example
## Simplified Template Example
Here's a simplified template example that uses Dev Containers with manual
configuration:
@@ -216,7 +219,9 @@ resource "coder_devcontainer" "my-repository" {
### Alternative: Project Discovery with Autostart
By default, discovered containers appear in the dashboard but developers must
manually start them. To have them start automatically, enable autostart:
manually start them. To have them start automatically, enable autostart by setting the `CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE` environment to `true` within the workspace.
For example, with a Docker-based template:
```terraform
resource "docker_container" "workspace" {
@@ -236,24 +241,24 @@ With autostart enabled:
- Discovered containers automatically build and start during workspace
initialization
- The `coder_devcontainer` resource is not required
- The [`coder_devcontainer`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/devcontainer) resource is not required
- Developers can work with multiple projects seamlessly
> [!NOTE]
>
> When using project discovery, you still need to install the devcontainers CLI
> When using project discovery, you still need to install the `devcontainer` CLI
> using the module or in your base image.
## Example Template
The [Docker (Dev Containers)](https://github.com/coder/coder/tree/main/examples/templates/docker-devcontainer)
starter template demonstrates Dev Containers integration using Docker-in-Docker.
It includes the `devcontainers-cli` module, `git-clone` module, and the
`coder_devcontainer` resource.
starter template demonstrates the Dev Containers CLI Integration using Docker-in-Docker.
It includes the [`devcontainers-cli`](https://registry.coder.com/modules/coder/devcontainers-cli) module, [`git-clone`](https://registry.coder.com/modules/git-clone) module, and the
[`coder_devcontainer`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/devcontainer) resource.
## Next Steps
- [Dev Containers Integration](../../../user-guides/devcontainers/index.md)
- [Dev Containers CLI Integration user guide](../../../user-guides/devcontainers/index.md)
- [Customizing Dev Containers](../../../user-guides/devcontainers/customizing-dev-containers.md)
- [Working with Dev Containers](../../../user-guides/devcontainers/working-with-dev-containers.md)
- [Troubleshooting Dev Containers](../../../user-guides/devcontainers/troubleshooting-dev-containers.md)
+164 -99
View File
@@ -104,105 +104,170 @@ deployment. They will always be available from the agent.
<!-- Code generated by 'make docs/admin/integrations/prometheus.md'. DO NOT EDIT -->
| Name | Type | Description | Labels |
|---------------------------------------------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| `agent_scripts_executed_total` | counter | Total number of scripts executed by the Coder agent. Includes cron scheduled scripts. | `agent_name` `success` `template_name` `username` `workspace_name` |
| `coder_aibridged_injected_tool_invocations_total` | counter | The number of times an injected MCP tool was invoked by aibridge. | `model` `name` `provider` `server` |
| `coder_aibridged_interceptions_duration_seconds` | histogram | The total duration of intercepted requests, in seconds. The majority of this time will be the upstream processing of the request. aibridge has no control over upstream processing time, so it's just an illustrative metric. | `model` `provider` |
| `coder_aibridged_interceptions_inflight` | gauge | The number of intercepted requests which are being processed. | `model` `provider` `route` |
| `coder_aibridged_interceptions_total` | counter | The count of intercepted requests. | `initiator_id` `method` `model` `provider` `route` `status` |
| `coder_aibridged_non_injected_tool_selections_total` | counter | The number of times an AI model selected a tool to be invoked by the client. | `model` `name` `provider` |
| `coder_aibridged_prompts_total` | counter | The number of prompts issued by users (initiators). | `initiator_id` `model` `provider` |
| `coder_aibridged_tokens_total` | counter | The number of tokens used by intercepted requests. | `initiator_id` `model` `provider` `type` |
| `coderd_agentapi_metadata_batch_size` | histogram | Total number of metadata entries in each batch, updated before flushes. | |
| `coderd_agentapi_metadata_batch_utilization` | histogram | Number of metadata keys per agent in each batch, updated before flushes. | |
| `coderd_agentapi_metadata_batches_total` | counter | Total number of metadata batches flushed. | `reason` |
| `coderd_agentapi_metadata_dropped_keys_total` | counter | Total number of metadata keys dropped due to capacity limits. | |
| `coderd_agentapi_metadata_flush_duration_seconds` | histogram | Time taken to flush metadata batch to database and pubsub. | `reason` |
| `coderd_agentapi_metadata_flushed_total` | counter | Total number of unique metadatas flushed. | |
| `coderd_agentapi_metadata_publish_errors_total` | counter | Total number of metadata batch pubsub publish calls that have resulted in an error. | |
| `coderd_agents_apps` | gauge | Agent applications with statuses. | `agent_name` `app_name` `health` `username` `workspace_name` |
| `coderd_agents_connection_latencies_seconds` | gauge | Agent connection latencies in seconds. | `agent_name` `derp_region` `preferred` `username` `workspace_name` |
| `coderd_agents_connections` | gauge | Agent connections with statuses. | `agent_name` `lifecycle_state` `status` `tailnet_node` `username` `workspace_name` |
| `coderd_agents_up` | gauge | The number of active agents per workspace. | `template_name` `username` `workspace_name` |
| `coderd_agentstats_connection_count` | gauge | The number of established connections by agent | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_connection_median_latency_seconds` | gauge | The median agent connection latency | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_currently_reachable_peers` | gauge | The number of peers (e.g. clients) that are currently reachable over the encrypted network. | `agent_name` `connection_type` `template_name` `username` `workspace_name` |
| `coderd_agentstats_rx_bytes` | gauge | Agent Rx bytes | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_jetbrains` | gauge | The number of session established by JetBrains | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_reconnecting_pty` | gauge | The number of session established by reconnecting PTY | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_ssh` | gauge | The number of session established by SSH | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_vscode` | gauge | The number of session established by VSCode | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_startup_script_seconds` | gauge | The number of seconds the startup script took to execute. | `agent_name` `success` `template_name` `username` `workspace_name` |
| `coderd_agentstats_tx_bytes` | gauge | Agent Tx bytes | `agent_name` `username` `workspace_name` |
| `coderd_api_active_users_duration_hour` | gauge | The number of users that have been active within the last hour. | |
| `coderd_api_concurrent_requests` | gauge | The number of concurrent API requests. | |
| `coderd_api_concurrent_websockets` | gauge | The total number of concurrent API websockets. | |
| `coderd_api_request_latencies_seconds` | histogram | Latency distribution of requests in seconds. | `method` `path` |
| `coderd_api_requests_processed_total` | counter | The total number of processed API requests | `code` `method` `path` |
| `coderd_api_websocket_durations_seconds` | histogram | Websocket duration distribution of requests in seconds. | `path` |
| `coderd_api_workspace_latest_build` | gauge | The latest workspace builds with a status. | `status` |
| `coderd_insights_applications_usage_seconds` | gauge | The application usage per template. | `application_name` `slug` `template_name` |
| `coderd_insights_parameters` | gauge | The parameter usage per template. | `parameter_name` `parameter_type` `parameter_value` `template_name` |
| `coderd_insights_templates_active_users` | gauge | The number of active users of the template. | `template_name` |
| `coderd_license_active_users` | gauge | The number of active users. | |
| `coderd_license_errors` | gauge | The number of active license errors. | |
| `coderd_license_limit_users` | gauge | The user seats limit based on the active Coder license. | |
| `coderd_license_user_limit_enabled` | gauge | Returns 1 if the current license enforces the user limit. | |
| `coderd_license_warnings` | gauge | The number of active license warnings. | |
| `coderd_metrics_collector_agents_execution_seconds` | histogram | Histogram for duration of agents metrics collection in seconds. | |
| `coderd_oauth2_external_requests_rate_limit` | gauge | The total number of allowed requests per interval. | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_next_reset_unix` | gauge | Unix timestamp of the next interval | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_remaining` | gauge | The remaining number of allowed requests in this interval. | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_reset_in_seconds` | gauge | Seconds until the next interval | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_used` | gauge | The number of requests made in this interval. | `name` `resource` |
| `coderd_oauth2_external_requests_total` | counter | The total number of api calls made to external oauth2 providers. 'status_code' will be 0 if the request failed with no response. | `name` `source` `status_code` |
| `coderd_prebuilt_workspace_claim_duration_seconds` | histogram | Time to claim a prebuilt workspace by organization, template, and preset. | `organization_name` `preset_name` `template_name` |
| `coderd_provisionerd_job_timings_seconds` | histogram | The provisioner job time duration in seconds. | `provisioner` `status` |
| `coderd_provisionerd_jobs_current` | gauge | The number of currently running provisioner jobs. | `provisioner` |
| `coderd_provisionerd_num_daemons` | gauge | The number of provisioner daemons. | |
| `coderd_provisionerd_workspace_build_timings_seconds` | histogram | The time taken for a workspace to build. | `status` `template_name` `template_version` `workspace_transition` |
| `coderd_template_workspace_build_duration_seconds` | histogram | Duration from workspace build creation to agent ready, by template. | `is_prebuild` `organization_name` `status` `template_name` `transition` |
| `coderd_workspace_builds_total` | counter | The number of workspaces started, updated, or deleted. | `action` `owner_email` `status` `template_name` `template_version` `workspace_name` |
| `coderd_workspace_creation_duration_seconds` | histogram | Time to create a workspace by organization, template, preset, and type (regular or prebuild). | `organization_name` `preset_name` `template_name` `type` |
| `coderd_workspace_creation_total` | counter | Total regular (non-prebuilt) workspace creations by organization, template, and preset. | `organization_name` `preset_name` `template_name` |
| `coderd_workspace_latest_build_status` | gauge | The current workspace statuses by template, transition, and owner. | `status` `template_name` `template_version` `workspace_owner` `workspace_transition` |
| `go_gc_duration_seconds` | summary | A summary of the pause duration of garbage collection cycles. | |
| `go_goroutines` | gauge | Number of goroutines that currently exist. | |
| `go_info` | gauge | Information about the Go environment. | `version` |
| `go_memstats_alloc_bytes` | gauge | Number of bytes allocated and still in use. | |
| `go_memstats_alloc_bytes_total` | counter | Total number of bytes allocated, even if freed. | |
| `go_memstats_buck_hash_sys_bytes` | gauge | Number of bytes used by the profiling bucket hash table. | |
| `go_memstats_frees_total` | counter | Total number of frees. | |
| `go_memstats_gc_sys_bytes` | gauge | Number of bytes used for garbage collection system metadata. | |
| `go_memstats_heap_alloc_bytes` | gauge | Number of heap bytes allocated and still in use. | |
| `go_memstats_heap_idle_bytes` | gauge | Number of heap bytes waiting to be used. | |
| `go_memstats_heap_inuse_bytes` | gauge | Number of heap bytes that are in use. | |
| `go_memstats_heap_objects` | gauge | Number of allocated objects. | |
| `go_memstats_heap_released_bytes` | gauge | Number of heap bytes released to OS. | |
| `go_memstats_heap_sys_bytes` | gauge | Number of heap bytes obtained from system. | |
| `go_memstats_last_gc_time_seconds` | gauge | Number of seconds since 1970 of last garbage collection. | |
| `go_memstats_lookups_total` | counter | Total number of pointer lookups. | |
| `go_memstats_mallocs_total` | counter | Total number of mallocs. | |
| `go_memstats_mcache_inuse_bytes` | gauge | Number of bytes in use by mcache structures. | |
| `go_memstats_mcache_sys_bytes` | gauge | Number of bytes used for mcache structures obtained from system. | |
| `go_memstats_mspan_inuse_bytes` | gauge | Number of bytes in use by mspan structures. | |
| `go_memstats_mspan_sys_bytes` | gauge | Number of bytes used for mspan structures obtained from system. | |
| `go_memstats_next_gc_bytes` | gauge | Number of heap bytes when next garbage collection will take place. | |
| `go_memstats_other_sys_bytes` | gauge | Number of bytes used for other system allocations. | |
| `go_memstats_stack_inuse_bytes` | gauge | Number of bytes in use by the stack allocator. | |
| `go_memstats_stack_sys_bytes` | gauge | Number of bytes obtained from system for stack allocator. | |
| `go_memstats_sys_bytes` | gauge | Number of bytes obtained from system. | |
| `go_threads` | gauge | Number of OS threads created. | |
| `process_cpu_seconds_total` | counter | Total user and system CPU time spent in seconds. | |
| `process_max_fds` | gauge | Maximum number of open file descriptors. | |
| `process_open_fds` | gauge | Number of open file descriptors. | |
| `process_resident_memory_bytes` | gauge | Resident memory size in bytes. | |
| `process_start_time_seconds` | gauge | Start time of the process since unix epoch in seconds. | |
| `process_virtual_memory_bytes` | gauge | Virtual memory size in bytes. | |
| `process_virtual_memory_max_bytes` | gauge | Maximum amount of virtual memory available in bytes. | |
| `promhttp_metric_handler_requests_in_flight` | gauge | Current number of scrapes being served. | |
| `promhttp_metric_handler_requests_total` | counter | Total number of scrapes by HTTP status code. | `code` |
| Name | Type | Description | Labels |
|-------------------------------------------------------------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|
| `agent_scripts_executed_total` | counter | Total number of scripts executed by the Coder agent. Includes cron scheduled scripts. | `agent_name` `success` `template_name` `username` `workspace_name` |
| `coder_aibridged_circuit_breaker_rejects_total` | counter | Total number of requests rejected due to open circuit breaker. | `endpoint` `model` `provider` |
| `coder_aibridged_circuit_breaker_state` | gauge | Current state of the circuit breaker (0=closed, 0.5=half-open, 1=open). | `endpoint` `model` `provider` |
| `coder_aibridged_circuit_breaker_trips_total` | counter | Total number of times the circuit breaker transitioned to open state. | `endpoint` `model` `provider` |
| `coder_aibridged_injected_tool_invocations_total` | counter | The number of times an injected MCP tool was invoked by aibridge. | `model` `name` `provider` `server` |
| `coder_aibridged_interceptions_duration_seconds` | histogram | The total duration of intercepted requests, in seconds. The majority of this time will be the upstream processing of the request. aibridge has no control over upstream processing time, so it's just an illustrative metric. | `model` `provider` |
| `coder_aibridged_interceptions_inflight` | gauge | The number of intercepted requests which are being processed. | `model` `provider` `route` |
| `coder_aibridged_interceptions_total` | counter | The count of intercepted requests. | `initiator_id` `method` `model` `provider` `route` `status` |
| `coder_aibridged_non_injected_tool_selections_total` | counter | The number of times an AI model selected a tool to be invoked by the client. | `model` `name` `provider` |
| `coder_aibridged_passthrough_total` | counter | The count of requests which were not intercepted but passed through to the upstream. | `method` `provider` `route` |
| `coder_aibridged_prompts_total` | counter | The number of prompts issued by users (initiators). | `initiator_id` `model` `provider` |
| `coder_aibridged_tokens_total` | counter | The number of tokens used by intercepted requests. | `initiator_id` `model` `provider` `type` |
| `coder_aibridgeproxyd_connect_sessions_total` | counter | Total number of CONNECT sessions established. | `type` |
| `coder_aibridgeproxyd_inflight_mitm_requests` | gauge | Number of MITM requests currently being processed. | `provider` |
| `coder_aibridgeproxyd_mitm_requests_total` | counter | Total number of MITM requests handled by the proxy. | `provider` |
| `coder_aibridgeproxyd_mitm_responses_total` | counter | Total number of MITM responses by HTTP status code class. | `code` `provider` |
| `coder_pubsub_connected` | gauge | Whether we are connected (1) or not connected (0) to postgres | |
| `coder_pubsub_current_events` | gauge | The current number of pubsub event channels listened for | |
| `coder_pubsub_current_subscribers` | gauge | The current number of active pubsub subscribers | |
| `coder_pubsub_disconnections_total` | counter | Total number of times we disconnected unexpectedly from postgres | |
| `coder_pubsub_latency_measure_errs_total` | counter | The number of pubsub latency measurement failures | |
| `coder_pubsub_latency_measures_total` | counter | The number of pubsub latency measurements | |
| `coder_pubsub_messages_total` | counter | Total number of messages received from postgres | `size` |
| `coder_pubsub_published_bytes_total` | counter | Total number of bytes successfully published across all publishes | |
| `coder_pubsub_publishes_total` | counter | Total number of calls to Publish | `success` |
| `coder_pubsub_receive_latency_seconds` | gauge | The time taken to receive a message from a pubsub event channel | |
| `coder_pubsub_received_bytes_total` | counter | Total number of bytes received across all messages | |
| `coder_pubsub_send_latency_seconds` | gauge | The time taken to send a message into a pubsub event channel | |
| `coder_pubsub_subscribes_total` | counter | Total number of calls to Subscribe/SubscribeWithErr | `success` |
| `coder_servertailnet_connections_total` | counter | Total number of TCP connections made to workspace agents. | `network` |
| `coder_servertailnet_open_connections` | gauge | Total number of TCP connections currently open to workspace agents. | `network` |
| `coderd_agentapi_metadata_batch_size` | histogram | Total number of metadata entries in each batch, updated before flushes. | |
| `coderd_agentapi_metadata_batch_utilization` | histogram | Number of metadata keys per agent in each batch, updated before flushes. | |
| `coderd_agentapi_metadata_batches_total` | counter | Total number of metadata batches flushed. | `reason` |
| `coderd_agentapi_metadata_dropped_keys_total` | counter | Total number of metadata keys dropped due to capacity limits. | |
| `coderd_agentapi_metadata_flush_duration_seconds` | histogram | Time taken to flush metadata batch to database and pubsub. | `reason` |
| `coderd_agentapi_metadata_flushed_total` | counter | Total number of unique metadatas flushed. | |
| `coderd_agentapi_metadata_publish_errors_total` | counter | Total number of metadata batch pubsub publish calls that have resulted in an error. | |
| `coderd_agents_apps` | gauge | Agent applications with statuses. | `agent_name` `app_name` `health` `username` `workspace_name` |
| `coderd_agents_connection_latencies_seconds` | gauge | Agent connection latencies in seconds. | `agent_name` `derp_region` `preferred` `username` `workspace_name` |
| `coderd_agents_connections` | gauge | Agent connections with statuses. | `agent_name` `lifecycle_state` `status` `tailnet_node` `username` `workspace_name` |
| `coderd_agents_up` | gauge | The number of active agents per workspace. | `template_name` `template_version` `username` `workspace_name` |
| `coderd_agentstats_connection_count` | gauge | The number of established connections by agent | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_connection_median_latency_seconds` | gauge | The median agent connection latency | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_currently_reachable_peers` | gauge | The number of peers (e.g. clients) that are currently reachable over the encrypted network. | `agent_name` `connection_type` `template_name` `username` `workspace_name` |
| `coderd_agentstats_rx_bytes` | gauge | Agent Rx bytes | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_jetbrains` | gauge | The number of session established by JetBrains | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_reconnecting_pty` | gauge | The number of session established by reconnecting PTY | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_ssh` | gauge | The number of session established by SSH | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_session_count_vscode` | gauge | The number of session established by VSCode | `agent_name` `username` `workspace_name` |
| `coderd_agentstats_startup_script_seconds` | gauge | The number of seconds the startup script took to execute. | `agent_name` `success` `template_name` `username` `workspace_name` |
| `coderd_agentstats_tx_bytes` | gauge | Agent Tx bytes | `agent_name` `username` `workspace_name` |
| `coderd_api_active_users_duration_hour` | gauge | The number of users that have been active within the last hour. | |
| `coderd_api_concurrent_requests` | gauge | The number of concurrent API requests. | `method` `path` |
| `coderd_api_concurrent_websockets` | gauge | The total number of concurrent API websockets. | `path` |
| `coderd_api_request_latencies_seconds` | histogram | Latency distribution of requests in seconds. | `method` `path` |
| `coderd_api_requests_processed_total` | counter | The total number of processed API requests | `code` `method` `path` |
| `coderd_api_total_user_count` | gauge | The total number of registered users, partitioned by status. | `status` |
| `coderd_api_websocket_durations_seconds` | histogram | Websocket duration distribution of requests in seconds. | `path` |
| `coderd_api_workspace_latest_build` | gauge | The current number of workspace builds by status for all non-deleted workspaces. | `status` |
| `coderd_authz_authorize_duration_seconds` | histogram | Duration of the 'Authorize' call in seconds. Only counts calls that succeed. | `allowed` |
| `coderd_authz_prepare_authorize_duration_seconds` | histogram | Duration of the 'PrepareAuthorize' call in seconds. | |
| `coderd_db_query_counts_total` | counter | Total number of queries labelled by HTTP route, method, and query name. | `method` `query` `route` |
| `coderd_db_query_latencies_seconds` | histogram | Latency distribution of queries in seconds. | `query` |
| `coderd_db_tx_duration_seconds` | histogram | Duration of transactions in seconds. | `success` `tx_id` |
| `coderd_db_tx_executions_count` | counter | Total count of transactions executed. 'retries' is expected to be 0 for a successful transaction. | `retries` `success` `tx_id` |
| `coderd_dbpurge_iteration_duration_seconds` | histogram | Duration of each dbpurge iteration in seconds. | `success` |
| `coderd_dbpurge_records_purged_total` | counter | Total number of records purged by type. | `record_type` |
| `coderd_experiments` | gauge | Indicates whether each experiment is enabled (1) or not (0) | `experiment` |
| `coderd_insights_applications_usage_seconds` | gauge | The application usage per template. | `application_name` `slug` `template_name` |
| `coderd_insights_parameters` | gauge | The parameter usage per template. | `parameter_name` `parameter_type` `parameter_value` `template_name` |
| `coderd_insights_templates_active_users` | gauge | The number of active users of the template. | `template_name` |
| `coderd_license_active_users` | gauge | The number of active users. | |
| `coderd_license_errors` | gauge | The number of active license errors. | |
| `coderd_license_limit_users` | gauge | The user seats limit based on the active Coder license. | |
| `coderd_license_user_limit_enabled` | gauge | Returns 1 if the current license enforces the user limit. | |
| `coderd_license_warnings` | gauge | The number of active license warnings. | |
| `coderd_lifecycle_autobuild_execution_duration_seconds` | histogram | Duration of each autobuild execution. | |
| `coderd_notifications_dispatcher_send_seconds` | histogram | The time taken to dispatch notifications. | `method` |
| `coderd_notifications_inflight_dispatches` | gauge | The number of dispatch attempts which are currently in progress. | `method` `notification_template_id` |
| `coderd_notifications_pending_updates` | gauge | The number of dispatch attempt results waiting to be flushed to the store. | |
| `coderd_notifications_queued_seconds` | histogram | The time elapsed between a notification being enqueued in the store and retrieved for dispatching (measures the latency of the notifications system). This should generally be within CODER_NOTIFICATIONS_FETCH_INTERVAL seconds; higher values for a sustained period indicates delayed processing and CODER_NOTIFICATIONS_LEASE_COUNT can be increased to accommodate this. | `method` |
| `coderd_notifications_retry_count` | counter | The count of notification dispatch retry attempts. | `method` `notification_template_id` |
| `coderd_notifications_synced_updates_total` | counter | The number of dispatch attempt results flushed to the store. | |
| `coderd_oauth2_external_requests_rate_limit` | gauge | The total number of allowed requests per interval. | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_next_reset_unix` | gauge | Unix timestamp for when the next interval starts | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_remaining` | gauge | The remaining number of allowed requests in this interval. | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_reset_in_seconds` | gauge | Seconds until the next interval | `name` `resource` |
| `coderd_oauth2_external_requests_rate_limit_used` | gauge | The number of requests made in this interval. | `name` `resource` |
| `coderd_oauth2_external_requests_total` | counter | The total number of api calls made to external oauth2 providers. 'status_code' will be 0 if the request failed with no response. | `name` `source` `status_code` |
| `coderd_open_file_refs_current` | gauge | The count of file references currently open in the file cache. Multiple references can be held for the same file. | |
| `coderd_open_file_refs_total` | counter | The total number of file references ever opened in the file cache. The 'hit' label indicates if the file was loaded from the cache. | `hit` |
| `coderd_open_files_current` | gauge | The count of unique files currently open in the file cache. | |
| `coderd_open_files_size_bytes_current` | gauge | The current amount of memory of all files currently open in the file cache. | |
| `coderd_open_files_size_bytes_total` | counter | The total amount of memory ever opened in the file cache. This number never decrements. | |
| `coderd_open_files_total` | counter | The total count of unique files ever opened in the file cache. | |
| `coderd_prebuilds_reconciliation_duration_seconds` | histogram | Duration of each prebuilds reconciliation cycle. | |
| `coderd_prebuilt_workspace_claim_duration_seconds` | histogram | Time to claim a prebuilt workspace by organization, template, and preset. | `organization_name` `preset_name` `template_name` |
| `coderd_prebuilt_workspaces_claimed_total` | counter | Total number of prebuilt workspaces which were claimed by users. Claiming refers to creating a workspace with a preset selected for which eligible prebuilt workspaces are available and one is reassigned to a user. | `organization_name` `preset_name` `template_name` |
| `coderd_prebuilt_workspaces_created_total` | counter | Total number of prebuilt workspaces that have been created to meet the desired instance count of each template preset. | `organization_name` `preset_name` `template_name` |
| `coderd_prebuilt_workspaces_desired` | gauge | Target number of prebuilt workspaces that should be available for each template preset. | `organization_name` `preset_name` `template_name` |
| `coderd_prebuilt_workspaces_eligible` | gauge | Current number of prebuilt workspaces that are eligible to be claimed by users. These are workspaces that have completed their build process with their agent reporting 'ready' status. | `organization_name` `preset_name` `template_name` |
| `coderd_prebuilt_workspaces_failed_total` | counter | Total number of prebuilt workspaces that failed to build. | `organization_name` `preset_name` `template_name` |
| `coderd_prebuilt_workspaces_metrics_last_updated` | gauge | The unix timestamp when the metrics related to prebuilt workspaces were last updated; these metrics are cached. | |
| `coderd_prebuilt_workspaces_preset_hard_limited` | gauge | Indicates whether a given preset has reached the hard failure limit (1 = hard-limited). Metric is omitted otherwise. | `organization_name` `preset_name` `template_name` |
| `coderd_prebuilt_workspaces_reconciliation_paused` | gauge | Indicates whether prebuilds reconciliation is currently paused (1 = paused, 0 = not paused). | |
| `coderd_prebuilt_workspaces_resource_replacements_total` | counter | Total number of prebuilt workspaces whose resource(s) got replaced upon being claimed. In Terraform, drift on immutable attributes results in resource replacement. This represents a worst-case scenario for prebuilt workspaces because the pre-provisioned resource would have been recreated when claiming, thus obviating the point of pre-provisioning. See https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces#preventing-resource-replacement | `organization_name` `preset_name` `template_name` |
| `coderd_prebuilt_workspaces_running` | gauge | Current number of prebuilt workspaces that are in a running state. These workspaces have started successfully but may not yet be claimable by users (see coderd_prebuilt_workspaces_eligible). | `organization_name` `preset_name` `template_name` |
| `coderd_prometheusmetrics_agents_execution_seconds` | histogram | Histogram for duration of agents metrics collection in seconds. | |
| `coderd_prometheusmetrics_agentstats_execution_seconds` | histogram | Histogram for duration of agent stats metrics collection in seconds. | |
| `coderd_prometheusmetrics_metrics_aggregator_execution_cleanup_seconds` | histogram | Histogram for duration of metrics aggregator cleanup in seconds. | |
| `coderd_prometheusmetrics_metrics_aggregator_execution_update_seconds` | histogram | Histogram for duration of metrics aggregator update in seconds. | |
| `coderd_prometheusmetrics_metrics_aggregator_store_size` | gauge | The number of metrics stored in the aggregator | |
| `coderd_provisioner_job_queue_wait_seconds` | histogram | Time from job creation to acquisition by a provisioner daemon. | `build_reason` `job_type` `provisioner_type` `transition` |
| `coderd_provisionerd_job_timings_seconds` | histogram | The provisioner job time duration in seconds. | `provisioner` `status` |
| `coderd_provisionerd_jobs_current` | gauge | The number of currently running provisioner jobs. | `provisioner` |
| `coderd_provisionerd_num_daemons` | gauge | The number of provisioner daemons. | |
| `coderd_provisionerd_workspace_build_timings_seconds` | histogram | The time taken for a workspace to build. | `status` `template_name` `template_version` `workspace_transition` |
| `coderd_proxyhealth_health_check_duration_seconds` | histogram | Histogram for duration of proxy health collection in seconds. | |
| `coderd_proxyhealth_health_check_results` | gauge | This endpoint returns a number to indicate the health status. -3 (unknown), -2 (Unreachable), -1 (Unhealthy), 0 (Unregistered), 1 (Healthy) | `proxy_id` |
| `coderd_template_workspace_build_duration_seconds` | histogram | Duration from workspace build creation to agent ready, by template. | `is_prebuild` `organization_name` `status` `template_name` `transition` |
| `coderd_workspace_builds_enqueued_total` | counter | Total number of workspace build enqueue attempts. | `build_reason` `provisioner_type` `status` `transition` |
| `coderd_workspace_builds_total` | counter | The number of workspaces started, updated, or deleted. | `status` `template_name` `template_version` `workspace_name` `workspace_owner` `workspace_transition` |
| `coderd_workspace_creation_duration_seconds` | histogram | Time to create a workspace by organization, template, preset, and type (regular or prebuild). | `organization_name` `preset_name` `template_name` `type` |
| `coderd_workspace_creation_total` | counter | Total regular (non-prebuilt) workspace creations by organization, template, and preset. | `organization_name` `preset_name` `template_name` |
| `coderd_workspace_latest_build_status` | gauge | The current workspace statuses by template, transition, and owner for all non-deleted workspaces. | `status` `template_name` `template_version` `workspace_owner` `workspace_transition` |
| `go_gc_duration_seconds` | summary | A summary of the pause duration of garbage collection cycles. | |
| `go_goroutines` | gauge | Number of goroutines that currently exist. | |
| `go_info` | gauge | Information about the Go environment. | `version` |
| `go_memstats_alloc_bytes` | gauge | Number of bytes allocated and still in use. | |
| `go_memstats_alloc_bytes_total` | counter | Total number of bytes allocated, even if freed. | |
| `go_memstats_buck_hash_sys_bytes` | gauge | Number of bytes used by the profiling bucket hash table. | |
| `go_memstats_frees_total` | counter | Total number of frees. | |
| `go_memstats_gc_sys_bytes` | gauge | Number of bytes used for garbage collection system metadata. | |
| `go_memstats_heap_alloc_bytes` | gauge | Number of heap bytes allocated and still in use. | |
| `go_memstats_heap_idle_bytes` | gauge | Number of heap bytes waiting to be used. | |
| `go_memstats_heap_inuse_bytes` | gauge | Number of heap bytes that are in use. | |
| `go_memstats_heap_objects` | gauge | Number of allocated objects. | |
| `go_memstats_heap_released_bytes` | gauge | Number of heap bytes released to OS. | |
| `go_memstats_heap_sys_bytes` | gauge | Number of heap bytes obtained from system. | |
| `go_memstats_last_gc_time_seconds` | gauge | Number of seconds since 1970 of last garbage collection. | |
| `go_memstats_lookups_total` | counter | Total number of pointer lookups. | |
| `go_memstats_mallocs_total` | counter | Total number of mallocs. | |
| `go_memstats_mcache_inuse_bytes` | gauge | Number of bytes in use by mcache structures. | |
| `go_memstats_mcache_sys_bytes` | gauge | Number of bytes used for mcache structures obtained from system. | |
| `go_memstats_mspan_inuse_bytes` | gauge | Number of bytes in use by mspan structures. | |
| `go_memstats_mspan_sys_bytes` | gauge | Number of bytes used for mspan structures obtained from system. | |
| `go_memstats_next_gc_bytes` | gauge | Number of heap bytes when next garbage collection will take place. | |
| `go_memstats_other_sys_bytes` | gauge | Number of bytes used for other system allocations. | |
| `go_memstats_stack_inuse_bytes` | gauge | Number of bytes in use by the stack allocator. | |
| `go_memstats_stack_sys_bytes` | gauge | Number of bytes obtained from system for stack allocator. | |
| `go_memstats_sys_bytes` | gauge | Number of bytes obtained from system. | |
| `go_threads` | gauge | Number of OS threads created. | |
| `process_cpu_seconds_total` | counter | Total user and system CPU time spent in seconds. | |
| `process_max_fds` | gauge | Maximum number of open file descriptors. | |
| `process_open_fds` | gauge | Number of open file descriptors. | |
| `process_resident_memory_bytes` | gauge | Resident memory size in bytes. | |
| `process_start_time_seconds` | gauge | Start time of the process since unix epoch in seconds. | |
| `process_virtual_memory_bytes` | gauge | Virtual memory size in bytes. | |
| `process_virtual_memory_max_bytes` | gauge | Maximum amount of virtual memory available in bytes. | |
| `promhttp_metric_handler_requests_in_flight` | gauge | Current number of scrapes being served. | |
| `promhttp_metric_handler_requests_total` | counter | Total number of scrapes by HTTP status code. | `code` |
<!-- End generated by 'make docs/admin/integrations/prometheus.md'. -->
+26 -26
View File
@@ -13,32 +13,32 @@ We track the following resources:
<!-- Code generated by 'make docs/admin/security/audit-logs.md'. DO NOT EDIT -->
| <b>Resource<b> | | |
|----------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>allow_list</td><td>false</td></tr><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scopes</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_system</td><td>false</td></tr><tr><td>member_permissions</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| GroupSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>auto_create_missing_groups</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>legacy_group_name_mapping</td><td>false</td></tr><tr><td>mapping</td><td>true</td></tr><tr><td>regex_filter</td><td>true</td></tr></tbody></table> |
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
| NotificationTemplate<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>actions</td><td>true</td></tr><tr><td>body_template</td><td>true</td></tr><tr><td>enabled_by_default</td><td>true</td></tr><tr><td>group</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>kind</td><td>true</td></tr><tr><td>method</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>title_template</td><td>true</td></tr></tbody></table> |
| NotificationsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>notifier_paused</td><td>true</td></tr></tbody></table> |
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>callback_url</td><td>true</td></tr><tr><td>client_id_issued_at</td><td>false</td></tr><tr><td>client_secret_expires_at</td><td>true</td></tr><tr><td>client_type</td><td>true</td></tr><tr><td>client_uri</td><td>true</td></tr><tr><td>contacts</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamically_registered</td><td>true</td></tr><tr><td>grant_types</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwks</td><td>true</td></tr><tr><td>jwks_uri</td><td>true</td></tr><tr><td>logo_uri</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>policy_uri</td><td>true</td></tr><tr><td>redirect_uris</td><td>true</td></tr><tr><td>registration_access_token</td><td>true</td></tr><tr><td>registration_client_uri</td><td>true</td></tr><tr><td>response_types</td><td>true</td></tr><tr><td>scope</td><td>true</td></tr><tr><td>software_id</td><td>true</td></tr><tr><td>software_version</td><td>true</td></tr><tr><td>token_endpoint_auth_method</td><td>true</td></tr><tr><td>tos_uri</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
| Organization<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>workspace_sharing_disabled</td><td>true</td></tr></tbody></table> |
| OrganizationSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>assign_default</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
| PrebuildsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>reconciliation_paused</td><td>true</td></tr></tbody></table> |
| RoleSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
| TaskTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>prompt</td><td>true</td></tr><tr><td>template_parameters</td><td>true</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>workspace_id</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>cors_behavior</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>disable_module_cache</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_classic_parameter_flow</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| <b>Resource<b> | | |
|-----------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| APIKey<br><i>login, logout, register, create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>allow_list</td><td>false</td></tr><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scopes</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
| AuditableOrganizationMember<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>roles</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| CustomRole<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_system</td><td>false</td></tr><tr><td>member_permissions</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>org_permissions</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>site_permissions</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_permissions</td><td>true</td></tr></tbody></table> |
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| GroupSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>auto_create_missing_groups</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>legacy_group_name_mapping</td><td>false</td></tr><tr><td>mapping</td><td>true</td></tr><tr><td>regex_filter</td><td>true</td></tr></tbody></table> |
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
| NotificationTemplate<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>actions</td><td>true</td></tr><tr><td>body_template</td><td>true</td></tr><tr><td>enabled_by_default</td><td>true</td></tr><tr><td>group</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>kind</td><td>true</td></tr><tr><td>method</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>title_template</td><td>true</td></tr></tbody></table> |
| NotificationsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>notifier_paused</td><td>true</td></tr></tbody></table> |
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>callback_url</td><td>true</td></tr><tr><td>client_id_issued_at</td><td>false</td></tr><tr><td>client_secret_expires_at</td><td>true</td></tr><tr><td>client_type</td><td>true</td></tr><tr><td>client_uri</td><td>true</td></tr><tr><td>contacts</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamically_registered</td><td>true</td></tr><tr><td>grant_types</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwks</td><td>true</td></tr><tr><td>jwks_uri</td><td>true</td></tr><tr><td>logo_uri</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>policy_uri</td><td>true</td></tr><tr><td>redirect_uris</td><td>true</td></tr><tr><td>registration_access_token</td><td>true</td></tr><tr><td>registration_client_uri</td><td>true</td></tr><tr><td>response_types</td><td>true</td></tr><tr><td>scope</td><td>true</td></tr><tr><td>software_id</td><td>true</td></tr><tr><td>software_version</td><td>true</td></tr><tr><td>token_endpoint_auth_method</td><td>true</td></tr><tr><td>tos_uri</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
| Organization<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr><tr><td>workspace_sharing_disabled</td><td>true</td></tr></tbody></table> |
| OrganizationSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>assign_default</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
| PrebuildsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>reconciliation_paused</td><td>true</td></tr></tbody></table> |
| RoleSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>field</td><td>true</td></tr><tr><td>mapping</td><td>true</td></tr></tbody></table> |
| TaskTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>deleted_at</td><td>false</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>prompt</td><td>true</td></tr><tr><td>template_parameters</td><td>true</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>workspace_id</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>cors_behavior</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>disable_module_cache</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>use_classic_parameter_flow</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
<!-- End generated by 'make docs/admin/security/audit-logs.md'. -->
@@ -4,11 +4,11 @@ Dev containers extend your template with containerized development environments,
allowing developers to work in consistent, reproducible setups defined by
`devcontainer.json` files.
Coder's Dev Containers Integration uses the standard `@devcontainers/cli` and
Coder's main Dev Containers Integration uses the standard `@devcontainers/cli` and
Docker to run containers inside workspaces.
For setup instructions, see
[Dev Containers Integration](../../integrations/devcontainers/integration.md).
For an alternative approach that doesn't require Docker, see
[Envbuilder](../../integrations/devcontainers/envbuilder/index.md).
For alternative approaches and comparisons between them, see the
[Dev Containers](../../integrations/devcontainers/index.md) page.
@@ -54,21 +54,31 @@ For a full list of available modules please check
## Offline installations
In offline and restricted deployments, there are two ways to fetch modules.
In offline and restricted deployments, there are three ways to fetch modules.
1. Artifactory
2. Private git repository
1. Artifactory Remote Terraform Repository (Recommended)
2. Artifactory Local Repository (manual publishing)
3. Private git repository
### Artifactory
### Artifactory Remote Terraform Repository (Recommended)
Air gapped users can clone the [coder/registry](https://github.com/coder/registry/)
Configure Artifactory as a **Remote Terraform Repository** that proxies and
caches the Coder registry. This approach provides automatic updates and
requires no manual synchronization.
See [Mirror the Coder Registry with JFrog Artifactory](../../../install/registry-mirror-artifactory.md)
for complete setup instructions.
### Artifactory Local Repository
Air-gapped users can clone the [coder/registry](https://github.com/coder/registry/)
repo and publish a
[local terraform module repository](https://jfrog.com/help/r/jfrog-artifactory-documentation/set-up-a-terraform-module/provider-registry)
to resolve modules via [Artifactory](https://jfrog.com/artifactory/).
1. Create a local-terraform-repository with name `coder-modules-local`
2. Create a virtual repository with name `tf`
3. Follow the below instructions to publish coder modules to Artifactory
1. Create a virtual repository with name `tf`
1. Follow the below instructions to publish coder modules to Artifactory
```shell
git clone https://github.com/coder/registry
@@ -77,9 +87,9 @@ to resolve modules via [Artifactory](https://jfrog.com/artifactory/).
jf tf p --namespace="coder" --provider="coder" --tag="1.0.0"
```
4. Generate a token with access to the `tf` repo and set an `ENV` variable
1. Generate a token with access to the `tf` repo and set an `ENV` variable
`TF_TOKEN_example.jfrog.io="XXXXXXXXXXXXXXX"` on the Coder provisioner.
5. Create a file `.terraformrc` with following content and mount at
1. Create a file `.terraformrc` with following content and mount at
`/home/coder/.terraformrc` within the Coder provisioner.
```tf
@@ -93,7 +103,7 @@ to resolve modules via [Artifactory](https://jfrog.com/artifactory/).
}
```
6. Update module source as:
1. Update module source as:
```tf
module "module-name" {
+19
View File
@@ -115,6 +115,25 @@ specified in your template in the `disable_params` search params list
[![Open in Coder](https://YOUR_ACCESS_URL/open-in-coder.svg)](https://YOUR_ACCESS_URL/templates/YOUR_TEMPLATE/workspace?disable_params=first_parameter,second_parameter)
```
### Security: consent dialog for automatic creation
When using `mode=auto` with prefilled `param.*` values, Coder displays a
security consent dialog before creating the workspace. This protects users
from malicious links that could provision workspaces with untrusted
configurations, such as dotfiles or startup scripts from unknown sources.
The dialog shows:
- A warning that a workspace is about to be created automatically from a link
- All prefilled `param.*` values from the URL
- **Confirm and Create** and **Cancel** buttons
The workspace is only created if the user explicitly clicks **Confirm and
Create**. Clicking **Cancel** falls back to the standard creation form where
all parameters can be reviewed manually.
![Consent dialog for automatic workspace creation](../../images/templates/auto-create-consent-dialog.png)
### Example: Kubernetes
For a full example of the Open in Coder flow in Kubernetes, check out
+18
View File
@@ -96,6 +96,24 @@ You can use the
server flag to set the maximum duration for long-lived tokens in your
deployment.
### Remove or expire a token
You can remove a token using the CLI or the API. By default, `coder tokens remove`
expires the token, (soft-delete):
```console
coder tokens remove <name|id>
```
Expired tokens can no longer be used for authentication but remain visible in
token listings.
To hard-delete a token, use the `--delete` flag:
```console
coder tokens remove --delete <name|id>
```
## API Key Scopes
API key scopes allow you to limit the permissions of a token to specific operations. By default, tokens are created with the `all` scope, granting full access to all actions the user can perform. For improved security, you can create tokens with limited scopes that restrict access to only the operations needed.

Some files were not shown because too many files have changed in this diff Show More