Compare commits

..

475 Commits

Author SHA1 Message Date
Stephen Kirby 1a027b0774 chore: sign the windows installer (#14353) (#14368)
(cherry picked from commit 6f9b3c1592)

Co-authored-by: Kyle Carberry <kyle@coder.com>
2024-08-20 13:20:54 -05:00
Stephen Kirby bddf0bf85a chore: update emoji-mart data (#13746) (#14189)
(cherry picked from commit 4a0fd7466c)

Co-authored-by: Kayla Washburn-Love <mckayla@hey.com>
2024-08-06 11:58:37 -05:00
Stephen Kirby d4f8a6481a fix: change time format string from 15:40 to 15:04 (#14033) (#14072)
* Change string format to constant value

(cherry picked from commit eacdfb9f9c)

Co-authored-by: Charlie Voiselle <464492+angrycub@users.noreply.github.com>
2024-08-01 12:34:10 -05:00
Stephen Kirby 48c4859942 chore(scripts): fix cherry-pick check in check_commit_metadata.sh (#13980) (#13994)
(cherry picked from commit 5a4dbcfc02)

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-07-23 15:08:32 -05:00
Stephen Kirby 5b120203b2 chore: keep active users active in scim (#13955) (#13973)
* chore: scim should keep active users active
* chore: add a unit test to excercise dormancy bug
* audit log should not be dropped when there is no change
* add ability to cancel audit log

(cherry picked from commit 03c5d42233)

Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
2024-07-22 16:22:01 -05:00
Stephen Kirby 5edfccf30d chore: patch 2.13.1 (#13927)
* fix: let workspace pages download partial logs for unhealthy workspaces (#13761)

* fix: get basic fix in for  preventing download logs from blowing up UI

* fix: make sure blob units can't go out of bounds

* fix: make sure timeout is cleared on component unmount

* fix: reduce risk of shared cache state breaking useAgentLogs

* fix: allow partial downloading of logs

* fix: make sure useMemo cache is used properly

* wip: commit current progress on updated logs functionality

* docs: rewrite comment for clarity

* refactor: clean up current code

* fix: update styles for unavailable logs

* fix: resolve linter violations

* fix: update type signature of getErrorDetail

* fix: revert log/enabled logic for useAgentLogs

* fix: remove memoization from DownloadLogsDialog

* fix: update name of timeout state

* refactor: make log web sockets logic more clear

* docs: reword comment for clarity

* fix: commit current style update progress

* fix: finish style updates

(cherry picked from commit 940afa1ab1)

* fix(site): enable dormant workspace to be deleted (#13850)

(cherry picked from commit 01b30eaa32)

* chore: remove `organizationIds` from `AuthProvider` (#13917)

(cherry picked from commit 80cbffe843)

---------

Co-authored-by: Michael Smith <throwawayclover@gmail.com>
Co-authored-by: Bruno Quaresma <bruno@coder.com>
Co-authored-by: Kayla Washburn-Love <mckayla@hey.com>
2024-07-18 16:18:09 -05:00
Mathias Fredriksson 56bf386b15 ci: remove release make concurrency to fix docker image race (#13769)
(cherry picked from commit a114288ef2)
2024-07-02 17:54:06 +00:00
Cian Johnston c620bffbb3 ci: use postgres version 13 to test migrations (#13767)
(cherry picked from commit 5ea5db29e9)
2024-07-02 17:04:33 +00:00
Michael Smith 84d992062b chore: add SVG desktop icon (#13765)
* chore: add SVG desktop icon

* fix: add desktop icon to to icons.json

(cherry picked from commit 21a923a7a0)
2024-07-02 16:02:26 +00:00
Steven Masley 6daf330d3a chore: allow organization name or uuid for audit log searching (#13721)
* chore: allow organization name or uuid for audit log searching
2024-06-28 10:01:23 -05:00
Steven Masley 3cc86cf62d chore: implement sane default pagination limit for audit logs (#13676)
* chore: implement sane default pagination limit for audit logs
2024-06-28 07:38:04 -05:00
Cian Johnston 1a877716ca chore: add envbuilder-dogfood template (#13720)
Adds a template that can be used to dogfood on envbuilder!
2024-06-28 12:56:22 +01:00
Danny Kopping 0a221e8d5b feat: create database tables and queries for notifications (#13536) 2024-06-28 09:21:25 +00:00
dependabot[bot] 4213560b7a chore: bump @mui/icons-material from 5.14.0 to 5.15.20 in /site (#13703)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-28 03:49:56 +03:00
dependabot[bot] 2a21b0d144 ci: bump toshimaru/auto-author-assign in the github-actions group (#13696)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-28 03:48:01 +03:00
dependabot[bot] 86ee75b672 chore: bump undici from 6.11.1 to 6.19.2 in /site (#13699)
Bumps [undici](https://github.com/nodejs/undici) from 6.11.1 to 6.19.2.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.11.1...v6.19.2)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-27 12:43:24 -06:00
dependabot[bot] 0b8b227dcf chore: bump emoji-mart from 5.4.0 to 5.6.0 in /site (#13709)
Bumps [emoji-mart](https://github.com/missive/emoji-mart/tree/HEAD/packages/emoji-mart) from 5.4.0 to 5.6.0.
- [Release notes](https://github.com/missive/emoji-mart/releases)
- [Commits](https://github.com/missive/emoji-mart/commits/v5.6.0/packages/emoji-mart)

---
updated-dependencies:
- dependency-name: emoji-mart
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-27 12:41:09 -06:00
Muhammad Atif Ali bda94bfc77 chore: optimize dependabot configuration (#13670) 2024-06-27 21:14:17 +03:00
Kayla Washburn-Love 8b615f4522 fix: disable agent app buttons while a blocking startup script is running (#13667) 2024-06-27 11:25:03 -06:00
Jaayden Halko 093ec3d05b fix: improve checkbox text in template schedule settings dialog (#13669)
* fix: improve checkbox text in template schedule settings dialog

* fix: format

* fix: remove (s) plural language

* fix: fix format
2024-06-27 13:14:06 -04:00
Steven Masley 5a0afd8b7e chore: revert to default survey templates (#13690)
Revert to the default template provided by the package. Includes all fields like the "Message" field. Our template omits headers and keybind helpers.
2024-06-27 05:32:12 -10:00
Steven Masley 22f2c6da4f chore: add command to render all existing cli prompts (#13689)
Adds a command to render all the existing cli prompts we use. This is to validate a change to how our cli ui prompts look.
2024-06-27 05:20:15 -10:00
Spike Curtis ce7f13c6c3 fix: fix TestPGCoordinatorSingle_MissedHeartbeats flake (#13686) 2024-06-27 19:17:24 +04:00
Muhammad Atif Ali 089f06886b chore: add .pnpm-store to .gitignore (#13685) 2024-06-27 15:07:04 +03:00
Spike Curtis c94b5188bd fix: modify workspacesdk to ask for tailnet API 2.0 (#13684)
#13617 bumped the Agent/Tailnet API minor version because it adds telemetry features.  However, we don't actually use the protocol features yet, so it's a bit obnoxious for our CLI client to ask for the newest API version.

This is particularly true of the CLI client, since that's distributed separately, so if an end user installs the latest CLI client and their organization hasn't fully upgraded, then it will fail to connect.

Since we have a release coming up and the telemetry stuff won't make it, I think we should roll back to version 2.0 until we actually implement the telemetry stuff. That way the newest release (2.13) will work with Coder servers all the way back to 2.9.
2024-06-27 15:38:21 +04:00
Spike Curtis 5b59f2880f fix: fix workspacesdk to return error on API mismatch (#13683) 2024-06-27 15:02:43 +04:00
Marcin Tojek c4f1676055 feat: expose workspace build ID to terraform-plugin-coder (#13680) 2024-06-27 10:07:30 +02:00
Steven Masley 30c4b4db5c chore: implement fetch all authorized templates api (#13678) 2024-06-26 11:50:32 -06:00
Steven Masley 08e728bcb2 chore: implement organization scoped audit log requests (#13663)
* chore: add organization_id filter to audit logs
* chore: implement organization scoped audit log requests
2024-06-26 12:38:46 -05:00
Cian Johnston 20e59e0797 ci: test with multiple postgres versions (#13665)
- Tests now run on postgres 16 by default when run locally (can be specified with POSTGRES_VERSION)
- Adds test-go-pg-16 to test against postgres version 16
- Updates Dogfood dockerfile / nix flake to postgres version 16
- Updates docker-compose.yaml postgres tag to 16
2024-06-26 16:22:24 +01:00
Danny Kopping d5d8b918d7 feat: lint github actions workflows (#13552) 2024-06-26 10:28:16 +02:00
Cian Johnston 8a3592582b feat: add "Full Name" field to user creation (#13659)
Adds the ability to specify "Full Name" (a.k.a. Name) when
creating users either via CLI or UI.
2024-06-26 09:00:42 +01:00
austinrhode 87ad560aff feat: add groups and group members to telemetry snapshot (#13655)
* feat: Added in groups and groups members to telemetry snapshot
* feat: adding in test to dbauthz for getting group members and groups
2024-06-25 11:01:40 -07:00
Muhammad Atif Ali 58325dfd14 chore: minor improvements and link updates in README.md (#13656)
* chore: minor improvements and link updates in README.md

* fixup!
2024-06-25 13:29:08 -04:00
Garrett Delfosse fed668b432 chore: switch ssh session stats based on experiment (#13637) 2024-06-25 10:58:45 -04:00
Steven Masley d7eadee4d7 chore: insert audit log entries for organization CRUD (#13660)
* chore: insert audit log entries for organization CRUD
2024-06-25 09:03:15 -05:00
Spike Curtis 9c1a6a29f2 feat: add docstrings to mock timer and ticker methods and structs (#13658) 2024-06-25 16:59:35 +04:00
Spike Curtis 46e1c36c42 feat: add Next() method to mock Clock (#13657) 2024-06-25 16:48:26 +04:00
Spike Curtis 0d2f14606b chore: add usage information to clock library README (#13594)
Adds a Usage section to the README of the clock testing library.
2024-06-25 16:38:32 +04:00
Muhammad Atif Ali 136900268e ci: migrate to depot.dev runners (#13467) 2024-06-25 09:36:33 +03:00
Cian Johnston 313d4e02d2 chore(scaletest/dashboard): stub out initChromeDPCtx in unit tests (#13650) 2024-06-24 21:33:24 +01:00
Steven Masley 65b9f9bfd6 chore: audit organization member add/delete/edit (#13620)
* chore: audit organization member add/removals
2024-06-24 14:19:32 -05:00
Stephen Kirby 94639730f8 docs: bump mainline version to v2.12.3 (#13652)
* docs: bump mainline version to v2.12.3

* updated stable version
2024-06-24 14:07:42 -05:00
Steven Masley 34c67e8428 test: add unit test helper to create templates in second organizations (#13628)
* chore: add coderdtest helpers
2024-06-24 12:55:57 -05:00
Steven Masley e4333c0433 chore: 'coder login' reset cli organization context (#13646)
Cli organization context is reset on `coder login` if the organization
selected is an invalid organization.
2024-06-24 12:55:39 -05:00
dependabot[bot] 8ccdf05bbc chore: bump github.com/aws/aws-sdk-go-v2 from 1.27.0 to 1.30.0 (#13643)
Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.27.0 to 1.30.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.27.0...v1.30.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 12:22:17 -05:00
dependabot[bot] 218f429336 chore: bump github.com/hashicorp/hcl/v2 from 2.20.0 to 2.21.0 (#13642)
Bumps [github.com/hashicorp/hcl/v2](https://github.com/hashicorp/hcl) from 2.20.0 to 2.21.0.
- [Release notes](https://github.com/hashicorp/hcl/releases)
- [Changelog](https://github.com/hashicorp/hcl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/hcl/compare/v2.20.0...v2.21.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/hcl/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 12:21:45 -05:00
dependabot[bot] 6f4a9b6b51 chore: bump github.com/valyala/fasthttp from 1.54.0 to 1.55.0 (#13640)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.54.0 to 1.55.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/1.54.0...v1.55.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 12:21:18 -05:00
Colin Adler 3dec6ff32f chore: add protobuf types for tailnet telemetry (#13617) 2024-06-24 12:13:03 -05:00
Stephen Kirby b9d83c75de fixed changelog script release channel flag (#13649) 2024-06-24 11:50:24 -05:00
dependabot[bot] 7e20b56352 chore: bump alpine from 3.20.0 to 3.20.1 in /scripts (#13644)
Bumps alpine from 3.20.0 to 3.20.1.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 11:48:07 -05:00
Stephen Kirby 3d6c9799e3 fixed script ref (#13647) 2024-06-24 11:27:13 -05:00
Cian Johnston b4a5c7ffa9 chore: upgrade Go version to 1.22.4 (#13623)
Updates Go version to 1.22.4

Co-authored-by: Muhammad Atif Ali <me@matifali.dev>
2024-06-24 15:50:52 +01:00
dependabot[bot] 7cb8bfb133 ci: bump the github-actions group with 2 updates (#13645)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 15:52:05 +03:00
Stephen Kirby 2cfadad023 manually updated autoversion (#13633) 2024-06-22 12:16:07 -05:00
Stephen Kirby 9abaa94599 docs: bump mainline version to v2.12.2 (#13632) 2024-06-21 15:11:58 -05:00
Kyle Carberry 54e8f30002 chore: remove failing_sections from healthcheck (#13426)
Closes #10854.
2024-06-21 14:49:02 -04:00
Steven Masley 5177f366f5 fix: organization 404 write 1 http status (#13629) 2024-06-21 13:01:46 -05:00
Steven Masley 0e933f0537 chore: refactor user -> rbac.subject into a function (#13624)
* chore: refactor user subject logic to be in 1 place
* test: implement test to assert deleted custom roles are omitted
* add unit test for deleted role
2024-06-21 11:30:02 -05:00
Kyle Carberry 3ef12ac284 fix: remove connected button (#13625)
It didn't make a lot of sense in current form. It will when we improve autostop.
2024-06-21 11:41:59 -04:00
Steven Masley 75e7213ac2 feat: add cli command to remove organization member (#13619) 2024-06-21 10:35:59 -05:00
Bruno Quaresma cbdaa63b68 chore(site): refactor popover to make it easier to extend (#13611) 2024-06-21 11:15:37 -03:00
Ethan 714f2ef83c fix: fix shallow clones not retrieving a valid semver (#13609) 2024-06-22 00:02:12 +10:00
Bruno Quaresma 73a25c3bc5 chore(site): add InputGroup component (#13597) 2024-06-21 10:54:55 -03:00
Steven Masley 819bfd3170 fix: remove assigning org-member role, this is implied from membership (#13578) 2024-06-21 08:01:39 -05:00
dependabot[bot] 66a604d779 chore: bump golang.org/x/tools from 0.21.0 to 0.22.0 (#13513)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-21 08:54:06 +03:00
Steven Masley 2ef2f97388 chore: improve error message on adding existing org_member (#13621) 2024-06-20 18:05:11 -05:00
Colin Adler 889daf200e feat(enterprise): add auditing to SCIM (#13614) 2024-06-20 17:22:27 -05:00
Steven Masley c4656d77cc chore: add help to error to reset organization context (#13616) 2024-06-20 16:44:47 -05:00
Kyle Carberry 495eea452f fix: track login page correctly (#13618) 2024-06-20 21:33:47 +00:00
Asher 43e45f4ab7 fix: fill out zero-value user properties in /audit (#13604) 2024-06-20 12:40:08 -08:00
Kyle Carberry 57b38e5bb8 fix: allow coder.com in CSP if telemetry is enabled (#13615)
* fix: allow coder.com in CSP if telemetry is enabled

* Fix control couple lint
2024-06-20 16:05:22 -04:00
Kyle Carberry 0793a4b35b feat: add cross-origin reporting for telemetry in the dashboard (#13612)
* feat: add cross-origin reporting for telemetry in the dashboard

* Respect the telemetry flag

* Fix embedded metadata

* Fix compilation error

* Fix linting
2024-06-20 15:19:45 -04:00
Steven Masley a1db6d809e chore: implement delete organization member (#13589)
Side effects of removing an organization member will orphan their
user resources. These side effects are not addressed here
2024-06-20 10:06:37 -05:00
Kelly Peilin Chan a1ec8ad6e9 Update docker-in-workspaces.md (#13606) 2024-06-20 11:05:21 -04:00
Steven Masley 8e06ad46d0 chore: add organization member api + cli (#13577) 2024-06-20 09:19:24 -05:00
Dean Sheather 4699adee5e chore: update dogfood sydney server (#13610) 2024-06-20 14:12:25 +00:00
Spike Curtis 8923ce5216 fix: fix flake in TestAppHealth_Healthy (#13607) 2024-06-20 12:02:31 +04:00
Spike Curtis 02ffff11dd feat: add NewTicker to clock testing library (#13593) 2024-06-20 10:16:04 +04:00
Kyle Carberry 7049d7a881 fix: display trial errors in the dashboard (#13601)
* fix: display trial errors in the dashboard

The error was essentially being ignored before!

* Remove day mention in product of trial

* fmt
2024-06-19 12:02:51 -04:00
dependabot[bot] 84cdcac8ad chore: bump ws from 8.14.2 to 8.17.1 in /site (#13595)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 10:51:25 +03:00
Kayla Washburn-Love e987ad1d89 fix: don't allow "new" or "create" as url-friendly names (#13596) 2024-06-18 15:36:13 -06:00
Kyle Carberry 3a1fa04590 fix: write server config to telemetry (#13590)
* fix: add external auth configs to telemetry

* Refactor telemetry to send the entire config

* gen

* Fix linting
2024-06-18 16:20:21 -04:00
Spike Curtis d0b2f6196c fix: allow mock clock Timers to accept negative duration (#13592)
The standard library `NewTimer`, `AfterFunc` and `Reset` allow negative durations, so our mock clock library should as well.
2024-06-18 15:40:56 +04:00
Spike Curtis 1de023a121 chore: add README to clock testing (#13583)
Adds README with some draft content explaining why the library exists.  Will be most relevant when we spin out into a standalone library.
2024-06-18 10:16:49 +04:00
Steven Masley 1d3642d0be chore: fix link in v2.0.0 changelog to scale tests (#13591) 2024-06-17 14:24:07 -05:00
Kayla Washburn-Love 8c1bd32c33 feat(site): add basic organization management ui (#13288) 2024-06-17 11:02:39 -06:00
Kayla Washburn-Love 07cd9acb2c fix: fix workspace actions options (#13572) 2024-06-17 10:24:30 -06:00
dependabot[bot] eed9794516 ci: bump crate-ci/typos in the github-actions group (#13584)
Bumps the github-actions group with 1 update: [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `crate-ci/typos` from 1.22.3 to 1.22.7
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.22.3...v1.22.7)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 09:03:26 -04:00
Ben Potter 808e1c0d89 docs: add screenshots page (#13582)
* docs: add screenshots page

* fmt
2024-06-15 15:34:18 -05:00
Garrett Delfosse 44d69139d5 chore: accept payload on workspace usage route (#13544) 2024-06-14 10:08:45 -04:00
Eric Paulsen 87820a29d7 docs: reorganize scaling docs (#13574)
* refactor scaling docs

* manifest

* make fmt

* fix 404s

* fix 404s pt 2

* fix manifest
2024-06-14 09:30:04 -04:00
Spike Curtis c01d6fdf46 chore: refactor apphealth and tests to use clock testing library (#13576)
Refactors the apphealth subsystem and unit tests to use `clock.Clock`.

Also slightly simplifies the implementation, which wrapped a function that never returned an error in a `retry.Retry`.  The retry is entirely superfluous in that case, so removed.

UTs used to take a few seconds to run, and now run in milliseconds or better.  No sleeps, `Eventually`, or polling.

Dropped the "no spamming" test since we can directly assert the number of handler calls on the mainline test case.
2024-06-14 15:06:33 +04:00
Cian Johnston fe240add86 fix(coderd): userOIDC: ignore leading @ of EmailDomain (#13568) 2024-06-14 09:29:07 +01:00
Steven Masley d04959cea8 chore: implement custom role assignment for organization admins (#13570)
* chore: static role assignment mapping

Until a dynamic approach is created in the database, only org-admins
can assign custom organization roles.
2024-06-13 15:59:06 -05:00
Steven Masley 3d30c8dc68 chore: protect reserved builtin rolenames (#13571)
Conflicting built-in and database role names makes it hard to
disambiguate
2024-06-13 15:12:37 -05:00
Steven Masley 7d51515f9d chore: implement assign organization roles from the cli (#13558)
Basic functionality to assign roles to an organization member via cli.
2024-06-13 14:49:32 -05:00
Eric Paulsen 87a172fb14 docs: add validated architecture (#13561)
* docs: add validated architecture

* make: fmt

* formatting

* fix 404s

* fix 404s pt 2

* fix 404s pt 3
2024-06-13 13:00:26 -04:00
Cian Johnston c587af7c0e fix(dogfood/Dockerfile): add explicit --chown to COPY directive (#13569) 2024-06-13 15:16:34 +01:00
Cian Johnston 5d3f3c08cd chore(dogfood): add devcontainer for use with envbuilder (#13567) 2024-06-13 14:31:49 +01:00
Spike Curtis 0268c7a659 chore: refactor autobuild/notify to use clock test (#13566)
Refactor autobuild/notify and tests to use the clock testing library.

I also rewrote some of the comments because I didn't understand them when I was looking at the package.
2024-06-13 16:01:17 +04:00
Spike Curtis 4b0b9b08d5 feat: add interfaces report to support bundle (#13563) 2024-06-13 13:09:54 +04:00
Spike Curtis 88eb6ce378 fix: fix flake in TestDERPEndToEnd (#13564) 2024-06-13 11:38:51 +04:00
Spike Curtis fc09077b7b feat!: add interface report to coder netcheck (#13562)
re: #13327

Adds local interfaces to `coder netcheck` and checks their MTUs for potential problems.

This is mostly relevant for end-user systems where VPNs are common.  We _could_ also add it to coderd healthcheck, but until I see coderd connecting to workspaces over a VPN in the wild, I don't think its worth the UX effort.

Netcheck results get the following:

```
  "interfaces": {
    "error": null,
    "severity": "ok",
    "warnings": null,
    "dismissed": false,
    "interfaces": [
      {
        "name": "lo0",
        "mtu": 16384,
        "addresses": [
          "127.0.0.1/8",
          "::1/128",
          "fe80::1/64"
        ]
      },
      {
        "name": "en8",
        "mtu": 1500,
        "addresses": [
          "192.168.50.217/24",
          "fe80::c13:1a92:3fa5:dd7e/64"
        ]
      }
    ]
  }
```

_Technically_ not back compatible if anyone is parsing `coder netcheck` output as JSON, since the original output is now under `"derp"` in the output.
2024-06-13 10:19:36 +04:00
Steven Masley d0fc81a51c chore: implement cli list organization members (#13555)
example cli command: 
`coder organization members`
2024-06-12 10:07:12 -10:00
Steven Masley bbe23edc7d chore: implement api layer for listing organization members (#13546) 2024-06-12 09:52:18 -10:00
Steven Masley de9e6889bb chore: merge organization member db queries (#13542)
Merge members queries into 1 that also joins in the user table for username.
Required to list organization members on UI/cli
2024-06-12 09:23:48 -10:00
Kyle Carberry 1ca5dc0328 chore: always use the latest released version tag when building (#13556)
* chore: always use the latest released version tag when building

* Update version.sh

Co-authored-by: Dean Sheather <dean@deansheather.com>

---------

Co-authored-by: Dean Sheather <dean@deansheather.com>
2024-06-12 14:52:35 -04:00
Kayla Washburn-Love 28228f1bcb feat: allow editing org icon (#13547) 2024-06-12 12:28:13 -06:00
Ethan 58bf0ec1c6 chore: add additional tailnet topology integration tests (#13549) 2024-06-12 16:02:34 +00:00
Spike Curtis ba7d1835e5 fix: fix flake in TestWorkspaceAgent_Metadata_CatchMemoryLeak (#13553)
Fixes flake seen here: https://github.com/coder/coder/actions/runs/9461246505/job/26061605278

#13486 subtly changes the test so that `post` uses the new v2 Agent API, and when canceling context, there is a race condition where the yamux session underpinning the API can get torn down before the RPC processes the canceled context, yielding a different error response than the test was previously expecting.

I've refactored the test to just stop posting when the test finishes, rather than depend on a context cancel to end the posting goroutine.
2024-06-12 18:33:22 +04:00
Bruno Quaresma 0c627a4cb9 refactor(site): refactor filter search field (#13545) 2024-06-12 10:22:20 -03:00
Ethan a11f8b003b chore: write speedtest connection updates to stderr (#13550) 2024-06-12 07:10:28 +00:00
Kira Pilot dd99897bb2 chore: updating Ashby link to be position agnostic (#13543) 2024-06-11 12:59:33 -04:00
Steven Masley 5ccf5084e8 chore: create type for unique role names (#13506)
* chore: create type for unique role names

Using `string` was confusing when something should be combined with
org context, and when not to. Naming this new name, "RoleIdentifier"
2024-06-11 08:55:28 -05:00
Kyle Carberry c9cca9d56e fix: transform underscores to hyphens for github login (#13384)
Fixes #13339.
2024-06-11 13:34:05 +00:00
Marcin Tojek 7958c52918 docs: faq: restrict file transfers from workspaces (#13534) 2024-06-11 09:29:29 +00:00
Spike Curtis 1f9bdc36bf fix: ignore yamux.ErrSessionShutdown on TestTailnetAPIConnector_Disconnects (#13532) 2024-06-11 11:16:49 +04:00
Ethan dd243686e4 chore!: remove deprecated agent v1 routes (#13486) 2024-06-11 12:22:59 +10:00
dependabot[bot] e7bea17e70 chore: bump braces from 3.0.2 to 3.0.3 in /site (#13526)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-11 03:54:42 +03:00
dependabot[bot] 363dbad3a3 ci: bump the github-actions group with 2 updates (#13521)
Bumps the github-actions group with 2 updates: [crate-ci/typos](https://github.com/crate-ci/typos) and [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action).


Updates `crate-ci/typos` from 1.21.0 to 1.22.3
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.21.0...v1.22.3)

Updates `aquasecurity/trivy-action` from 0.21.0 to 0.22.0
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/fd25fed6972e341ff0007ddb61f77e88103953c2...595be6a0f6560a0a8fc419ddf630567fc623531d)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: aquasecurity/trivy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-11 01:06:40 +03:00
Garrett Delfosse 5b9a65e5c1 chore: move Batcher and Tracker to workspacestats (#13418) 2024-06-10 15:35:23 -04:00
Colin Adler c7e7312cb0 fix(site): don't show start button while starting (#13495) 2024-06-10 13:28:21 -05:00
Marcin Tojek e96652ebbc feat: block file transfers for security (#13501) 2024-06-10 12:12:23 +00:00
Spike Curtis 8326a3a675 chore: change mock clock to allow Advance() within timer/tick functions (#13500) 2024-06-10 15:27:24 +04:00
Kyle Carberry 7c081dcd6f fix: replace invalid utf-8 sequences in agent logs (#13436)
* fix: replace invalid utf-8 sequences in agent logs

Fixes #13433.

* fix: replace invalid UTF-8 with , add regression

Signed-off-by: Spike Curtis <spike@coder.com>

---------

Signed-off-by: Spike Curtis <spike@coder.com>
Co-authored-by: Spike Curtis <spike@coder.com>
2024-06-10 15:27:11 +04:00
Steven Masley 0d65143301 chore: implement audit log for custom role edits (#13494)
* chore: implement audit log for custom role edits
2024-06-07 14:11:57 -05:00
Bruno Quaresma 056a697eff feat(site): add download logs option (#13466) 2024-06-07 10:03:05 -03:00
Cian Johnston 48ecee1025 chore(cli): address cli netcheck test flake (#13492)
* netcheck: removes check for healthy node report in test
* coderd/healthcheck/derphealth: do not override parent context deadline
2024-06-07 10:01:54 +01:00
Steven Masley 7c3b8b6224 chore: duplicate migration file fix, 000216 (#13498) 2024-06-06 16:13:00 -05:00
Steven Masley e2b330fcba chore: change sql parameter for custom roles to be a (name,org_id) tuple (#13480)
* chore: sql parameter to custom roles to be a (name,org) tuple

CustomRole lookup takes (name,org_id) tuples as the search criteria.
2024-06-06 15:36:37 -05:00
Bruno Quaresma 1adc19b41f fix(site): allow user to update their name (#13493) 2024-06-06 15:32:51 -03:00
Bruno Quaresma 4dfa901990 refactor(site): hide select helper when only one proxy exists (#13496) 2024-06-06 15:17:43 -03:00
Bruno Quaresma a8a81a61cd fix(site): fix tooltip in start button group (#13497) 2024-06-06 14:51:52 -03:00
Kayla Washburn-Love 44a70a5bc2 feat: edit org display names and descriptions (#13474) 2024-06-06 10:59:59 -06:00
Cian Johnston 1131772e79 feat(coderd): set full name from IDP name claim (#13468)
* Updates OIDC and GitHub OAuth login to fetch set name from relevant claim fields
* Adds CODER_OIDC_NAME_FIELD as configurable source of user name claim
* Adds httpapi function to normalize a username such that it will pass validation
* Adds firstName / lastName fields to dev OIDC setup
2024-06-06 13:37:08 +01:00
Colin Adler e743588843 docs: bump k8s install version (#13487) 2024-06-06 03:31:32 +00:00
Colin Adler 37676c46d5 chore(scripts): remove remaining gh_auth calls from release scripts (#13485) 2024-06-05 22:24:26 -05:00
Jon Ayers 7995d7c3d6 fix: only render tooltip when require_active_version enabled (#13484) 2024-06-06 02:52:49 +00:00
Colin Adler f1b42a15fa fix(site): show workspace start button when require active version is enabled (#13482) 2024-06-05 16:50:52 -05:00
Steven Masley 8f62311f00 chore: remove organization_id suffix from org_member roles in database (#13473)
Organization member's table is already scoped to an organization.
Rolename should avoid having the org_id appended.

Wipes all existing organization role assignments, which should not be used anyway.
2024-06-05 11:25:02 -05:00
Spike Curtis fade8ba759 fix: fix MeasureLatencyRecvTimeout to accept send=0 (#13477)
Fixes the flake seen here: https://github.com/coder/coder/runs/25832852690

Linux is not a real time operating system, and so there is no guarantee that subsequent `time.Now()` `time.Since()` calls will return a non-zero time.  This assert is mainly there to ensure we don't return `-1`.
2024-06-05 18:27:56 +04:00
Spike Curtis 775fc3f5e9 chore: add Now, Since, AfterFunc to clock; use clock for configmaps test (#13476)
* chore: add Now, Since, AfterFunc to clock; use clock for configmaps test

* chore: update flake.nix vendor hash

Signed-off-by: Spike Curtis <spike@coder.com>

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2024-06-05 16:00:02 +04:00
Spike Curtis ffcfbb6c55 chore: add example test case for clock package (#13465) 2024-06-05 15:49:31 +04:00
Spike Curtis 9c3fd5dd26 chore: add explicit Wait() to clock.Advance() (#13464) 2024-06-05 15:37:16 +04:00
Spike Curtis 42324b386a chore: add clock pkg for testing time (#13461)
Adds a package for testing time/timer/ticker functions.  Implementation is limited to `NewTimer` and `NewContextTicker`, but will eventually be expanded to all `time` functions from the standard library as well as `context.WithTimeout()`, `context.WithDeadline()`.

Replaces `benbjohnson/clock` for the pubsub watchdog, as a proof of concept.

Eventually, as we expand functionality, we will replace most time-related functions with this library for testing.
2024-06-05 13:55:45 +04:00
Ethan a4bba520a2 feat(cli): add json output to coder speedtest (#13475) 2024-06-05 08:31:44 +00:00
Mathias Fredriksson 9a757f8e74 chore(scripts): fix release promote stable to set latest tag (#13471) 2024-06-04 23:01:26 +00:00
dependabot[bot] 83ac386533 chore: bump ejs from 3.1.9 to 3.1.10 in /site (#13447)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 23:52:59 +03:00
Kayla Washburn-Love 0ea89a3d41 chore: add cleanup callbacks to some useEffect calls (#13444) 2024-06-04 12:18:03 -06:00
Stephen Kirby 213848e2e3 chore(docs): rename banners and show usage of multiple (#13435)
* renamed banners in docs

* fmt

* Update appearance.md

Co-authored-by: Kayla Washburn-Love <mckayla@hey.com>

* fmt

---------

Co-authored-by: Kayla Washburn-Love <mckayla@hey.com>
Co-authored-by: Ben <me@bpmct.net>
2024-06-04 17:49:13 +00:00
Ben Potter 8435b70bea chore: update docs for v2.12 mainline and v2.11 stable (#13469)
* chore: update docs for v2.12 mainline and v2.11 stable

* remove broken link
2024-06-04 12:22:13 -05:00
Mathias Fredriksson 3b7f9534fb chore(scripts): fix dry run for autoversion in release.sh (#13470) 2024-06-04 20:10:15 +03:00
Steven Masley e3206612e1 chore: implement typed database for custom permissions (breaks existing custom roles) (#13457)
* chore: typed database custom permissions
* add migration to fix any custom roles out there
2024-06-04 09:27:44 -05:00
Cian Johnston 168d2d6ba0 chore(coderd): add update user profile test for members (#13463) 2024-06-04 14:17:17 +01:00
Marcin Tojek cd32c42699 fix(cli): inherit provisioner tags from last template version (#13462) 2024-06-04 11:59:54 +00:00
Muhammad Atif Ali e527bc6242 chore(dogfood): replace deprecated coder_workspace.owner_oidc_access_token and add order to agent metadata (#13456) 2024-06-04 09:21:01 +01:00
Mathias Fredriksson a51076a4cd chore(scripts): fix unbound variable in tag_version.sh (#13428) 2024-06-03 21:29:24 +00:00
Kayla Washburn-Love 78b8264a90 feat(site): add deployment menu to navbar (#13401) 2024-06-03 15:05:49 -06:00
Muhammad Atif Ali c7233eccec chore(dogfood): bump module versions (#13455) 2024-06-04 00:03:34 +03:00
Colin Adler 40390ecc30 chore: fix TestServer/Prometheus/DBMetricsDisabled test flake (#13453)
See: https://github.com/coder/coder/actions/runs/9352137263/job/25739550487#step:5:368
2024-06-03 15:38:59 -05:00
Kayla Washburn-Love 2806752c7d chore: add light mode snapshot to chromatic for WorkspaceBuildPageView (#13449) 2024-06-03 13:50:59 -06:00
Colin Adler e4ac691468 chore: fix (*coderdtest.WorkspaceAgentWaiter).Wait() flake (#13451) 2024-06-03 14:46:56 -05:00
Colin Adler 43ef00401c chore: linting fixes (#13450) 2024-06-03 14:33:37 -05:00
Steven Masley 27f26910b6 chore: external auth validate response "Forbidden" should return invalid, not an error (#13446)
* chore: add unit test to delete workspace from suspended user
* chore: account for forbidden as well as unauthorized response codes
2024-06-03 13:16:51 -05:00
dependabot[bot] 0b019cad77 chore: bump google.golang.org/api from 0.181.0 to 0.182.0 (#13439)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.181.0 to 0.182.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.181.0...v0.182.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 12:30:12 -05:00
Colin Adler 9d00a26a90 fix: add missing route for codersdk.PostLogSource (#13421) 2024-06-03 12:29:50 -05:00
dependabot[bot] 8cdd468107 chore: bump github.com/coder/terraform-provider-coder from 0.22.0 to 0.23.0 (#13440)
Bumps [github.com/coder/terraform-provider-coder](https://github.com/coder/terraform-provider-coder) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/coder/terraform-provider-coder/releases)
- [Changelog](https://github.com/coder/terraform-provider-coder/blob/main/.goreleaser.yml)
- [Commits](https://github.com/coder/terraform-provider-coder/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: github.com/coder/terraform-provider-coder
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 12:29:29 -05:00
Kayla Washburn-Love cb94dfb1f6 fix: fix build error background color (#13445) 2024-06-03 11:18:44 -06:00
Kayla Washburn-Love 79fd736387 chore(site): enable React's StrictMode (#13399) 2024-06-03 10:03:46 -06:00
Steven Masley 973cc2b875 chore: add edit organization role to cli (#13365)
Editing custom org roles from hidden org cli command.
2024-06-03 09:34:10 -05:00
Steven Masley 24ba81930b chore: return failed refresh errors on external auth as string (was boolean) (#13402)
* chore: return failed refresh errors on external auth

Failed refreshes should return errors. These errors are captured
as validate errors.
2024-06-03 09:33:49 -05:00
Marcin Tojek bf98b0dfe4 fix: correct swagger description for Insights API (#13442) 2024-06-03 15:48:31 +02:00
Colin Adler b723da9e91 chore: upgrade terraform to v1.8.5 (#13429) 2024-06-02 13:10:28 -04:00
Kayla Washburn-Love b248f125e1 chore: rename notification banners to announcement banners (#13419) 2024-05-31 10:59:28 -06:00
Garrett Delfosse de8149fbfd chore: move template meta last_used_at update to workspacestats (#13415) 2024-05-31 12:26:19 -04:00
Michael Smith 19530c6b44 fix: update DeleteWorkspaceOptions to pick properties correctly (#13423)
* fix: update typo

* fix: update typo in call site

* fix: update type for deleteWorkspace mock

* fix: update one more type mismatch
2024-05-31 10:23:59 -04:00
Mathias Fredriksson 4758952ebc chore(scripts): fix expression interpreted as exit code on some Bash versions (#13417) 2024-05-30 17:24:41 +00:00
Kira Pilot bee4ece1b9 fix: update install.sh to remove dead doc link (#13308)
* chore(docs): update install.sh to remove dead doc link

* Update install.sh

Co-authored-by: Kyle Carberry <kyle@coder.com>

* escaping script properly

---------

Co-authored-by: Kyle Carberry <kyle@coder.com>
2024-05-30 10:39:17 -04:00
Danny Kopping 7569cccc51 chore: remove git pinning (#13414)
Alpine 3.20 includes 2.45.1 by default: https://git.alpinelinux.org/aports/tree/main/git/APKBUILD?h=3.20-stable#n56

Follow-up from https://github.com/coder/coder/pull/13411#issuecomment-2139028721

Signed-off-by: Danny Kopping <danny@coder.com>
2024-05-30 15:58:32 +02:00
Danny Kopping 59ab5053b1 fix: return error if agent init script fails to download valid binary (#13280) 2024-05-30 13:33:00 +02:00
Cian Johnston e176867d77 chore: update deprecated usage of coder_workspace.owner* fields (#13390)
Per https://github.com/coder/terraform-provider-coder/releases/tag/v0.23.0

Performs a mechanical rename of existing usage deprecated fields in the latest version of the coder/coder provider.

Closes #13382
2024-05-30 11:31:51 +01:00
Cian Johnston 7cc96f5d40 chore(docs): add recommendations for dependency management (#13400) 2024-05-30 10:17:26 +01:00
Muhammad Atif Ali 7a7bef0dab ci: fix syntax issue in docker-base.yaml (#13412) 2024-05-30 08:49:30 +00:00
Danny Kopping a1671a633c Upgrade to git v2.45.1 to fix alpine 3.20 builds (#13411)
Possibly fixes https://github.com/coder/coder/issues/13407

Signed-off-by: Danny Kopping <danny@coder.com>
2024-05-30 08:36:24 +00:00
Muhammad Atif Ali 6730c24c58 ci: build base image on PRs (#13409) 2024-05-30 08:35:37 +00:00
Spike Curtis 5aea80381c fix: increses DERP send queue length to 512 for increased throughput (#13406) 2024-05-30 11:46:18 +04:00
Mathias Fredriksson 9eb797eb5a chore(scripts): add safety check for difference between dry run release notes (#13398) 2024-05-29 22:01:10 +03:00
Mathias Fredriksson 5fb231774c chore(scripts): add custom gh auth to release script (#13396) 2024-05-29 18:37:04 +00:00
Mathias Fredriksson 9ae825ebae chore(scripts): push version bump pr branch in release script (#13397) 2024-05-29 18:30:42 +00:00
Mathias Fredriksson 374f0a0fd1 chore(scripts): handle renamed cherry-pick commits in release script (#13395) 2024-05-29 21:30:11 +03:00
Michael Brewer bc8126fa45 fix(cli): skip optional coder_external_auth (#13368)
* fix(cli): skip over coder_external_auth that are optional

* chore: Delete package-lock.json
2024-05-29 17:37:54 +00:00
Garrett Delfosse 5789ea5397 chore: move stat reporting into workspacestats package (#13386) 2024-05-29 11:49:08 -04:00
Steven Masley afd9d3b35f feat: add api for patching custom org roles (#13357)
* chore: implement patching custom organization roles
2024-05-29 09:49:43 -05:00
Matt Vollmer b69f6358f0 Update manifest.json (#13391) 2024-05-29 12:37:06 +00:00
Cian Johnston cca3cb1c55 feat(provisioner): pass owner git ssh key (#13366) 2024-05-29 11:43:08 +01:00
Spike Curtis b7edf5bbc7 fix: block writes from gVisor to tailscale instead of dropping (#13389)
fixes: #13108

upgrades our tailscale fork to include https://github.com/coder/tailscale/pull/52
2024-05-29 14:30:24 +04:00
Spike Curtis 84b3121777 fix: stop logging workspace agent unless verbose (#13378) 2024-05-29 08:17:35 +04:00
Spike Curtis a551aa51ab fix: respect --disable-direct-connections on coder speedtest (#13377) 2024-05-29 08:07:48 +04:00
Stephen Kirby ec78f54941 added jetbrains fleet link to manifest.json (#13363) 2024-05-29 06:55:22 +03:00
dependabot[bot] ef4ed64a29 chore: bump gopkg.in/DataDog/dd-trace-go.v1 from 1.61.0 to 1.64.0 (#13316)
Bumps gopkg.in/DataDog/dd-trace-go.v1 from 1.61.0 to 1.64.0.

---
updated-dependencies:
- dependency-name: gopkg.in/DataDog/dd-trace-go.v1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 06:51:59 +03:00
Colin Adler 02c36868b2 chore: upgrade go.uber.org/goleak (#13388)
The latest published version is broken on go 1.20
2024-05-28 17:15:37 -05:00
dependabot[bot] 7ea510e091 chore: bump github.com/gohugoio/hugo from 0.125.3 to 0.126.1 (#13323)
Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from 0.125.3 to 0.126.1.
- [Release notes](https://github.com/gohugoio/hugo/releases)
- [Changelog](https://github.com/gohugoio/hugo/blob/master/hugoreleaser.toml)
- [Commits](https://github.com/gohugoio/hugo/compare/v0.125.3...v0.126.1)

---
updated-dependencies:
- dependency-name: github.com/gohugoio/hugo
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 15:49:34 -05:00
dependabot[bot] 18692058a9 chore: bump google.golang.org/grpc from 1.63.2 to 1.64.0 (#13319)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.63.2 to 1.64.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.63.2...v1.64.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 15:48:58 -05:00
dependabot[bot] 5a8a254c93 chore: bump github.com/hashicorp/hc-install from 0.6.3 to 0.7.0 (#13372)
Bumps [github.com/hashicorp/hc-install](https://github.com/hashicorp/hc-install) from 0.6.3 to 0.7.0.
- [Release notes](https://github.com/hashicorp/hc-install/releases)
- [Commits](https://github.com/hashicorp/hc-install/compare/v0.6.3...v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/hc-install
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 20:17:23 +00:00
dependabot[bot] 00f6cfe3cf chore: bump github.com/hashicorp/go-version from 1.6.0 to 1.7.0 (#13374)
Bumps [github.com/hashicorp/go-version](https://github.com/hashicorp/go-version) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/hashicorp/go-version/releases)
- [Changelog](https://github.com/hashicorp/go-version/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/go-version/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-version
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 20:04:49 +00:00
Dean Sheather 9299e9f6ba chore: hard NAT <-> easy NAT integration test (#13314) 2024-05-29 06:04:07 +10:00
dependabot[bot] e5d848f19d chore: bump github.com/valyala/fasthttp from 1.53.0 to 1.54.0 (#13373)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.53.0 to 1.54.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.53.0...1.54.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 15:01:03 -05:00
dependabot[bot] 1edd46dd5f chore: bump github.com/hashicorp/terraform-json from 0.21.0 to 0.22.1 (#13322)
Bumps [github.com/hashicorp/terraform-json](https://github.com/hashicorp/terraform-json) from 0.21.0 to 0.22.1.
- [Release notes](https://github.com/hashicorp/terraform-json/releases)
- [Commits](https://github.com/hashicorp/terraform-json/compare/v0.21.0...v0.22.1)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/terraform-json
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 19:54:06 +00:00
dependabot[bot] 762cb84f4a chore: bump github.com/aws/aws-sdk-go-v2 from 1.26.1 to 1.27.0 (#13324)
Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.26.1 to 1.27.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.26.1...v1.27.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 19:48:23 +00:00
Steven Masley 6293c33746 chore: add refresh token and error to user's external auth page (#13380)
* chore: add story for failed refresh error
* chore: add refresh icon to tokens that can refresh
2024-05-28 14:07:22 -05:00
dependabot[bot] 5b78ec97b6 chore: bump alpine from 3.19.1 to 3.20.0 in /scripts (#13375)
Bumps alpine from 3.19.1 to 3.20.0.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 12:45:16 -05:00
Kyle Carberry 79d73f77f5 chore: skip Azure TestExpiresSoon (#13385)
Adds some context to the test skip so it can be removed or enabled in the future.
2024-05-28 16:45:41 +00:00
dependabot[bot] a1d3b82dd1 ci: bump aquasecurity/trivy-action from 0.20.0 to 0.21.0 in the github-actions group (#13376)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 19:57:36 +03:00
Muhammad Atif Ali 47f8f5d963 chore(docs): update github app permission to read org members (#13362) 2024-05-24 23:15:29 +03:00
dependabot[bot] 60224fa216 chore: bump github.com/fatih/color from 1.16.0 to 1.17.0 (#13321)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2024-05-24 16:45:11 +00:00
dependabot[bot] 87dd878779 chore: bump google.golang.org/api from 0.180.0 to 0.181.0 (#13317)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 19:33:35 +03:00
dependabot[bot] ff617cc545 chore: bump github.com/valyala/fasthttp from 1.52.0 to 1.53.0 (#13318)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 18:46:57 +03:00
Spike Curtis a0962ba089 fix: wait for PGCoordinator to clean up db state (#13351)
c.f. https://github.com/coder/coder/pull/13192#issuecomment-2097657692

We need to wait for PGCoordinator to finish its work before returning on `Close()`, so that we delete database state (best effort -- if this fails others will filter it out based on heartbeats).
2024-05-24 12:01:03 +04:00
Dean Sheather e5bb0a7a00 chore: add easy NAT integration tests part 2 (#13312) 2024-05-24 16:32:30 +10:00
Steven Masley 1b4ca00428 chore: include custom roles in list org roles (#13336)
* chore: include custom roles in list org roles
* move cli show roles to org scope
2024-05-23 07:54:59 -10:00
Kayla Washburn-Love d748c6d718 fix(site): correct the size and position of the timeline trail in safari (#13348) 2024-05-23 11:25:10 -06:00
Marcin Tojek 98fa823c79 docs: describe workspace tags (#13352) 2024-05-23 15:20:50 +02:00
Spike Curtis b43344b672 feat: use latest gVisor and go 1.22.3 (#13338) 2024-05-23 08:22:44 -04:00
Cian Johnston c67eba10d5 chore: update scale docs to include guidelines for wsproxies (#13350) 2024-05-23 10:00:23 +01:00
Marcin Tojek c2837a62e4 feat: evaluate provisioner tags (#13333) 2024-05-23 07:53:51 +00:00
Colin Adler fa9edc1f42 chore(scripts): remove gh_auth from release.sh (#13347)
It breaks the `gh` cli for creating workflows.
2024-05-22 14:28:21 -05:00
Colin Adler a40e954afc chore(docs): update k8s mainline version (#13346) 2024-05-22 14:01:11 -05:00
Kyle Carberry 3364abecdd chore: generate terraform testdata with matching terraform version (#13343)
Terraform changed the default output of the `terraform graph` command. You must put `-type=plan` to keep the prior behavior.


Co-authored-by: Colin Adler <colin1adler@gmail.com>
2024-05-22 12:45:47 -05:00
Ammar Bandukwala ed6ee9aaa8 chore(README): add hiring link (#13345) 2024-05-22 12:01:29 -05:00
Bruno Quaresma 390ff9ac05 refactor(site): hide unavailable usage information (#13341) 2024-05-22 13:26:59 +00:00
Justin Shoffstall 7ea4a89a20 chore: update kubernetes.md, bumping stable from v2.9.4 to v2.10.2 (#13275) 2024-05-22 12:24:28 +03:00
Bruno Quaresma 78deaba481 feat(site): show "update and start" button when update is forced (#13334) 2024-05-21 19:29:54 +00:00
Bruno Quaresma f27f5c0002 feat(site): show number of times coder_app is opened (#13335) 2024-05-21 16:04:41 -03:00
Kayla Washburn-Love 3f1e9c038a feat(coderd): add endpoints for editing and deleting organizations (#13287) 2024-05-21 12:46:31 -06:00
Steven Masley 0a86d6d176 chore: expose formatExamples enterprise commands (#13304)
Exporting it allows enterprise functions to also use it.
2024-05-21 13:26:34 -05:00
Steven Masley c61b64be61 feat: add hidden enterprise cmd command to list roles (#13303)
* feat: add hidden enterprise cmd command to list roles

This includes custom roles, and has a json ouput option for
more granular permissions
2024-05-21 13:14:00 -05:00
Asher 8e78b9495d feat: open most recent directory or workspace when launching VS Code (#13326) 2024-05-21 09:19:59 -08:00
Dean Sheather 273209432d chore: fix tailnet integration test flake (#13313) 2024-05-21 12:57:39 +10:00
Marcin Tojek b8b80fe6d2 feat: store coder_workspace_tags in the database (#13294) 2024-05-20 13:30:19 +00:00
Cian Johnston 45b45f1107 ci: re-enable test migrations in release workflow (#13307) 2024-05-20 10:35:06 +01:00
Kayla Washburn-Love a63d427efd chore: add unique org name constraint to db (#13311) 2024-05-17 12:40:38 -06:00
Bruno Quaresma 4af0f093ee fix(site): fix floating number on duration fields (#13209) 2024-05-17 15:26:00 -03:00
dependabot[bot] d8bb5a05db chore: bump github.com/fergusstrange/embedded-postgres from 1.26.0 to 1.27.0 (#13255)
Bumps [github.com/fergusstrange/embedded-postgres](https://github.com/fergusstrange/embedded-postgres) from 1.26.0 to 1.27.0.
- [Release notes](https://github.com/fergusstrange/embedded-postgres/releases)
- [Commits](https://github.com/fergusstrange/embedded-postgres/compare/v1.26.0...v1.27.0)

---
updated-dependencies:
- dependency-name: github.com/fergusstrange/embedded-postgres
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-17 12:03:32 -05:00
Colin Adler f176ff532f ci: re-pin actions/dependency-review-action back to a release (#13309) 2024-05-17 11:55:30 -05:00
Cian Johnston f23d4802b5 ci: fix test-migrations target when main branch is not present locally (#13306) 2024-05-17 10:24:56 +01:00
Mathias Fredriksson f66d0445da chore(scripts): fix stable release promote script (#13204) 2024-05-17 08:25:10 +00:00
Mathias Fredriksson 0998cedb5c chore(scripts): fix a few release script changelog issues (#13200) 2024-05-17 11:19:48 +03:00
Colin Adler 92c5dfa266 docs: bump k8s install version (#13302) 2024-05-16 20:24:03 +00:00
Colin Adler 80538c079d chore: update git -> 2.43.4 and terraform -> 1.7.5 (#13299)
This fixes an RCE in git and gets us one minor version closer to fixing
a critical Terraform vulnerability. In the next release we'll bump to
1.8.x.
2024-05-16 19:07:07 +00:00
Steven Masley ad8c314130 chore: implement api for creating custom roles (#13298)
api endpoint (gated by experiment) to create custom_roles
2024-05-16 13:47:47 -05:00
Colin Adler 85de0e966d chore: fix TestMeasureLatency/MeasureLatencyRecvTimeout flake (#13301) 2024-05-16 13:42:42 -05:00
Steven Masley cf91eff7cf chore: implement databased backend for custom roles (#13295)
Includes db schema and dbauthz layer for upserting custom roles. Unit test in `customroles_test.go` verify against escalating permissions through this feature.
2024-05-16 13:11:26 -05:00
Steven Masley 194be12133 chore: verify validity of built in rbac roles (#13296)
Verifies our built in roles are valid according to our policy.go. Working on custom roles requires the dynamic roles to adhere to these rules. Feels fair the built in ones do too.
2024-05-16 12:07:44 -05:00
Mathias Fredriksson a0fce363cd feat(coderd): add times_used to coder_apps in insights API (#13292)
For now, only applied to `coder_app`s, same logic can be implemented for
VS Code, SSH, etc.

Part of #13099
2024-05-16 16:53:01 +03:00
Michael Smith 63e06853eb fix: update tests for useClipboard to minimize risks of flakes (#13250)
* wip: commit progress on test revamps

* fix: update existing tests to new format

* chore: add test case for global snackbar

* refactor: consolidate files

* refactor: make http dependency more explicit

* chore: add extra test case for exposed error value

* docs: fix typos

* fix: make sure clipboard is reset between test runs

* docs: add more context to comments

* refactor: update mock console.error logic to use jest.spyOn

* docs: add more clarifying comments

* refactor: split off type alias for clarity
2024-05-15 16:59:15 -04:00
Stephen Kirby 114fb31fbb fixed sharable port + coder_app interaction (#13285) 2024-05-15 14:40:46 -05:00
Kayla Washburn-Love fc6f18aa96 feat(site): add an organization switcher to the user menu (#13269) 2024-05-15 13:14:34 -06:00
Steven Masley 1f5788feff chore: remove rbac psuedo resources, add custom verbs (#13276)
Removes our pseudo rbac resources like `WorkspaceApplicationConnect` in favor of additional verbs like `ssh`. This is to make more intuitive permissions for building custom roles.

The source of truth is now `policy.go`
2024-05-15 11:09:42 -05:00
Steven Masley cb6b5e8fbd chore: push rbac actions to policy package (#13274)
Just moved `rbac.Action` -> `policy.Action`. This is for the stacked PR to not have circular dependencies when doing autogen. Without this, the autogen can produce broken golang code, which prevents the autogen from compiling.

So just avoiding circular dependencies. Doing this in it's own PR to reduce LoC diffs in the primary PR, since this has 0 functional changes.
2024-05-15 09:46:35 -05:00
Bruno Quaresma f14927955d fix(site): fix group badge visual (#13263) 2024-05-14 13:52:16 -03:00
Kayla Washburn-Love a8a0be98b8 chore: expose all organization ids from AuthContext (#13268) 2024-05-14 10:48:15 -06:00
Garrett Delfosse 721ab2a1b4 chore: add workspace activity linter (#13273) 2024-05-14 12:31:31 -04:00
Kayla Washburn-Love 2b29559984 chore: add setting to enable multi-organization ui (#13266) 2024-05-13 14:41:45 -06:00
Steven Masley 9ced001570 chore: add multi-org experiment for UI view toggling (#13260)
* chore: Add multi-org experiment

UI will use to toggle different views
2024-05-13 13:46:01 -05:00
Garrett Delfosse ebee9288ae fix: properly convert max port share level for oss (#13261) 2024-05-13 14:37:51 -04:00
Bruno Quaresma a5a64948cd feat(site): open README links in new tab (#13264) 2024-05-13 15:11:01 -03:00
Bruno Quaresma 8412450ae3 chore(site): fix portforward issue with vite (#13262) 2024-05-13 17:13:41 +00:00
dependabot[bot] c41d0efff9 chore: bump github.com/prometheus/client_golang from 1.18.0 to 1.19.1 (#13232)
* chore: bump github.com/prometheus/client_golang from 1.18.0 to 1.19.1
2024-05-13 13:01:28 +00:00
Muhammad Atif Ali 7358c1b1ac chore(dogfood): bump module versions to latest (#13246)
We should use the latest versions as these are the ones most customers will use.

We can automate this with @dependabot once we resolve https://github.com/coder/registry.coder.com/issues/13
2024-05-13 09:51:47 +03:00
dependabot[bot] 4e7381341f chore: bump google.golang.org/api from 0.176.1 to 0.180.0 (#13235)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-12 23:34:59 +03:00
dependabot[bot] 228b99d9c2 chore: bump google.golang.org/protobuf from 1.33.0 to 1.34.1 (#13236)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-12 23:33:37 +03:00
Michael Smith f13b1c9af6 refactor: improve test isolation for Axios API logic (#13125)
* wip: commit progress on code split-up

* wip: commit more progress

* wip: finish initial version of class implementation

* chore: update all import paths to go through client instance

* fix: remove temp comments

* refactor: smoooooooosh the API

* refactor: update import setup for tests
2024-05-12 19:05:22 +00:00
dependabot[bot] 5ddbeddf85 chore: bump protobufjs from 7.2.4 to 7.2.5 in /site (#13245)
Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.2.4 to 7.2.5.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.2.4...protobufjs-v7.2.5)

---
updated-dependencies:
- dependency-name: protobufjs
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-10 22:19:19 +00:00
dependabot[bot] 3d707cbe5a chore: bump tar from 6.2.0 to 6.2.1 in /site (#13244)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-11 01:10:39 +03:00
Jon Ayers ee817b4d80 fix: fix nix flake sed command (#13243) 2024-05-11 01:10:19 +03:00
dependabot[bot] c557c25b3d chore: bump golang.org/x/tools from 0.20.0 to 0.21.0 (#13237)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-11 00:08:38 +03:00
Jon Ayers 82c1562f82 fix: skip license review for dependabot (#13239) 2024-05-10 18:14:03 +00:00
dependabot[bot] 8c9560ddb8 ci: bump the github-actions group with 2 updates (#13238)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-10 17:38:07 +00:00
Kayla Washburn-Love 7eb228e3ff feat: popover paywall in appearance settings (#13217) 2024-05-10 11:21:21 -06:00
Muhammad Atif Ali 6182ee90f0 chore: remove dependabot config for dogfood template (#13230) 2024-05-10 20:14:37 +03:00
Danny Kopping 989575c5b6 chore: prevent commit signing in tests (#13222) 2024-05-10 16:35:59 +02:00
Danny Kopping 4671ebb330 feat: measure pubsub latencies and expose metrics (#13126) 2024-05-10 12:31:49 +00:00
Kayla Washburn-Love e14f8fb64b fix(install.sh): install from github when using --stable on macOS (#13216) 2024-05-09 13:14:31 -06:00
Muhammad Atif Ali 679099373b docs(ides): document connection via JetBrains Fleet (#13179)
* docs: add docs to connect via JetBrains Fleet

* Create fleet.md

* Update fleet.md

* Create ssh-connect-to-coder.png

* Add files via upload

* `make fmt`

* Update fleet.md

* Update docs/ides/fleet.md

Co-authored-by: Kyle Carberry <kyle@coder.com>

* Update fleet.md

---------

Co-authored-by: Kyle Carberry <kyle@coder.com>
2024-05-09 21:29:53 +03:00
Kayla Washburn-Love d8e0be6ee6 feat: add support for multiple banners (#13081) 2024-05-08 15:40:43 -06:00
Steven Masley a4bd50c985 chore: enable terraform provisioners in e2e by default (#13134)
* skip docker test for now, it leaks containers
2024-05-08 13:34:22 -05:00
Spike Curtis 1832a755e1 docs: describe AWS hard NAT (#13205)
Documents what I've learned about getting direct connections on AWS.  Several customers have had issues.
2024-05-08 20:29:12 +04:00
Bruno Quaresma 35cb572888 refactor(site): refactor the workspace settings form (#13198) 2024-05-08 13:12:48 -03:00
Bruno Quaresma 24448e79fe fix: prevent extending if template disallows (#13182) 2024-05-08 12:58:14 -03:00
Stephen Kirby c73d5a2617 docs: bump mainline version to v2.11.0 (#13202)
* docs: bump mainline version to v2.11.0

* bump release schedule
2024-05-07 16:29:51 -05:00
Mathias Fredriksson 06dd656e08 ci: disable make test-migrations in release.yaml (#13201) 2024-05-07 17:15:12 +00:00
dependabot[bot] b7a921a2bf chore: bump express from 4.18.2 to 4.19.2 in /site (#13196)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 11:08:33 -04:00
dependabot[bot] 30227dae97 chore: bump follow-redirects from 1.15.4 to 1.15.6 in /site (#13197)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 11:08:24 -04:00
dependabot[bot] 96f2cec541 chore: bump vite from 4.5.2 to 4.5.3 in /site (#13189)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.2 to 4.5.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 14:01:13 +03:00
dependabot[bot] 3905e2c541 chore: bump undici from 6.7.1 to 6.11.1 in /site (#13190)
Bumps [undici](https://github.com/nodejs/undici) from 6.7.1 to 6.11.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.7.1...v6.11.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 14:00:48 +03:00
Colin Adler 421c0d1242 chore: add nginx topology to tailnet tests (#13188) 2024-05-07 18:17:38 +10:00
Dean Sheather 677be9aab2 chore: add tailnet integration test CI job (#13181) 2024-05-07 06:21:17 +00:00
Dean Sheather 72f2efe048 chore: implement easy NAT direct integration test (#13169) 2024-05-07 06:07:57 +00:00
Dean Sheather 5e8f97d8c3 chore: add DERP websocket integration tests (#13168)
- `DERPForceWebSockets`: Test that DERP over WebSocket (as well as DERPForceWebSockets works). This does not test the actual DERP failure detection code and automatic fallback.
- `DERPFallbackWebSockets`: Test that falling back to DERP over WebSocket works.

Also:
- Rearranges some test code and refactors `TestTopology.StartServer` to be `TestTopology.ServerOptions` and take a struct instead of a function

Closes #13045
2024-05-06 20:37:01 -07:00
Muhammad Atif Ali b56c9c438f ci: only send docs-check notifications on schedule (#13191) 2024-05-07 01:40:18 +03:00
Idleite 6f5c183c80 docs: show the proper Redirect URI for Gitea (#13162) 2024-05-06 22:28:04 +00:00
Kyle Carberry 3e3118794f chore: add build targets to nix flake (#13186)
* chore: add build targets to nix flake

Enables `nix build github:coder/coder#main`!

* Fix all packages

* Add back pnpm

* Update flake.nix

Co-authored-by: Asher <ash@coder.com>

* Remove yarn

* fmt

---------

Co-authored-by: Asher <ash@coder.com>
2024-05-06 18:21:20 -04:00
Muhammad Atif Ali 05facc971b ci: sync terraform version (#13187) 2024-05-06 20:06:21 +00:00
dependabot[bot] e7c87a806b ci: bump the github-actions group with 2 updates (#13177)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 22:43:17 +03:00
Spike Curtis dfd27f559e Revert "chore: fix build ci (#13164)" (#13180)
This reverts commit 886a97b425.
2024-05-06 13:13:24 +00:00
Spike Curtis deee9492e3 Revert "fix: install openrc service on alpine (#12294) (#12870)" (#13178)
This reverts commit b20c63c185.
2024-05-06 16:48:19 +04:00
Mathias Fredriksson 619ec927e9 test(coderd/database): fix DST issue in dbpurge test (#13170)
Fixes #13165
2024-05-06 14:14:38 +03:00
Spike Curtis e76b595052 fix: use a native websocket.NetConn for agent RPC client (#13142)
One cause of #13139 is a peculiar failure mode of `WebsocketNetConn` which causes it to return `context.Canceled` in some circumstances when the underlying websocket fails.  We have special processing for that error in the `agent.run()` routine, which is erroneously being triggered.

Since we don't actually need the returned context from `WebsocketNetConn`, we can simplify and just use the netConn from the `websocket` library directly.
2024-05-06 15:00:34 +04:00
Spike Curtis d51c6912a7 fix: make handleManifest always signal dependents (#13141)
Fixes #13139

Using a bare channel to signal dependent goroutines means that we can only signal success, not failure, which leads to deadlock if we fail in a way that doesn't cause the whole `apiConnRoutineManager` to tear down routines.

Instead, we use a new object called a `checkpoint` that signals success or failure, so that dependent routines get unblocked if the routine they depend on fails.
2024-05-06 14:47:41 +04:00
Spike Curtis 2efb46a10e chore: remove superfluous context.Canceled handling (#13140)
Removes a check for `context.Canceled` inside the `handleManifest` routine.  This checking is handled in the `apiConnRoutineManager`, so checking inside the handler is redundant.
2024-05-06 14:33:16 +04:00
Muhammad Atif Ali 7c3ec51997 docs(admin/external-auth.md): add JFrog Artifactory guide (#13166) 2024-05-06 11:34:21 +03:00
Muhammad Atif Ali 3e77f5b512 chore(docs): replace git-auth with external-auth (#13167) 2024-05-06 11:17:19 +03:00
Dean Sheather d956af0a3a chore: add EasyNATDERP tailnet integration test (#13138) 2024-05-06 15:36:54 +10:00
Colin Adler 886a97b425 chore: fix build ci (#13164) 2024-05-06 05:01:47 +00:00
Colin Adler 13dd526f11 fix: prevent stdlib logging from messing up ssh (#13161)
Fixes https://github.com/coder/coder/issues/13144
2024-05-03 22:12:06 +00:00
recanman b20c63c185 fix: install openrc service on alpine (#12294) (#12870)
* fix: install openrc service on alpine (#12294)

* fmt

---------

Co-authored-by: Kyle Carberry <kyle@coder.com>
2024-05-03 21:09:23 +00:00
Michael Brewer 060f023174 feat: mask coder login token to enhance security (#12948)
* feat(login): treat coder token as a secret

* Update login.go
2024-05-03 17:03:13 -04:00
Colin Adler 205c43da99 fix(enterprise): mark nodes from unhealthy coordinators as lost (#13123)
Instead of removing the mappings of unhealthy coordinators entirely,
mark them as lost instead. This prevents peers from disappearing from
other peers if a coordinator misses a heartbeat.
2024-05-03 14:07:29 -05:00
Garrett Delfosse a3c23ed313 chore: add docs for sharing ports (#13136)
Co-authored-by: kirby <kirby@coder.com>
Co-authored-by: Stephen Kirby <me@skirby.dev>
2024-05-03 12:38:29 -04:00
Michael Smith 34a3bdc4ec fix: add more tests for metadata hook functionality (#13145) 2024-05-03 15:28:54 +00:00
Steven Masley 09f00c08df chore: shutdown provisioner should stop waiting on client (#13118)
* chore: shutdown provisioner should stop waiting on client
* chore: add unit test that replicates failed client conn
2024-05-03 10:15:17 -05:00
Steven Masley 94a3e3a563 chore: allow terraform & echo built-in provisioners (#13121)
* chore: allow terraform & echo built-in provisioners

Built-in provisioners serve all specified types. This allows running terraform, echo, or both in built in.
The cli flag to control the types is hidden by default, to be used primarily for testing purposes.
2024-05-03 10:14:26 -05:00
Michael Smith 7873c961e3 fix: ensure signing out cannot cause any runtime render errors (#13137)
* fix: remove some of the jank around our core App component

* refactor: scope navigation logic more aggressively

* refactor: add explicit return type to useAuthenticated

* refactor: clean up ProxyContext code

* wip: add code for consolidating the HTML metadata

* refactor: clean up hook logic

* refactor: rename useHtmlMetadata to useEmbeddedMetadata

* fix: correct names that weren't updated

* fix: update type-safety of useEmbeddedMetadata further

* wip: switch codebase to use metadata hook

* refactor: simplify design of metadata hook

* fix: update stray type mismatches

* fix: more type fixing

* fix: resolve illegal invocation error

* fix: get metadata issue resolved

* fix: update comments

* chore: add unit tests for MetadataManager

* fix: beef up tests

* fix: update typo in tests
2024-05-03 10:40:06 -04:00
Dean Sheather ed0ca76b0b chore: do network integration tests in isolated net ns (#13117) 2024-05-03 05:42:13 +00:00
Steven Masley 7779c0a1dc chore: enable playwright test extension in vscode (#13135)
* chore: enable playwright test extension in vscode

This enables using the vscode debugger in playwright tests
2024-05-02 23:14:24 +00:00
Garrett Delfosse 699e187d55 fix: remove mention of protocol lag (#13133) 2024-05-02 17:10:30 -04:00
Eric Paulsen 565b45deba docs: add island integration guide (#13113)
* docs: add island integration guide

* make: fmt

* F

omit F

* fix: naming and manifest

---------

Co-authored-by: Matt Vollmer <matthewjvollmer@outlook.com>
2024-05-02 15:12:34 -04:00
Garrett Delfosse c550d0641d feat: move shared ports out of experiment (#13120) 2024-05-02 14:11:33 -04:00
Steven Masley c2cb0e9fe2 chore: testIDP to be usable as primary auth (#13132)
Flags printed to console show as external or primary auth.
Usage assumes only 1 static oidc_member for now
2024-05-02 11:19:19 -05:00
Spike Curtis 3de737fdc8 fix: start packet capture immediately on speedtest (#13128)
I initially made this change when hacking wgengine to also capture wireguard packets going into the magicsock, so that we could capture the initial wireguard handshake. 

I don't think we should ship that additional capture logic, but... it seems generally useful to capture packets from the get go on speedtest, so that you can see disco and pings before the TCP speedtest session starts.
2024-05-02 19:44:32 +04:00
Kyle Carberry 93d8812284 chore: remove codecov (#13124)
* chore: remove codecov

It wasn't being used anymore.

* Update actions packages
2024-05-01 21:47:25 +00:00
Steven Masley 845407fe7a chore: cover deadline crossing autostart border on start (#13115)
When starting a workspace, if the deadline crosses an autostart boundary, the deadline is set to autostart + TTL. 
This copies the behavior in `ActivityBumpWorkspace`, but does not require activity.
2024-05-01 10:43:04 -05:00
Bruno Quaresma 71a03a8b1d fix(site): fix template schedule update overriding other settings (#13114) 2024-05-01 10:25:40 -03:00
Dean Sheather f2dd0a8e5d feat: try IPv6 when dialing IPv4 in workspaces (#13116) 2024-05-01 21:45:25 +10:00
Muhammad Atif Ali 3ff9cef498 chore(scripts): auto authenticate gh CLI in scripts on dogfood (#13107)
* chore: auto authenticate gh CLI in scripts

* fix shellcheck issues
2024-04-30 19:36:12 +03:00
Steven Masley 53f7e9e0a1 chore: dynamically determine gitlab external auth defaults (#13102)
* chore: dynamically determine gitlab external auth defaults

Static defaults work for github cloud, but not self hosted.
Self hosted setups will now have sane defaults if omitted.
2024-04-30 09:45:52 -05:00
Kyle Carberry 47993e3fcf chore: update tailscale to fix leaking dns lookup (#13109)
See failure in: https://github.com/coder/coder/actions/runs/8887860105/job/24403798734#step:5:376
2024-04-30 12:36:16 +00:00
Kyle Carberry d302570091 chore: remove GITHUB_TOKEN from dogfood env vars (#13106)
This was stale all the time!
2024-04-30 01:26:58 +00:00
Kyle Carberry 4e5960660e chore: fix dependency review action (#13105)
See https://github.com/actions/dependency-review-action/issues/757
2024-04-29 20:51:01 -04:00
Kyle Carberry fbb98b950a chore: centralize build info for site (#13104)
The build info passed to the frontend via HTML was incorrect.
2024-04-29 20:50:49 -04:00
Kyle Carberry 1bda8a0856 feat: add deployment_id to the ui and licenses (#13096)
* feat: expose `deployment_id` in the user dropdown

* feat: add license deployment_id verification

* Ignore wireguard.com from mlc config
2024-04-29 16:50:11 -04:00
Aaron Lehmann 0e3dc2a80f feat: influence parameter defaults through cli flag/env (#13039)
* feat: influence parameter defaults through cli flag/env

Add a --parameter-default flag / CODER_RICH_PARAMETER_DEFAULT
environment variable which overrides default values suggested for
parameters.

This allows scripts or middleware wrapping the CLI to substitute
defaults for parameter values beyond those defined at the template
level. For example, Git repository/branch parameters can be given
defaults based on the current checkout, or default parameter values can
be parsed out of files inside the repo.

* Rename defaults arg to defaultOverrides
2024-04-29 14:23:54 -04:00
Bruno Quaresma 053c56cc1a fix(site): fix template schedule options (#13084) 2024-04-29 14:14:24 -03:00
dependabot[bot] ed07921752 ci: bump crate-ci/typos from 1.20.9 to 1.20.10 in the github-actions group (#13090)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 16:57:53 +00:00
dependabot[bot] 4a83e84a23 chore: bump github.com/jmoiron/sqlx from 1.3.5 to 1.4.0 (#13095)
Bumps [github.com/jmoiron/sqlx](https://github.com/jmoiron/sqlx) from 1.3.5 to 1.4.0.
- [Release notes](https://github.com/jmoiron/sqlx/releases)
- [Commits](https://github.com/jmoiron/sqlx/compare/v1.3.5...v1.4.0)

---
updated-dependencies:
- dependency-name: github.com/jmoiron/sqlx
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 19:54:40 +03:00
dependabot[bot] f2a21c604b chore: bump github.com/moby/moby (#13093)
Bumps [github.com/moby/moby](https://github.com/moby/moby) from 26.0.1+incompatible to 26.1.0+incompatible.
- [Release notes](https://github.com/moby/moby/releases)
- [Commits](https://github.com/moby/moby/compare/v26.0.1...v26.1.0)

---
updated-dependencies:
- dependency-name: github.com/moby/moby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 19:54:27 +03:00
dependabot[bot] 74b921cf81 chore: bump google.golang.org/api from 0.175.0 to 0.176.1 (#13092)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.175.0 to 0.176.1.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.175.0...v0.176.1)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 16:54:20 +00:00
Kyle Carberry 1b3185c047 chore: lower azure expires soon (#13097)
They haven't provisioned new certificates yet.
2024-04-29 12:34:18 -04:00
Jon Ayers 8269124ab7 feat: sign windows binaries (#13086) 2024-04-29 10:43:27 -05:00
Colin Adler 15157c1c40 chore: add network integration test suite scaffolding (#13072)
* chore: add network integration test suite scaffolding

* dean comments
2024-04-26 17:48:41 +00:00
Cian Johnston 73ba36c9d2 chore(docs): add note regarding Apr 26 scaletest (#13085) 2024-04-26 17:06:36 +01:00
Garrett Delfosse 8ba05a9052 feat: add switch http(s) button to error page (#12942) 2024-04-26 11:52:53 -04:00
Michael Brewer 848ea7e9f1 chore: correct name for github enterprise example (#13083)
Co-authored-by: Muhammad Atif Ali <me@matifali.dev>
2024-04-26 14:43:28 +00:00
Cian Johnston f1ef9fd673 chore(docs): add note regarding vcredist for embedded postgres (#13020) 2024-04-26 10:56:43 +01:00
Mathias Fredriksson d50a31ef62 chore(scripts): auto create autoversion PR from release script (#13074)
Ref #12465
2024-04-26 12:53:22 +03:00
Cian Johnston 365231b1e5 fix(cli): scaletest: ignore errors syncing output (#13076) 2024-04-26 09:18:33 +01:00
Kayla Washburn-Love 74f27719b8 feat: specify a custom "terms of service" link (#13068) 2024-04-25 16:36:51 -06:00
Stephen Kirby 341114a020 chore(docs): remove max_ttl docs (#13077)
* removed MAX_TTL docs, updated template-level scheduling controls

* fmt
2024-04-25 16:13:42 -05:00
Cian Johnston 99dda4a43a fix(agent): keep track of lastReportIndex between invocations of reportLifecycle() (#13075) 2024-04-25 16:54:51 +01:00
Mathias Fredriksson c24b562199 chore(scripts): fix release tagging sanity checks (#13073) 2024-04-25 12:26:37 +03:00
Mathias Fredriksson 46dced9cfe chore(scripts): add release autoversion to bump releases in docs (#13063)
This PR adds a command to bump versions in docs/markdown.

This is still standalone and needs to be wired up.

For now, I'm planning on putting this in `scripts/release.sh` (checkout main -> autoversion (this command) -> commit -> submit PR).

It would be pretty neat to make it a GH actions that's triggered on release though, something for the future.

Part of #12465
2024-04-25 12:11:55 +03:00
Mathias Fredriksson c933c75aa7 chore(scripts): add script to promote mainline to stable (#13054)
Fixes #12459

Example dry-run:

<img width="1229" alt="Screenshot 2024-04-23 at 21 16 55" src="https://github.com/coder/coder/assets/147409/7018d322-501b-41e2-bf47-af3fc39fb3d2">

Example dry-run for non-latest version:

<img width="1228" alt="Screenshot 2024-04-23 at 21 17 52" src="https://github.com/coder/coder/assets/147409/a05fcd44-560f-4e44-81b5-76c071c591b4">

**Note:** This PR does not yet update docs to reflect the promoted version. This will be part of #12465.
2024-04-24 22:59:22 +03:00
Mathias Fredriksson b82a782619 chore(scripts): implement mainline and stable release channels (#13048)
Fixes #12458
2024-04-24 19:43:11 +00:00
Frederik Dudzik a6af7a5e3d chore(README): add contributing section to readme (#13059) 2024-04-24 22:15:14 +03:00
Michael Smith 3f21cb8a2f fix: update API code to use Axios instances (#13029)
* fix: update API code to use Axios instance

* docs: fix typo

* fix: update all global axios imports to use Coder instance

* fix: remove needless import

* fix: update import order

* refactor: rename coderAxiosInstance to axiosInstance

* docs: update variable reference in FE contributing docs
2024-04-24 17:01:23 +00:00
Stephen Kirby dd27a8a634 updated helm install flags to match patches (#13064) 2024-04-24 10:26:36 -05:00
dependabot[bot] 39ccff97c1 chore: bump github.com/gohugoio/hugo from 0.125.2 to 0.125.3 (#13057)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-24 13:39:52 +00:00
Frederik Dudzik 8b6227d031 chore(docs): add k8s terraform link to k8s installation docs (#13040)
* add k8s terraform link to k8s installation docs

* Update kubernetes.md
2024-04-24 06:28:16 -04:00
Cian Johnston a518047f10 chore(coderd): provisionerdserver: downgrade heartbeat failure log to Warn instead of Error (#13061) 2024-04-24 09:36:36 +01:00
Pavel Aseev 4682355eed chore: deprecate gauge metrics with _total suffix (#12744) (#12976)
* chore: deprecate gauge metrics with _total suffix (#12744)

Deprecated metrics:
- coderd_oauth2_external_requests_rate_limit_total
- coderd_api_workspace_latest_build_total

* Apply suggestions from code review

add link to follow-up issue

Co-authored-by: Cian Johnston <public@cianjohnston.ie>

---------

Co-authored-by: Cian Johnston <public@cianjohnston.ie>
2024-04-24 11:23:24 +03:00
Frederik Dudzik 5780050493 chore(docs): fix broken links (#13056)
* fix broken links

* fmt
2024-04-24 11:21:22 +03:00
Cian Johnston a04c76ce40 ci: release: test migrations before building (#13051) 2024-04-24 08:31:01 +01:00
Kayla Washburn-Love 215dd7b152 feat: show version on login page (#13033) 2024-04-23 11:18:56 -06:00
Mathias Fredriksson a69fc657f2 chore(coderd/database): reduce dbpurge load with smaller batches of agent stats (#13049) 2024-04-23 15:01:56 +03:00
Bruno Quaresma 2f7f9d022a refactor(site): reorganize template schedule settings form (#13031)
Close https://github.com/coder/coder/issues/12617

**Demo**

https://github.com/coder/coder/assets/3165839/66d4f238-d31f-4ee8-a3de-ce68215b0492

**Autostop**
![Image](https://github.com/coder/coder/assets/3165839/7d7430b9-fdb6-4842-ab2d-3b22cebe579e)

**Autostart**
![Image](https://github.com/coder/coder/assets/3165839/fd65865e-f996-4b17-b16b-679fd8c6b449)

**Dormancy**
![Image](https://github.com/coder/coder/assets/3165839/625e4769-7742-47c7-bce8-b33a54abaa34)
2024-04-23 08:59:19 -03:00
Cian Johnston e57ca3cdaa feat(scripts): add script to check schema between migrations (#13037)
- migrations: allow passing in a custom migrate.FS
- gen/dump: extract some functions to dbtestutil
- scripts: write script to test migrations
2024-04-23 12:43:14 +01:00
dependabot[bot] 81fcdf717b chore: bump github.com/gohugoio/hugo from 0.124.0 to 0.125.2 (#13024)
* chore: bump github.com/gohugoio/hugo from 0.124.0 to 0.125.2

Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from 0.124.0 to 0.125.2.
- [Release notes](https://github.com/gohugoio/hugo/releases)
- [Changelog](https://github.com/gohugoio/hugo/blob/master/hugoreleaser.toml)
- [Commits](https://github.com/gohugoio/hugo/compare/v0.124.0...v0.125.2)

---
updated-dependencies:
- dependency-name: github.com/gohugoio/hugo
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* add license to allowlist

* syntax

* wrong format

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jon Ayers <jon@coder.com>
2024-04-23 02:27:32 +00:00
Kayla Washburn-Love fab5591cf6 chore: change site_configs.value to text (#13036)
* chore: change `site_configs.value` to `text`

* `make gen`
2024-04-22 17:25:36 -06:00
Kyle Carberry d3f3ace220 chore: reduce dashboard requests from seeded data (#13034)
* chore: reduce requests the dashboard makes from seeded data

We already inject all of this content in `index.html`.

There was also a bug with displaying a loading indicator when
the workspace proxies endpoint 404s.

* Fix first user fetch

* Add util

* Add cached query for entitlements and experiments

* Fix authmethods unnecessary request

* Fix unnecessary region request

* Fix fmt

* Debug

* Fix test
2024-04-22 16:07:56 -04:00
Kyle Carberry 8d1220e0c8 chore: add generate script for azure instance identity (#13028)
* chore: add generate script for azure instance identity

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

* Fix shell lint

* Fix shell fmt

* Fix RSA issuing certificate
2024-04-22 15:39:08 -04:00
Michael Brewer 7bd1b3bdb8 chore: fix broken mainline link (#13015)
* docs(releases): fix 404 for mainline link

* Delete x.sh

* Update releases.md

* docs: format and use term 'bleeding edge' for mainline releases
2024-04-22 10:57:59 -05:00
dependabot[bot] 3af317317a ci: bump crate-ci/typos from 1.19.0 to 1.20.9 in the github-actions group (#13027)
* ci: bump crate-ci/typos in the github-actions group

Bumps the github-actions group with 1 update: [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `crate-ci/typos` from 1.19.0 to 1.20.9
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.19.0...v1.20.9)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* Add `pn` exclusion

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kyle Carberry <kyle@carberry.com>
2024-04-22 10:25:23 -04:00
dependabot[bot] 2e49fa94d4 chore: bump github.com/coder/terraform-provider-coder (#13022)
Bumps [github.com/coder/terraform-provider-coder](https://github.com/coder/terraform-provider-coder) from 0.20.1 to 0.21.0.
- [Release notes](https://github.com/coder/terraform-provider-coder/releases)
- [Changelog](https://github.com/coder/terraform-provider-coder/blob/main/.goreleaser.yml)
- [Commits](https://github.com/coder/terraform-provider-coder/compare/v0.20.1...v0.21.0)

---
updated-dependencies:
- dependency-name: github.com/coder/terraform-provider-coder
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 08:23:35 -04:00
dependabot[bot] ea472c5388 chore: bump google.golang.org/api from 0.172.0 to 0.175.0 (#13026)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.172.0 to 0.175.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.172.0...v0.175.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 08:23:22 -04:00
Michael Brewer d2acb6776e chore: fix link to install (#13019) 2024-04-22 08:23:09 -04:00
Mathias Fredriksson 3adcccb618 fix(coderd/database): reduce db load via dbpurge advisory locking (#13021) 2024-04-22 11:10:32 +00:00
Aaron Lehmann 8a1216254e feat(cli): add --env flag for coder ssh (#12991)
This allows environment variables to be set on the SSH session.

Example:

   coder ssh myworkspace --env VAR1=val1,VAR2=val2
2024-04-22 13:13:48 +03:00
Mathias Fredriksson e17e8aa3c9 feat(coderd/database): keep only 1 day of workspace_agent_stats after rollup (#12674) 2024-04-22 13:11:50 +03:00
Michael Brewer 4a6693a171 chore: fix 404 for managed terraform variables (#13018) 2024-04-22 13:09:05 +03:00
Frederik Dudzik b40f54f603 chore(docs): make external auth docs easier to follow (#12970)
* add additional context to github external auth provider documentation

* Apply suggestions from code review

Co-authored-by: Kyle Carberry <kyle@carberry.com>

* Update docs/admin/external-auth.md

* fmt

* fmt

---------

Co-authored-by: Kyle Carberry <kyle@carberry.com>
2024-04-20 16:26:53 +00:00
Marcin Tojek 3d7740bd32 test(site): add e2e tests for workspace proxies (#13009) 2024-04-19 14:45:52 +02:00
Colin Adler 3aa0d73811 chore: fix down migration 196 (#13006)
It didn't account for null values.
2024-04-18 18:47:02 -05:00
Danny Kopping 319fd5bf1d chore: add e2e test against an external auth provider during workspace creation (#12985) 2024-04-18 19:43:10 +02:00
Marcin Tojek 75223dfd8b test(site): add e2e tests for observability 2024-04-18 12:50:34 +02:00
Ben Potter f5a32b3f27 docs: explain that mainline stays around for one month now (#12993)
* docs: mainline stays around for one month

* switch to .x to encourage latest
2024-04-17 20:10:40 -05:00
Dean Sheather d426569d4a fix: make terminal raw in ssh command on windows (#12990) 2024-04-17 18:01:20 +00:00
Mathias Fredriksson 92190443ff fix(coderd/metricscache): avoid logging error for no rows (#12988)
Fixes #12938
2024-04-17 20:43:13 +03:00
Colin Adler 6b4eb03192 chore: give additional time in tests for tailnetAPIConnector graceful disconnect (#12980)
Failure seen here: https://github.com/coder/coder/actions/runs/8711258577/job/23894964182?pr=12979
2024-04-17 12:38:17 -05:00
dependabot[bot] 3338cdca77 chore: bump github.com/moby/moby (#12960)
Bumps [github.com/moby/moby](https://github.com/moby/moby) from 25.0.2+incompatible to 26.0.1+incompatible.
- [Release notes](https://github.com/moby/moby/releases)
- [Commits](https://github.com/moby/moby/compare/v25.0.2...v26.0.1)

---
updated-dependencies:
- dependency-name: github.com/moby/moby
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-17 13:30:53 -04:00
Kyle Carberry 227e632053 fix: add grace period before showing replicas license error (#12989)
Fixes #8665.
2024-04-17 13:30:32 -04:00
Danny Kopping b85d5d8491 feat: add warning about use of old/removed/invalid experiments (#12962) 2024-04-17 16:59:31 +02:00
Marcin Tojek cb8c576c93 test(site): add e2e tests for network (#12987) 2024-04-17 16:06:49 +02:00
Marcin Tojek ee7dda8111 refactor(site): verify deployment config flags in e2e tests (#12986) 2024-04-17 11:51:55 +00:00
Jon Ayers 0c993566dd hotfix: skip dependency license review on main (#12982) 2024-04-16 23:08:22 -05:00
Jon Ayers 80f5978124 chore: add license review to CI (#12981) 2024-04-16 22:52:07 -05:00
Colin Adler 777dfbe965 feat(enterprise): add ready for handshake support to pgcoord (#12935) 2024-04-16 15:01:10 -05:00
Colin Adler 942e90270e fix: disable azureidentity test on darwin (#12979)
See https://github.com/coder/coder/issues/12978
2024-04-16 19:33:04 +00:00
Colin Adler 231fc26c92 fix(coderd): properly calculate query latency for tailnet queries (#12944)
The defer used seems correct, but the `time.Since` will always resolve
immediately since it's a param to the deferred function.
2024-04-16 19:03:27 +00:00
Colin Adler ba52a4fbe2 chore: fix linting issue (#12945)
The error wasn't used.
2024-04-16 13:50:46 -05:00
Cian Johnston 8e1e0f04a4 feat(cli): support bundle: show links to docs/admin/healthcheck (#12974) 2024-04-16 16:21:09 +01:00
Marcin Tojek b598aef543 test(site): add e2e tests for user auth (#12971) 2024-04-16 14:32:07 +02:00
Cian Johnston 407e61ecd4 feat(cli): support bundle: dump healthcheck summary (#12963)
* refactor(codersdk): extract common fields from HealthReport and friends
* feat(codersdk/healthsdk): add Summarize() method
* feat(cli): support bundle: dump healthcheck summary
2024-04-16 13:31:56 +01:00
Danny Kopping 06e042acfa chore: apply shellcheck recommendation which was causing "make lint" to fail locally (#12972) 2024-04-16 13:06:08 +02:00
Kyle Carberry 41ca6e4f7f chore: add created_at to workspace resource telemetry (#12969) 2024-04-15 20:06:59 +00:00
Garrett Delfosse 3ab5a51ec2 feat: add listening ports protocol selector (#12915) 2024-04-15 15:00:24 -04:00
Marcin Tojek 49689162bb test(site): add e2e tests for security (#12961) 2024-04-15 19:31:33 +02:00
Cian Johnston 9a4703a311 feat(coderd/healthcheck): improve detection of STUN issues (#12951)
Adds checks to coderd/healthcheck/derphealth for STUN issues:
- Alerts if there is not least one healthy STUN server,
- Alerts if we see variable port mapping.
2024-04-15 17:10:49 +01:00
Stephen Kirby c13909a1a2 chore: fix broken links in the jfrog guide (#12835)
* replaced jfrog guide links

* replaced github link

* fmt

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2024-04-15 09:51:58 -05:00
Steven Masley d9da054c9d chore: update generated array type definitions in TypeScript to be readonly (#12947)
* chore: types generated handling readonly slices

* add -update flag to update goldens

* revert excess gens

* fix: update most UI types to account for readonly modifiers

* fix: remove accidental mutation from NavBarView

* fix: remove mutation warning for BatchUpdateConfirmation stories

* fix: remove mutation warning for BactchUpdateConfirmation

* fix: format ActiveUserChart

* fix: update import to make linter happy

* fix: update fmt issue

* fix: disable file write lint rule from unit test

---------

Co-authored-by: Parkreiner <throwawayclover@gmail.com>
2024-04-15 09:46:10 -04:00
Kayla Washburn-Love 7cf8577f1c label some template settings as enterprise (#12952) 2024-04-15 09:24:11 -04:00
Kyle Carberry d3790bb5be fix: use provided username when fetching workspaces (#12955) 2024-04-13 14:39:57 -04:00
Kayla Washburn-Love 00fcf36999 test: add an e2e audit logs test (#12868) 2024-04-12 14:01:54 -06:00
Marcin Tojek cf2d2a98bd test(site): add e2e tests for appearance (#12950) 2024-04-12 14:46:44 +02:00
Cian Johnston b71af32113 chore(docs): add support bundle guide (#12931)
Adds a guide explaining support bundles.
2024-04-12 10:11:05 +01:00
Marcin Tojek dcf1d3a9ae test(site): add e2e tests for experiments (#12940) 2024-04-12 10:42:27 +02:00
Cian Johnston b163bc7f01 fix(support): correctly rename existing agent connection info, add real netcheck (#12946) 2024-04-12 09:40:04 +01:00
Kayla Washburn-Love c5367c201b test: fix url checks in e2e tests (#12881) 2024-04-11 15:48:53 -06:00
Steven Masley 93b46fe1f6 chore: skip global.setup if first user already exists (#12930)
* chore: skip global.setup if first user already exists

treat test as a setup, rather than a test

Co-authored-by: Kayla Washburn-Love <mckayla@hey.com>

---------

Co-authored-by: Kayla Washburn-Love <mckayla@hey.com>
2024-04-11 21:10:40 +00:00
Kayla Washburn-Love 2ad7fcc0b7 fix: show template autostop setting when it overrides the workspace setting (#12910) 2024-04-11 13:08:51 -06:00
Steven Masley 22785a307c chore: add -agpl to agpl e2e artifacts (#12943)
* chore: -agpl added to agpl e2e artifacts

Before was doing 'false' at the end of artifacts
2024-04-11 16:57:40 +00:00
Steven Masley b9936a4671 chore: deconflict e2e enterprise and AGPL artifacts in ci (#12941) 2024-04-11 09:42:21 -05:00
Cian Johnston fad97a14f9 fix(cli): allow generating partial support bundles with no workspace or agent (#12933)
* fix(cli): allow generating partial support bundles with no workspace or agent

* nolint control flag
2024-04-11 10:09:10 +01:00
Spike Curtis a231b5aef5 feat: add src_id and dst_id indexes to tailnet_tunnels (#12911)
Fixes #12780

Adds indexes to the `tailnet_tunnels` table to speed up `GetTailnetTunnelPeerIDs` and `GetTailnetTunnelPeerBindings` queries, which match on `src_id` and `dst_id`.
2024-04-11 10:05:53 +04:00
Stephen Kirby ab116af543 added releases.md to manifest (#12936) 2024-04-10 18:31:21 -05:00
Steven Masley 8da8b89af7 test: verify actually uploaded license with assert (#12934)
Prior page.GetByText did not assert it existed
2024-04-10 18:02:08 -05:00
Colin Adler e801e878ba feat: add agent acks to in-memory coordinator (#12786)
When an agent receives a node, it responds with an ACK which is relayed
to the client. After the client receives the ACK, it's allowed to begin
pinging.
2024-04-10 17:15:33 -05:00
Kayla Washburn-Love 9cf2358114 ci: execute enterprise and non-enterprise e2e tests concurrently (#12872) 2024-04-10 15:42:53 -06:00
Steven Masley 7fd9a75ad9 chore: nix shell to support playwright e2e tests (#12917)
* chore: nix shell to support playwright e2e tests

nix is running an older version of chromium, so had to reduce the
playwright version.

* Add to e2e readme

* add enterprise test comment

* add note about install to readme

* make fmt

* remove shellhook message

Co-authored-by: Kayla Washburn-Love <mckayla@hey.com>

* add link to nixos playwright package to get version

* formatting

---------

Co-authored-by: Kayla Washburn-Love <mckayla@hey.com>
2024-04-10 14:08:25 -05:00
Steven Masley 566f8f231d chore: add unit test for pass through external auth query params (#12928)
* chore: verify pass through external auth query params

Unit test added to verify behavior of query params set in the
auth url for external apps. This behavior is intended to specifically
support Auth0 audience query param.
2024-04-10 13:58:29 -05:00
Spike Curtis 06eae954c9 fix: stop sending DeleteTailnetPeer when coordinator is unhealthy (#12925)
fixes #12923

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

It does this by checking the health of the querier before accepting a connection, rather than unconditionally accepting it only for it to get swatted down later.
2024-04-10 22:49:13 +04:00
Steven Masley a607d5610e chore: disable pgcoord (HA) when --in-memory (#12919)
* chore: disable pgcoord (HA) when --in-memory

HA does not make any sense while using in-memory database
2024-04-10 11:05:55 -05:00
Steven Masley 838e8df5be chore: merge apikey/token session config values (#12817)
* chore: merge apikey/token session config values

There is a confusing difference between an apikey and a token. This
difference leaks into our configs. This change does not resolve the
difference. It only groups the config values to try and manage any
bloat that occurs from adding more similar config values
2024-04-10 10:34:49 -05:00
Steven Masley 4dc293d930 chore: add date information to windows startup logs (#12905) 2024-04-10 09:41:05 -05:00
Marcin Tojek e266ecf91b test(site): fix flaky outdated agent test (#12927) 2024-04-10 16:09:44 +02:00
Garrett Delfosse acaa254099 feat: link with protocol on shared ports (#12908) 2024-04-10 09:29:24 -04:00
Marcin Tojek 2f2a395ba9 e2e tests for deployment/licenses (#12926) 2024-04-10 15:00:39 +02:00
Marcin Tojek b6359b0a89 fix: ignore gomock temporary files (#12924) 2024-04-10 08:48:56 +00:00
Spike Curtis 5469011018 fix: stop logging session shutdown as warning (#12922)
A customer hit like 200k of ErrSessionShutdown, which just dupes any errors we would have generated when shutting down the session for e.g. Ping failures.
2024-04-10 11:50:46 +04:00
Steven Masley 0a8c8ce5cc chore: remove InsertWorkspaceAgentStat query (#12869)
* chore: remove InsertWorkspaceAgentStat query

InsertWorkspaceAgentStats (batch) exists. We only used the singular in
a single unit test place. Removing the single for the batch, reducing
the interface size.
2024-04-09 12:35:27 -05:00
Garrett Delfosse 1d4bf30c0d feat: add s suffix to use HTTPS for ports (#12862) 2024-04-09 12:06:22 -04:00
Steven Masley 189b8626d0 chore: deprecate agent report-stats endpoint (#12880)
* chore: deprecate agent report-stats endpoint

Agent API is now used instead.

* Update coderd/workspaceagents.go

Co-authored-by: Spike Curtis <spike@coder.com>

---------

Co-authored-by: Spike Curtis <spike@coder.com>
2024-04-09 09:38:26 -05:00
Marcin Tojek 08451ce80c feat: remove health link from deployment sidebar (#12914) 2024-04-09 13:47:47 +01:00
Mathias Fredriksson 0178bfe134 fix(examples): copy /etc/skel on init in docker template (#12913)
Fixes #10209
2024-04-09 14:54:17 +03:00
Marcin Tojek 28754a79e5 docs: describe air-gapped architecture (#12897) 2024-04-09 12:33:06 +02:00
coryb d82f2fd416 fix: update typo in audit log field (#12907) 2024-04-08 13:57:38 -05:00
dependabot[bot] 7179c86df3 chore: bump golang.org/x/oauth2 from 0.18.0 to 0.19.0 (#12893)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 11:52:35 -05:00
dependabot[bot] 11123018a2 chore: bump google.golang.org/grpc from 1.62.1 to 1.63.0 (#12892)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.62.1 to 1.63.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.62.1...v1.63.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 15:57:52 +00:00
dependabot[bot] 589434e8d8 chore: bump golang.org/x/tools from 0.19.0 to 0.20.0 (#12890)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 10:46:16 -05:00
dependabot[bot] 9a7d8034cb chore: bump golang.org/x/net from 0.22.0 to 0.24.0 (#12888)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.24.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 10:31:09 -05:00
dependabot[bot] f99fd807b1 chore: bump golang.org/x/sync from 0.6.0 to 0.7.0 (#12895)
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.6.0 to 0.7.0.
- [Commits](https://github.com/golang/sync/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 10:30:47 -05:00
dependabot[bot] 8ba8ec2f19 chore: bump github.com/elastic/go-sysinfo from 1.13.1 to 1.14.0 (#12894)
Bumps [github.com/elastic/go-sysinfo](https://github.com/elastic/go-sysinfo) from 1.13.1 to 1.14.0.
- [Release notes](https://github.com/elastic/go-sysinfo/releases)
- [Commits](https://github.com/elastic/go-sysinfo/compare/v1.13.1...v1.14.0)

---
updated-dependencies:
- dependency-name: github.com/elastic/go-sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 10:30:32 -05:00
dependabot[bot] 24135a2d0f chore: bump golang.org/x/term from 0.18.0 to 0.19.0 (#12886)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/term/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 15:27:46 +03:00
Spike Curtis 3b7380fa00 fix: fix race in assertWorkspaceLastUsedAtUpdated (#12899)
fixes #12789

Stats are collected asynchronously with respect to sessions ending.  Flush repeatedly so that we pick up the collection if we missed it.
2024-04-08 16:22:33 +04:00
Garrett Delfosse f96ce80ab9 feat: add owner groups to workspace data (#12841) 2024-04-05 15:06:17 -04:00
Kayla Washburn-Love c4b26f335a test: verify that enterprise tests are being run (#12871) 2024-04-05 11:45:32 -06:00
Colin Adler a2b28f80d7 fix(coderd): prevent agent reverse proxy from using HTTP[S]_PROXY envs (#12875)
Updates https://github.com/coder/coder/issues/12790
2024-04-05 12:29:08 -05:00
Mathias Fredriksson b06452ee88 fix(install.sh): remove extracted files after installation (#12879) 2024-04-05 19:04:12 +03:00
Marcin Tojek 7c0fac9906 docs: describe devcontainers as deployment model (#12877) 2024-04-05 15:30:49 +02:00
Mathias Fredriksson c243210ae5 fix(install.sh): change post-install advisory when installing specific version (#12878) 2024-04-05 15:55:11 +03:00
Michael Brewer 61e5721caa fix(install.sh): use --version when provided (#12873) 2024-04-05 13:14:49 +03:00
Bruno Quaresma 3fbcdb0ddc chore(site): add e2e tests for groups (#12866) 2024-04-04 21:56:28 -03:00
Kayla Washburn-Love bc9ea61eb4 ci: disable enterprise e2e tests temporarily (#12874) 2024-04-04 17:39:07 -06:00
Marcin Tojek 90efa1b846 docs: describe multi-cloud architecture (#12857) 2024-04-04 15:42:26 +02:00
Bruno Quaresma 41b8ff3e81 chore(site): add e2e to test add and remove user (#12851) 2024-04-04 09:21:03 -03:00
1064 changed files with 48274 additions and 16684 deletions
+2 -2
View File
@@ -4,12 +4,12 @@ description: |
inputs:
version:
description: "The Go version to use."
default: "1.21.9"
default: "1.22.4"
runs:
using: "composite"
steps:
- name: Setup Go
uses: buildjet/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ inputs.version }}
+2 -2
View File
@@ -11,11 +11,11 @@ runs:
using: "composite"
steps:
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v3
with:
version: 8
- name: Setup Node
uses: buildjet/setup-node@v3
uses: actions/setup-node@v4.0.1
with:
node-version: 18.19.0
# See https://github.com/actions/setup-node#caching-global-packages-data
+1 -1
View File
@@ -7,5 +7,5 @@ runs:
- name: Install Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.5.7
terraform_version: 1.8.4
terraform_wrapper: false
-43
View File
@@ -1,43 +0,0 @@
codecov:
require_ci_to_pass: false
notify:
after_n_builds: 5
comment: false
github_checks:
annotations: false
coverage:
range: 50..75
round: down
precision: 2
status:
patch:
default:
informational: yes
project:
default:
target: 65%
informational: true
ignore:
# This is generated code.
- coderd/database/models.go
- coderd/database/queries.sql.go
- coderd/database/databasefake
# These are generated or don't require tests.
- cmd
- coderd/tunnel
- coderd/database/dump
- coderd/database/postgres
- peerbroker/proto
- provisionerd/proto
- provisionersdk/proto
- scripts
- site/.storybook
- rules.go
# Packages used for writing tests.
- cli/clitest
- coderd/coderdtest
- pty/ptytest
+3 -45
View File
@@ -61,7 +61,9 @@ updates:
- dependency-name: "terraform"
- package-ecosystem: "npm"
directory: "/site/"
directories:
- "/site"
- "/offlinedocs"
schedule:
interval: "monthly"
time: "06:00"
@@ -82,47 +84,3 @@ updates:
update-types:
- version-update:semver-major
open-pull-requests-limit: 15
groups:
site:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/offlinedocs/"
schedule:
interval: "monthly"
time: "06:00"
timezone: "America/Chicago"
reviewers:
- "coder/ts"
commit-message:
prefix: "chore"
labels: []
ignore:
# Ignore patch updates for all dependencies
- dependency-name: "*"
update-types:
- version-update:semver-patch
# Ignore major updates to Node.js types, because they need to
# correspond to the Node.js engine version
- dependency-name: "@types/node"
update-types:
- version-update:semver-major
groups:
offlinedocs:
patterns:
- "*"
# Update dogfood.
- package-ecosystem: "terraform"
directory: "/dogfood/"
schedule:
interval: "weekly"
time: "06:00"
timezone: "America/Chicago"
commit-message:
prefix: "chore"
labels: []
ignore:
# We likely want to update this ourselves.
- dependency-name: "coder/coder"
+11 -10
View File
@@ -86,6 +86,7 @@ provider "kubernetes" {
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
os = "linux"
@@ -175,21 +176,21 @@ resource "coder_app" "code-server" {
resource "kubernetes_persistent_volume_claim" "home" {
metadata {
name = "coder-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}-home"
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
namespace = var.namespace
labels = {
"app.kubernetes.io/name" = "coder-pvc"
"app.kubernetes.io/instance" = "coder-pvc-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}"
"app.kubernetes.io/instance" = "coder-pvc-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
"app.kubernetes.io/part-of" = "coder"
//Coder-specific labels.
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace.me.owner_id
"com.coder.user.username" = data.coder_workspace.me.owner
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
annotations = {
"com.coder.user.email" = data.coder_workspace.me.owner_email
"com.coder.user.email" = data.coder_workspace_owner.me.email
}
}
wait_until_bound = false
@@ -210,20 +211,20 @@ resource "kubernetes_deployment" "main" {
]
wait_for_rollout = false
metadata {
name = "coder-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}"
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
namespace = var.namespace
labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = "coder-workspace-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}"
"app.kubernetes.io/instance" = "coder-workspace-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
"app.kubernetes.io/part-of" = "coder"
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace.me.owner_id
"com.coder.user.username" = data.coder_workspace.me.owner
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
annotations = {
"com.coder.user.email" = data.coder_workspace.me.owner_email
"com.coder.user.email" = data.coder_workspace_owner.me.email
}
}
+187 -94
View File
@@ -37,8 +37,10 @@ jobs:
k8s: ${{ steps.filter.outputs.k8s }}
ci: ${{ steps.filter.outputs.ci }}
db: ${{ steps.filter.outputs.db }}
gomod: ${{ steps.filter.outputs.gomod }}
offlinedocs-only: ${{ steps.filter.outputs.offlinedocs_count == steps.filter.outputs.all_count }}
offlinedocs: ${{ steps.filter.outputs.offlinedocs }}
tailnet-integration: ${{ steps.filter.outputs.tailnet-integration }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -90,6 +92,9 @@ jobs:
- "scaletest/**"
- "tailnet/**"
- "testutil/**"
gomod:
- "go.mod"
- "go.sum"
ts:
- "site/**"
- "Makefile"
@@ -103,15 +108,38 @@ jobs:
- ".github/workflows/ci.yaml"
offlinedocs:
- "offlinedocs/**"
tailnet-integration:
- "tailnet/**"
- "go.mod"
- "go.sum"
- id: debug
run: |
echo "${{ toJSON(steps.filter )}}"
update-flake:
needs: changes
if: needs.changes.outputs.gomod == 'true'
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Update Nix Flake SRI Hash
run: ./scripts/update-flake.sh
- name: Ensure No Changes
run: git diff --exit-code
lint:
needs: changes
if: needs.changes.outputs.offlinedocs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -132,7 +160,7 @@ jobs:
echo "LINT_CACHE_DIR=$dir" >> $GITHUB_ENV
- name: golangci-lint cache
uses: buildjet/cache@v4
uses: actions/cache@v4
with:
path: |
${{ env.LINT_CACHE_DIR }}
@@ -142,7 +170,7 @@ jobs:
# Check for any typos
- name: Check for typos
uses: crate-ci/typos@v1.19.0
uses: crate-ci/typos@v1.22.9
with:
config: .github/workflows/typos.toml
@@ -163,9 +191,15 @@ jobs:
run: |
make --output-sync=line -j lint
- name: Check workflow files
run: |
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 1.6.22
./actionlint -color -shellcheck= -ignore "set-output"
shell: bash
gen:
timeout-minutes: 8
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.docs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
steps:
@@ -183,6 +217,9 @@ jobs:
- name: Setup sqlc
uses: ./.github/actions/setup-sqlc
- name: Setup Terraform
uses: ./.github/actions/setup-tf
- name: go install tools
run: |
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
@@ -212,7 +249,7 @@ jobs:
fmt:
needs: changes
if: needs.changes.outputs.offlinedocs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
timeout-minutes: 7
steps:
- name: Checkout
@@ -223,12 +260,9 @@ jobs:
- name: Setup Node
uses: ./.github/actions/setup-node
# Use default Go version
- name: Setup Go
uses: buildjet/setup-go@v5
with:
# This doesn't need caching. It's super fast anyways!
cache: false
go-version: 1.21.9
uses: ./.github/actions/setup-go
- name: Install shfmt
run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.7.0
@@ -242,7 +276,7 @@ jobs:
run: ./scripts/check_unstaged.sh
test-go:
runs-on: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'coder' && 'buildjet-4vcpu-ubuntu-2204' || matrix.os == 'macos-latest' && github.repository_owner == 'coder' && 'macos-latest-xlarge' || matrix.os == 'windows-2022' && github.repository_owner == 'coder' && 'windows-latest-16-cores' || matrix.os }}
runs-on: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'coder' && 'depot-ubuntu-22.04-4' || matrix.os == 'macos-latest' && github.repository_owner == 'coder' && 'macos-latest-xlarge' || matrix.os == 'windows-2022' && github.repository_owner == 'coder' && 'windows-latest-16-cores' || matrix.os }}
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 20
@@ -269,16 +303,6 @@ jobs:
id: test
shell: bash
run: |
# Code coverage is more computationally expensive and also
# prevents test caching, so we disable it on alternate operating
# systems.
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
echo "cover=true" >> $GITHUB_OUTPUT
export COVERAGE_FLAGS='-covermode=atomic -coverprofile="gotests.coverage" -coverpkg=./...'
else
echo "cover=false" >> $GITHUB_OUTPUT
fi
# if macOS, install google-chrome for scaletests. As another concern,
# should we really have this kind of external dependency requirement
# on standard CI?
@@ -297,7 +321,7 @@ jobs:
fi
export TS_DEBUG_DISCO=true
gotestsum --junitfile="gotests.xml" --jsonfile="gotests.json" \
--packages="./..." -- $PARALLEL_FLAG -short -failfast $COVERAGE_FLAGS
--packages="./..." -- $PARALLEL_FLAG -short -failfast
- name: Upload test stats to Datadog
timeout-minutes: 1
@@ -307,21 +331,8 @@ jobs:
with:
api-key: ${{ secrets.DATADOG_API_KEY }}
- name: Check code coverage
uses: codecov/codecov-action@v4
# This action has a tendency to error out unexpectedly, it has
# the `fail_ci_if_error` option that defaults to `false`, but
# that is no guarantee, see:
# https://github.com/codecov/codecov-action/issues/788
continue-on-error: true
if: steps.test.outputs.cover && github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./gotests.coverage
flags: unittest-go-${{ matrix.os }}
test-go-pg:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
needs:
- changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
@@ -343,8 +354,10 @@ jobs:
uses: ./.github/actions/setup-tf
- name: Test with PostgreSQL Database
env:
POSTGRES_VERSION: "13"
TS_DEBUG_DISCO: "true"
run: |
export TS_DEBUG_DISCO=true
make test-postgres
- name: Upload test stats to Datadog
@@ -355,21 +368,48 @@ jobs:
with:
api-key: ${{ secrets.DATADOG_API_KEY }}
- name: Check code coverage
uses: codecov/codecov-action@v4
# This action has a tendency to error out unexpectedly, it has
# the `fail_ci_if_error` option that defaults to `false`, but
# that is no guarantee, see:
# https://github.com/codecov/codecov-action/issues/788
continue-on-error: true
if: github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
# NOTE: this could instead be defined as a matrix strategy, but we want to
# only block merging if tests on postgres 13 fail. Using a matrix strategy
# here makes the check in the above `required` job rather complicated.
test-go-pg-16:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
needs:
- changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
# This timeout must be greater than the timeout set by `go test` in
# `make test-postgres` to ensure we receive a trace of running
# goroutines. Setting this to the timeout +5m should work quite well
# even if some of the preceding steps are slow.
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./gotests.coverage
flags: unittest-go-postgres-linux
fetch-depth: 1
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Setup Terraform
uses: ./.github/actions/setup-tf
- name: Test with PostgreSQL Database
env:
POSTGRES_VERSION: "16"
TS_DEBUG_DISCO: "true"
run: |
make test-postgres
- name: Upload test stats to Datadog
timeout-minutes: 1
continue-on-error: true
uses: ./.github/actions/upload-datadog
if: success() || failure()
with:
api-key: ${{ secrets.DATADOG_API_KEY }}
test-go-race:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 25
@@ -397,8 +437,36 @@ jobs:
with:
api-key: ${{ secrets.DATADOG_API_KEY }}
# Tailnet integration tests only run when the `tailnet` directory or `go.sum`
# and `go.mod` are changed. These tests are to ensure we don't add regressions
# to tailnet, either due to our code or due to updating dependencies.
#
# These tests are skipped in the main go test jobs because they require root
# and mess with networking.
test-go-tailnet-integration:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
needs: changes
# Unnecessary to run on main for now
if: needs.changes.outputs.tailnet-integration == 'true' || needs.changes.outputs.ci == 'true'
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Go
uses: ./.github/actions/setup-go
# Used by some integration tests.
- name: Install Nginx
run: sudo apt-get update && sudo apt-get install -y nginx
- name: Run Tests
run: make test-tailnet-integration
test-js:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 20
@@ -414,24 +482,20 @@ jobs:
- run: pnpm test:ci --max-workers $(nproc)
working-directory: site
- name: Check code coverage
uses: codecov/codecov-action@v4
# This action has a tendency to error out unexpectedly, it has
# the `fail_ci_if_error` option that defaults to `false`, but
# that is no guarantee, see:
# https://github.com/codecov/codecov-action/issues/788
continue-on-error: true
if: github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./site/coverage/lcov.info
flags: unittest-js
test-e2e:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-16vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
variant:
- enterprise: false
name: test-e2e
- enterprise: true
name: test-e2e-enterprise
name: ${{ matrix.variant.name }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -444,52 +508,40 @@ jobs:
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Setup Terraform
uses: ./.github/actions/setup-tf
# Assume that the checked-in versions are up-to-date
- run: make gen/mark-fresh
name: make gen
- name: go install tools
run: |
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/mikefarah/yq/v4@v4.30.6
go install go.uber.org/mock/mockgen@v0.4.0
- name: Install Protoc
run: |
mkdir -p /tmp/proto
pushd /tmp/proto
curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip
unzip protoc.zip
cp -r ./bin/* /usr/local/bin
cp -r ./include /usr/local/bin/include
popd
- name: Build
run: |
make -B site/out/index.html
- run: pnpm build
working-directory: site
- run: pnpm playwright:install
working-directory: site
# Run tests that don't require an enterprise license without an enterprise license
- run: pnpm playwright:test --forbid-only --workers 1
if: ${{ !matrix.variant.enterprise }}
env:
DEBUG: pw:api
working-directory: site
# Run all of the tests with an enterprise license
- run: pnpm playwright:test --forbid-only --workers 1
if: ${{ matrix.variant.enterprise }}
env:
DEBUG: pw:api
CODER_E2E_ENTERPRISE_LICENSE: ${{ secrets.CODER_E2E_ENTERPRISE_LICENSE }}
CODER_E2E_REQUIRE_ENTERPRISE_TESTS: "1"
working-directory: site
# Temporarily allow these to fail so that I can gather data about which
# tests are failing.
continue-on-error: true
- name: Upload Playwright Failed Tests
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
uses: actions/upload-artifact@v4
with:
name: failed-test-videos
name: failed-test-videos${{ matrix.variant.enterprise && '-enterprise' || '-agpl' }}
path: ./site/test-results/**/*.webm
retention-days: 7
@@ -497,7 +549,7 @@ jobs:
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
uses: actions/upload-artifact@v4
with:
name: debug-pprof-dumps
name: debug-pprof-dumps${{ matrix.variant.enterprise && '-enterprise' || '-agpl' }}
path: ./site/test-results/**/debug-pprof-*.txt
retention-days: 7
@@ -575,7 +627,7 @@ jobs:
offlinedocs:
name: offlinedocs
needs: changes
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
if: needs.changes.outputs.offlinedocs == 'true' || needs.changes.outputs.ci == 'true' || needs.changes.outputs.docs == 'true'
steps:
@@ -643,6 +695,7 @@ jobs:
- test-e2e
- offlinedocs
- sqlc-vet
- dependency-license-review
# Allow this job to run even if the needed jobs fail, are skipped or
# cancelled.
if: always()
@@ -659,6 +712,7 @@ jobs:
echo "- test-js: ${{ needs.test-js.result }}"
echo "- test-e2e: ${{ needs.test-e2e.result }}"
echo "- offlinedocs: ${{ needs.offlinedocs.result }}"
echo "- dependency-license-review: ${{ needs.dependency-license-review.result }}"
echo
# We allow skipped jobs to pass, but not failed or cancelled jobs.
@@ -671,11 +725,10 @@ jobs:
build:
# This builds and publishes ghcr.io/coder/coder-preview:main for each commit
# to main branch. We are only building this for amd64 platform. (>95% pulls
# are for amd64)
# to main branch.
needs: changes
if: github.ref == 'refs/heads/main' && needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
outputs:
@@ -881,7 +934,7 @@ jobs:
# runs sqlc-vet to ensure all queries are valid. This catches any mistakes
# in migrations or sqlc queries that makes a query unable to be prepared.
sqlc-vet:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.db == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
steps:
@@ -899,3 +952,43 @@ jobs:
- name: Setup and run sqlc vet
run: |
make sqlc-vet
# dependency-license-review checks that no license-incompatible dependencies have been introduced.
# This action is not intended to do a vulnerability check since that is handled by a separate action.
dependency-license-review:
runs-on: ubuntu-latest
if: github.ref != 'refs/heads/main' && github.actor != 'dependabot[bot]'
steps:
- name: "Checkout Repository"
uses: actions/checkout@v4
- name: "Dependency Review"
id: review
uses: actions/dependency-review-action@v4.3.2
with:
allow-licenses: Apache-2.0, BSD-2-Clause, BSD-3-Clause, CC0-1.0, ISC, MIT, MIT-0, MPL-2.0
allow-dependencies-licenses: "pkg:golang/github.com/coder/wgtunnel@0.1.13-0.20240522110300-ade90dfb2da0, pkg:npm/pako@1.0.11"
license-check: true
vulnerability-check: false
- name: "Report"
# make sure this step runs even if the previous failed
if: always()
shell: bash
env:
VULNERABLE_CHANGES: ${{ steps.review.outputs.invalid-license-changes }}
run: |
fields=( "unlicensed" "unresolved" "forbidden" )
# This is unfortunate that we have to do this but the action does not support failing on
# an unknown license. The unknown dependency could easily have a GPL license which
# would be problematic for us.
# Track https://github.com/actions/dependency-review-action/issues/672 for when
# we can remove this brittle workaround.
for field in "${fields[@]}"; do
# Use jq to check if the array is not empty
if [[ $(echo "$VULNERABLE_CHANGES" | jq ".${field} | length") -ne 0 ]]; then
echo "Invalid or unknown licenses detected, contact @sreya to ensure your added dependency falls under one of our allowed licenses."
echo "$VULNERABLE_CHANGES" | jq
exit 1
fi
done
echo "No incompatible licenses detected"
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
steps:
- name: cla
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
uses: contributor-assistant/github-action@v2.3.2
uses: contributor-assistant/github-action@v2.4.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
+7 -1
View File
@@ -8,6 +8,11 @@ on:
- scripts/Dockerfile.base
- scripts/Dockerfile
pull_request:
paths:
- scripts/Dockerfile.base
- .github/workflows/docker-base.yaml
schedule:
# Run every week at 09:43 on Monday, Wednesday and Friday. We build this
# frequently to ensure that packages are up-to-date.
@@ -57,11 +62,12 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7
pull: true
no-cache: true
push: true
push: ${{ github.event_name != 'pull_request' }}
tags: |
ghcr.io/coder/coder-base:latest
- name: Verify that images are pushed properly
if: github.event_name != 'pull_request'
run: |
# retry 10 times with a 5 second delay as the images may not be
# available immediately
+3
View File
@@ -17,6 +17,9 @@
},
{
"pattern": "tailscale.com"
},
{
"pattern": "wireguard.com"
}
],
"aliveStatusCodes": [200, 0]
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
# While GitHub's toaster runners are likelier to flake, we want consistency
# between this environment and the regular test environment for DataDog
# statistics and to only show real workflow threats.
runs-on: "buildjet-8vcpu-ubuntu-2204"
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
# This runner costs 0.016 USD per minute,
# so 0.016 * 240 = 3.84 USD per run.
timeout-minutes: 240
@@ -40,7 +40,7 @@ jobs:
go-timing:
# We run these tests with p=1 so we don't need a lot of compute.
runs-on: "buildjet-2vcpu-ubuntu-2204"
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04' || 'ubuntu-latest' }}
timeout-minutes: 10
steps:
- name: Checkout
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Assign author
uses: toshimaru/auto-author-assign@v2.1.0
uses: toshimaru/auto-author-assign@v2.1.1
+1 -1
View File
@@ -189,7 +189,7 @@ jobs:
needs: get_info
# Run build job only if there are changes in the files that we care about or if the workflow is manually triggered with --build flag
if: needs.get_info.outputs.BUILD == 'true'
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
# This concurrency only cancels build jobs if a new build is triggred. It will avoid cancelling the current deployemtn in case of docs chnages.
concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}-${{ needs.get_info.outputs.BUILD }}
+94 -17
View File
@@ -1,11 +1,16 @@
# GitHub release workflow.
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
release_channel:
type: choice
description: Release channel
options:
- mainline
- stable
release_notes:
description: Release notes for the publishing the release. This is required to create a release.
dry_run:
description: Perform a dry-run release (devel). Note that ref must be an annotated tag when run without dry-run.
type: boolean
@@ -28,11 +33,13 @@ env:
# https://github.blog/changelog/2022-06-10-github-actions-inputs-unified-across-manual-and-reusable-workflows/
CODER_RELEASE: ${{ !inputs.dry_run }}
CODER_DRY_RUN: ${{ inputs.dry_run }}
CODER_RELEASE_CHANNEL: ${{ inputs.release_channel }}
CODER_RELEASE_NOTES: ${{ inputs.release_notes }}
jobs:
release:
name: Build and publish
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
env:
# Necessary for Docker manifest
DOCKER_CLI_EXPERIMENTAL: "enabled"
@@ -62,21 +69,45 @@ jobs:
echo "CODER_FORCE_VERSION=$version" >> $GITHUB_ENV
echo "$version"
- name: Create release notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# We always have to set this since there might be commits on
# main that didn't have a PR.
CODER_IGNORE_MISSING_COMMIT_METADATA: "1"
# Verify that all expectations for a release are met.
- name: Verify release input
if: ${{ !inputs.dry_run }}
run: |
set -euo pipefail
if [[ "${GITHUB_REF}" != "refs/tags/v"* ]]; then
echo "Ref must be a semver tag when creating a release, did you use scripts/release.sh?"
exit 1
fi
# 2.10.2 -> release/2.10
version="$(./scripts/version.sh)"
release_branch=release/${version%.*}
branch_contains_tag=$(git branch --remotes --contains "${GITHUB_REF}" --list "*/${release_branch}" --format='%(refname)')
if [[ -z "${branch_contains_tag}" ]]; then
echo "Ref tag must exist in a branch named ${release_branch} when creating a release, did you use scripts/release.sh?"
exit 1
fi
if [[ -z "${CODER_RELEASE_NOTES}" ]]; then
echo "Release notes are required to create a release, did you use scripts/release.sh?"
exit 1
fi
echo "Release inputs verified:"
echo
echo "- Ref: ${GITHUB_REF}"
echo "- Version: ${version}"
echo "- Release channel: ${CODER_RELEASE_CHANNEL}"
echo "- Release branch: ${release_branch}"
echo "- Release notes: true"
- name: Create release notes file
run: |
set -euo pipefail
ref=HEAD
old_version="$(git describe --abbrev=0 "$ref^1")"
version="v$(./scripts/version.sh)"
# Generate notes.
release_notes_file="$(mktemp -t release_notes.XXXXXX)"
./scripts/release/generate_release_notes.sh --check-for-changelog --old-version "$old_version" --new-version "$version" --ref "$ref" >> "$release_notes_file"
echo "$CODER_RELEASE_NOTES" > "$release_notes_file"
echo CODER_RELEASE_NOTES_FILE="$release_notes_file" >> $GITHUB_ENV
- name: Show release notes
@@ -97,6 +128,13 @@ jobs:
- name: Setup Node
uses: ./.github/actions/setup-node
# Necessary for signing Windows binaries.
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: "11.0"
- name: Install nsis and zstd
run: sudo apt-get install -y nsis zstd
@@ -130,6 +168,32 @@ jobs:
AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }}
AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }}
- name: Setup Windows EV Signing Certificate
run: |
set -euo pipefail
touch /tmp/ev_cert.pem
chmod 600 /tmp/ev_cert.pem
echo "$EV_SIGNING_CERT" > /tmp/ev_cert.pem
wget https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar -O /tmp/jsign-6.0.jar
env:
EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }}
- name: Test migrations from current ref to main
run: |
POSTGRES_VERSION=13 make test-migrations
# Setup GCloud for signing Windows binaries.
- name: Authenticate to Google Cloud
id: gcloud_auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }}
service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }}
token_format: "access_token"
- name: Setup GCloud SDK
uses: "google-github-actions/setup-gcloud@v2"
- name: Build binaries
run: |
set -euo pipefail
@@ -144,16 +208,26 @@ jobs:
build/coder_helm_"$version".tgz \
build/provisioner_helm_"$version".tgz
env:
CODER_SIGN_WINDOWS: "1"
CODER_SIGN_DARWIN: "1"
AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
AC_APIKEY_ISSUER_ID: ${{ secrets.AC_APIKEY_ISSUER_ID }}
AC_APIKEY_ID: ${{ secrets.AC_APIKEY_ID }}
AC_APIKEY_FILE: /tmp/apple_apikey.p8
EV_KEY: ${{ secrets.EV_KEY }}
EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }}
EV_TSA_URL: ${{ secrets.EV_TSA_URL }}
EV_CERTIFICATE_PATH: /tmp/ev_cert.pem
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
JSIGN_PATH: /tmp/jsign-6.0.jar
- name: Delete Apple Developer certificate and API key
run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
- name: Delete Windows EV Signing Cert
run: rm /tmp/ev_cert.pem
- name: Determine base image tag
id: image-base-tag
run: |
@@ -223,7 +297,7 @@ jobs:
# build Docker images for each architecture
version="$(./scripts/version.sh)"
make -j build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
# we can't build multi-arch if the images aren't pushed, so quit now
# if dry-running
@@ -234,7 +308,7 @@ jobs:
# build and push multi-arch manifest, this depends on the other images
# being pushed so will automatically push them.
make -j push/build/coder_"$version"_linux.tag
make push/build/coder_"$version"_linux.tag
# if the current version is equal to the highest (according to semver)
# version in the repo, also create a multi-arch image as ":latest" and
@@ -261,6 +335,9 @@ jobs:
set -euo pipefail
publish_args=()
if [[ $CODER_RELEASE_CHANNEL == "stable" ]]; then
publish_args+=(--stable)
fi
if [[ $CODER_DRY_RUN == *t* ]]; then
publish_args+=(--dry-run)
fi
+3 -3
View File
@@ -23,7 +23,7 @@ concurrency:
jobs:
codeql:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -56,7 +56,7 @@ jobs:
"${{ secrets.SLACK_SECURITY_FAILURE_WEBHOOK_URL }}"
trivy:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -114,7 +114,7 @@ jobs:
echo "image=$(cat "$image_job")" >> $GITHUB_OUTPUT
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@d710430a6722f083d3b36b8339ff66b32f22ee55
uses: aquasecurity/trivy-action@7c2007bcb556501da015201bcba5aa14069b74e2
with:
image-ref: ${{ steps.build.outputs.image }}
format: sarif
+2
View File
@@ -15,6 +15,7 @@ Hashi = "Hashi"
trialer = "trialer"
encrypter = "encrypter"
hel = "hel" # as in helsinki
pn = "pn" # this is used as proto node
[files]
extend-exclude = [
@@ -32,4 +33,5 @@ extend-exclude = [
"**/pnpm-lock.yaml",
"tailnet/testdata/**",
"site/src/pages/SetupPage/countries.tsx",
"provisioner/terraform/testdata/**",
]
+6 -1
View File
@@ -4,6 +4,11 @@ on:
schedule:
- cron: "0 9 * * 1"
workflow_dispatch: # allows to run manually for testing
pull_request:
branches:
- main
paths:
- "docs/**"
jobs:
check-docs:
@@ -24,7 +29,7 @@ jobs:
file-path: "./README.md"
- name: Send Slack notification
if: failure() && github.event_name != 'workflow_dispatch'
if: failure() && github.event_name == 'schedule'
run: |
curl -X POST -H 'Content-type: application/json' -d '{"msg":"Broken links found in the documentation. Please check the logs at ${{ env.LOGS_URL }}"}' ${{ secrets.DOCS_LINK_SLACK_WEBHOOK }}
echo "Sent Slack notification"
+3
View File
@@ -68,3 +68,6 @@ result
# Filebrowser.db
**/filebrowser.db
# pnpm
.pnpm-store/
+3
View File
@@ -71,6 +71,9 @@ result
# Filebrowser.db
**/filebrowser.db
# pnpm
.pnpm-store/
# .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier.
+3 -2
View File
@@ -195,7 +195,6 @@
"**.pb.go": true,
"**/*.gen.json": true,
"**/testdata/*": true,
"**Generated.ts": true,
"coderd/apidoc/**": true,
"docs/api/*.md": true,
"docs/templates/*.md": true,
@@ -222,5 +221,7 @@
"go.testFlags": ["-short", "-coverpkg=./..."],
// We often use a version of TypeScript that's ahead of the version shipped
// with VS Code.
"typescript.tsdk": "./site/node_modules/typescript/lib"
"typescript.tsdk": "./site/node_modules/typescript/lib",
// Playwright tests in VSCode will open a browser to live "view" the test.
"playwright.reuseBrowser": true
}
+58 -12
View File
@@ -36,6 +36,7 @@ GOOS := $(shell go env GOOS)
GOARCH := $(shell go env GOARCH)
GOOS_BIN_EXT := $(if $(filter windows, $(GOOS)),.exe,)
VERSION := $(shell ./scripts/version.sh)
POSTGRES_VERSION ?= 16
# Use the highest ZSTD compression level in CI.
ifdef CI
@@ -56,6 +57,9 @@ GO_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.go' -not -nam
# All the shell files in the repo, excluding ignored files.
SHELL_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.sh')
# Ensure we don't use the user's git configs which might cause side-effects
GIT_FLAGS = GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
# All ${OS}_${ARCH} combos we build for. Windows binaries have the .exe suffix.
OS_ARCHES := \
linux_amd64 linux_arm64 linux_armv7 \
@@ -200,7 +204,8 @@ endef
# calling this manually.
$(CODER_ALL_BINARIES): go.mod go.sum \
$(GO_SRC_FILES) \
$(shell find ./examples/templates)
$(shell find ./examples/templates) \
site/static/error.html
$(get-mode-os-arch-ext)
if [[ "$$os" != "windows" ]] && [[ "$$ext" != "" ]]; then
@@ -382,9 +387,9 @@ install: build/coder_$(VERSION)_$(GOOS)_$(GOARCH)$(GOOS_BIN_EXT)
cp "$<" "$$output_file"
.PHONY: install
BOLD := $(shell tput bold)
GREEN := $(shell tput setaf 2)
RESET := $(shell tput sgr0)
BOLD := $(shell tput bold 2>/dev/null)
GREEN := $(shell tput setaf 2 2>/dev/null)
RESET := $(shell tput sgr0 2>/dev/null)
fmt: fmt/eslint fmt/prettier fmt/terraform fmt/shfmt fmt/go
.PHONY: fmt
@@ -482,12 +487,14 @@ gen: \
$(DB_GEN_FILES) \
site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \
codersdk/rbacresources_gen.go \
docs/admin/prometheus.md \
docs/cli.md \
docs/admin/audit-logs.md \
coderd/apidoc/swagger.json \
.prettierignore.include \
.prettierignore \
provisioner/terraform/testdata/version \
site/.prettierrc.yaml \
site/.prettierignore \
site/.eslintignore \
@@ -553,6 +560,9 @@ coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $
coderd/database/dbmock/dbmock.go: coderd/database/db.go coderd/database/querier.go
go generate ./coderd/database/dbmock/
coderd/database/pubsub/psmock/psmock.go: coderd/database/pubsub/pubsub.go
go generate ./coderd/database/pubsub/psmock
tailnet/tailnettest/coordinatormock.go tailnet/tailnettest/multiagentmock.go tailnet/tailnettest/coordinateemock.go: tailnet/coordinator.go tailnet/multiagent.go
go generate ./tailnet/tailnettest/
@@ -606,8 +616,11 @@ site/src/theme/icons.json: $(wildcard scripts/gensite/*) $(wildcard site/static/
examples/examples.gen.json: scripts/examplegen/main.go examples/examples.go $(shell find ./examples/templates)
go run ./scripts/examplegen/main.go > examples/examples.gen.json
coderd/rbac/object_gen.go: scripts/rbacgen/main.go coderd/rbac/object.go
go run scripts/rbacgen/main.go ./coderd/rbac > coderd/rbac/object_gen.go
coderd/rbac/object_gen.go: scripts/rbacgen/rbacobject.gotmpl scripts/rbacgen/main.go coderd/rbac/object.go
go run scripts/rbacgen/main.go rbac > coderd/rbac/object_gen.go
codersdk/rbacresources_gen.go: scripts/rbacgen/codersdk.gotmpl scripts/rbacgen/main.go coderd/rbac/object.go
go run scripts/rbacgen/main.go codersdk > codersdk/rbacresources_gen.go
docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
go run scripts/metricsdocgen/main.go
@@ -673,6 +686,12 @@ provisioner/terraform/testdata/.gen-golden: $(wildcard provisioner/terraform/tes
go test ./provisioner/terraform -run="Test.*Golden$$" -update
touch "$@"
provisioner/terraform/testdata/version:
if [[ "$(shell cat provisioner/terraform/testdata/version.txt)" != "$(shell terraform version -json | jq -r '.terraform_version')" ]]; then
./provisioner/terraform/testdata/generate.sh
fi
.PHONY: provisioner/terraform/testdata/version
scripts/ci-report/testdata/.gen-golden: $(wildcard scripts/ci-report/testdata/*) $(wildcard scripts/ci-report/*.go)
go test ./scripts/ci-report -run=TestOutputMatchesGoldenFile -update
touch "$@"
@@ -738,7 +757,7 @@ site/.eslintignore site/.prettierignore: .prettierignore Makefile
done < "$<"
test:
gotestsum --format standard-quiet -- -v -short -count=1 ./...
$(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./...
.PHONY: test
# sqlc-cloud-is-setup will fail if no SQLc auth token is set. Use this as a
@@ -774,7 +793,7 @@ sqlc-vet: test-postgres-docker
test-postgres: test-postgres-docker
# The postgres test is prone to failure, so we limit parallelism for
# more consistent execution.
DB=ci DB_FROM=$(shell go run scripts/migrate-ci/main.go) gotestsum \
$(GIT_FLAGS) DB=ci DB_FROM=$(shell go run scripts/migrate-ci/main.go) gotestsum \
--junitfile="gotests.xml" \
--jsonfile="gotests.json" \
--packages="./..." -- \
@@ -783,9 +802,20 @@ test-postgres: test-postgres-docker
-count=1
.PHONY: test-postgres
test-migrations: test-postgres-docker
echo "--- test migrations"
set -euo pipefail
COMMIT_FROM=$(shell git log -1 --format='%h' HEAD)
echo "COMMIT_FROM=$${COMMIT_FROM}"
COMMIT_TO=$(shell git log -1 --format='%h' origin/main)
echo "COMMIT_TO=$${COMMIT_TO}"
if [[ "$${COMMIT_FROM}" == "$${COMMIT_TO}" ]]; then echo "Nothing to do!"; exit 0; fi
echo "DROP DATABASE IF EXISTS migrate_test_$${COMMIT_FROM}; CREATE DATABASE migrate_test_$${COMMIT_FROM};" | psql 'postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable'
go run ./scripts/migrate-test/main.go --from="$$COMMIT_FROM" --to="$$COMMIT_TO" --postgres-url="postgresql://postgres:postgres@localhost:5432/migrate_test_$${COMMIT_FROM}?sslmode=disable"
# NOTE: we set --memory to the same size as a GitHub runner.
test-postgres-docker:
docker rm -f test-postgres-docker || true
docker rm -f test-postgres-docker-${POSTGRES_VERSION} || true
docker run \
--env POSTGRES_PASSWORD=postgres \
--env POSTGRES_USER=postgres \
@@ -793,11 +823,11 @@ test-postgres-docker:
--env PGDATA=/tmp \
--tmpfs /tmp \
--publish 5432:5432 \
--name test-postgres-docker \
--name test-postgres-docker-${POSTGRES_VERSION} \
--restart no \
--detach \
--memory 16GB \
gcr.io/coder-dev-1/postgres:13 \
gcr.io/coder-dev-1/postgres:${POSTGRES_VERSION} \
-c shared_buffers=1GB \
-c work_mem=1GB \
-c effective_cache_size=1GB \
@@ -815,12 +845,28 @@ test-postgres-docker:
# Make sure to keep this in sync with test-go-race from .github/workflows/ci.yaml.
test-race:
gotestsum --junitfile="gotests.xml" -- -race -count=1 ./...
$(GIT_FLAGS) gotestsum --junitfile="gotests.xml" -- -race -count=1 ./...
.PHONY: test-race
test-tailnet-integration:
env \
CODER_TAILNET_TESTS=true \
CODER_MAGICSOCK_DEBUG_LOGGING=true \
TS_DEBUG_NETCHECK=true \
GOTRACEBACK=single \
go test \
-exec "sudo -E" \
-timeout=5m \
-count=1 \
./tailnet/test/integration
# Note: we used to add this to the test target, but it's not necessary and we can
# achieve the desired result by specifying -count=1 in the go test invocation
# instead. Keeping it here for convenience.
test-clean:
go clean -testcache
.PHONY: test-clean
.PHONY: test-e2e
test-e2e:
cd ./site && DEBUG=pw:api pnpm playwright:test --forbid-only --workers 1
+24 -14
View File
@@ -20,17 +20,17 @@
<br>
<br>
[Quickstart](#quickstart) | [Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Enterprise](https://coder.com/docs/v2/latest/enterprise)
[Quickstart](#quickstart) | [Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Enterprise](https://coder.com/docs/enterprise)
[![discord](https://img.shields.io/discord/747933592273027093?label=discord)](https://discord.gg/coder)
[![release](https://img.shields.io/github/v/release/coder/coder)](https://github.com/coder/coder/releases/latest)
[![godoc](https://pkg.go.dev/badge/github.com/coder/coder.svg)](https://pkg.go.dev/github.com/coder/coder)
[![Go Report Card](https://goreportcard.com/badge/github.com/coder/coder)](https://goreportcard.com/report/github.com/coder/coder)
[![Go Report Card](https://goreportcard.com/badge/github.com/coder/coder/v2)](https://goreportcard.com/report/github.com/coder/coder/v2)
[![license](https://img.shields.io/github/license/coder/coder)](./LICENSE)
</div>
[Coder](https://coder.com) enables organizations to set up development environments in their public or private cloud infrastructure. Cloud development environments are defined with Terraform, connected through a secure high-speed Wireguard® tunnel, and are automatically shut down when not in use to save on costs. Coder gives engineering teams the flexibility to use the cloud for workloads that are most beneficial to them.
[Coder](https://coder.com) enables organizations to set up development environments in their public or private cloud infrastructure. Cloud development environments are defined with Terraform, connected through a secure high-speed Wireguard® tunnel, and automatically shut down when not used to save on costs. Coder gives engineering teams the flexibility to use the cloud for workloads most beneficial to them.
- Define cloud development environments in Terraform
- EC2 VMs, Kubernetes Pods, Docker Containers, etc.
@@ -53,7 +53,7 @@ curl -L https://coder.com/install.sh | sh
coder server
# Navigate to http://localhost:3000 to create your initial user,
# create a Docker template, and provision a workspace
# create a Docker template and provision a workspace
```
## Install
@@ -69,7 +69,7 @@ curl -L https://coder.com/install.sh | sh
You can run the install script with `--dry-run` to see the commands that will be used to install without executing them. Run the install script with `--help` for additional flags.
> See [install](https://coder.com/docs/v2/latest/install) for additional methods.
> See [install](https://coder.com/docs/install) for additional methods.
Once installed, you can start a production deployment with a single command:
@@ -81,27 +81,27 @@ coder server
coder server --postgres-url <url> --access-url <url>
```
Use `coder --help` to get a list of flags and environment variables. Use our [install guides](https://coder.com/docs/v2/latest/install) for a full walkthrough.
Use `coder --help` to get a list of flags and environment variables. Use our [install guides](https://coder.com/docs/install) for a complete walkthrough.
## Documentation
Browse our docs [here](https://coder.com/docs/v2) or visit a specific section below:
Browse our docs [here](https://coder.com/docs) or visit a specific section below:
- [**Templates**](https://coder.com/docs/v2/latest/templates): Templates are written in Terraform and describe the infrastructure for workspaces
- [**Workspaces**](https://coder.com/docs/v2/latest/workspaces): Workspaces contain the IDEs, dependencies, and configuration information needed for software development
- [**IDEs**](https://coder.com/docs/v2/latest/ides): Connect your existing editor to a workspace
- [**Administration**](https://coder.com/docs/v2/latest/admin): Learn how to operate Coder
- [**Enterprise**](https://coder.com/docs/v2/latest/enterprise): Learn about our paid features built for large teams
- [**Templates**](https://coder.com/docs/templates): Templates are written in Terraform and describe the infrastructure for workspaces
- [**Workspaces**](https://coder.com/docs/workspaces): Workspaces contain the IDEs, dependencies, and configuration information needed for software development
- [**IDEs**](https://coder.com/docs/ides): Connect your existing editor to a workspace
- [**Administration**](https://coder.com/docs/admin): Learn how to operate Coder
- [**Enterprise**](https://coder.com/docs/enterprise): Learn about our paid features built for large teams
## Support
Feel free to [open an issue](https://github.com/coder/coder/issues/new) if you have questions, run into bugs, or have a feature request.
[Join our Discord](https://discord.gg/coder) to provide feedback on in-progress features, and chat with the community using Coder!
[Join our Discord](https://discord.gg/coder) to provide feedback on in-progress features and chat with the community using Coder!
## Integrations
We are always working on new integrations. Feel free to open an issue to request an integration. Contributions are welcome in any official or community repositories.
We are always working on new integrations. Please feel free to open an issue and ask for an integration. Contributions are welcome in any official or community repositories.
### Official
@@ -116,3 +116,13 @@ We are always working on new integrations. Feel free to open an issue to request
- [**Provision Coder with Terraform**](https://github.com/ElliotG/coder-oss-tf): Provision Coder on Google GKE, Azure AKS, AWS EKS, DigitalOcean DOKS, IBMCloud K8s, OVHCloud K8s, and Scaleway K8s Kapsule with Terraform
- [**Coder Template GitHub Action**](https://github.com/marketplace/actions/update-coder-template): A GitHub Action that updates Coder templates
## Contributing
We are always happy to see new contributors to Coder. If you are new to the Coder codebase, we have
[a guide on how to get started](https://coder.com/docs/CONTRIBUTING). We'd love to see your
contributions!
## Hiring
Apply [here](https://jobs.ashbyhq.com/coder?utm_source=github&utm_medium=readme&utm_campaign=unknown) if you're interested in joining our team.
+108 -99
View File
@@ -91,6 +91,7 @@ type Options struct {
ModifiedProcesses chan []*agentproc.Process
// ProcessManagementTick is used for testing process priority management.
ProcessManagementTick <-chan time.Time
BlockFileTransfer bool
}
type Client interface {
@@ -155,35 +156,36 @@ func New(options Options) Agent {
hardCtx, hardCancel := context.WithCancel(context.Background())
gracefulCtx, gracefulCancel := context.WithCancel(hardCtx)
a := &agent{
tailnetListenPort: options.TailnetListenPort,
reconnectingPTYTimeout: options.ReconnectingPTYTimeout,
logger: options.Logger,
gracefulCtx: gracefulCtx,
gracefulCancel: gracefulCancel,
hardCtx: hardCtx,
hardCancel: hardCancel,
coordDisconnected: make(chan struct{}),
environmentVariables: options.EnvironmentVariables,
client: options.Client,
exchangeToken: options.ExchangeToken,
filesystem: options.Filesystem,
logDir: options.LogDir,
tempDir: options.TempDir,
scriptDataDir: options.ScriptDataDir,
lifecycleUpdate: make(chan struct{}, 1),
lifecycleReported: make(chan codersdk.WorkspaceAgentLifecycle, 1),
lifecycleStates: []agentsdk.PostLifecycleRequest{{State: codersdk.WorkspaceAgentLifecycleCreated}},
ignorePorts: options.IgnorePorts,
portCacheDuration: options.PortCacheDuration,
reportMetadataInterval: options.ReportMetadataInterval,
serviceBannerRefreshInterval: options.ServiceBannerRefreshInterval,
sshMaxTimeout: options.SSHMaxTimeout,
subsystems: options.Subsystems,
addresses: options.Addresses,
syscaller: options.Syscaller,
modifiedProcs: options.ModifiedProcesses,
processManagementTick: options.ProcessManagementTick,
logSender: agentsdk.NewLogSender(options.Logger),
tailnetListenPort: options.TailnetListenPort,
reconnectingPTYTimeout: options.ReconnectingPTYTimeout,
logger: options.Logger,
gracefulCtx: gracefulCtx,
gracefulCancel: gracefulCancel,
hardCtx: hardCtx,
hardCancel: hardCancel,
coordDisconnected: make(chan struct{}),
environmentVariables: options.EnvironmentVariables,
client: options.Client,
exchangeToken: options.ExchangeToken,
filesystem: options.Filesystem,
logDir: options.LogDir,
tempDir: options.TempDir,
scriptDataDir: options.ScriptDataDir,
lifecycleUpdate: make(chan struct{}, 1),
lifecycleReported: make(chan codersdk.WorkspaceAgentLifecycle, 1),
lifecycleStates: []agentsdk.PostLifecycleRequest{{State: codersdk.WorkspaceAgentLifecycleCreated}},
ignorePorts: options.IgnorePorts,
portCacheDuration: options.PortCacheDuration,
reportMetadataInterval: options.ReportMetadataInterval,
announcementBannersRefreshInterval: options.ServiceBannerRefreshInterval,
sshMaxTimeout: options.SSHMaxTimeout,
subsystems: options.Subsystems,
addresses: options.Addresses,
syscaller: options.Syscaller,
modifiedProcs: options.ModifiedProcesses,
processManagementTick: options.ProcessManagementTick,
logSender: agentsdk.NewLogSender(options.Logger),
blockFileTransfer: options.BlockFileTransfer,
prometheusRegistry: prometheusRegistry,
metrics: newAgentMetrics(prometheusRegistry),
@@ -193,7 +195,7 @@ func New(options Options) Agent {
// that gets closed on disconnection. This is used to wait for graceful disconnection from the
// coordinator during shut down.
close(a.coordDisconnected)
a.serviceBanner.Store(new(codersdk.ServiceBannerConfig))
a.announcementBanners.Store(new([]codersdk.BannerConfig))
a.sessionToken.Store(new(string))
a.init()
return a
@@ -231,19 +233,21 @@ type agent struct {
environmentVariables map[string]string
manifest atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection.
reportMetadataInterval time.Duration
scriptRunner *agentscripts.Runner
serviceBanner atomic.Pointer[codersdk.ServiceBannerConfig] // serviceBanner is atomic because it is periodically updated.
serviceBannerRefreshInterval time.Duration
sessionToken atomic.Pointer[string]
sshServer *agentssh.Server
sshMaxTimeout time.Duration
manifest atomic.Pointer[agentsdk.Manifest] // manifest is atomic because values can change after reconnection.
reportMetadataInterval time.Duration
scriptRunner *agentscripts.Runner
announcementBanners atomic.Pointer[[]codersdk.BannerConfig] // announcementBanners is atomic because it is periodically updated.
announcementBannersRefreshInterval time.Duration
sessionToken atomic.Pointer[string]
sshServer *agentssh.Server
sshMaxTimeout time.Duration
blockFileTransfer bool
lifecycleUpdate chan struct{}
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
lifecycleMu sync.RWMutex // Protects following.
lifecycleStates []agentsdk.PostLifecycleRequest
lifecycleUpdate chan struct{}
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
lifecycleMu sync.RWMutex // Protects following.
lifecycleStates []agentsdk.PostLifecycleRequest
lifecycleLastReportedIndex int // Keeps track of the last lifecycle state we successfully reported.
network *tailnet.Conn
addresses []netip.Prefix
@@ -271,11 +275,12 @@ func (a *agent) TailnetConn() *tailnet.Conn {
func (a *agent) init() {
// pass the "hard" context because we explicitly close the SSH server as part of graceful shutdown.
sshSrv, err := agentssh.NewServer(a.hardCtx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, &agentssh.Config{
MaxTimeout: a.sshMaxTimeout,
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
ServiceBanner: func() *codersdk.ServiceBannerConfig { return a.serviceBanner.Load() },
UpdateEnv: a.updateCommandEnv,
WorkingDirectory: func() string { return a.manifest.Load().Directory },
MaxTimeout: a.sshMaxTimeout,
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
AnnouncementBanners: func() *[]codersdk.BannerConfig { return a.announcementBanners.Load() },
UpdateEnv: a.updateCommandEnv,
WorkingDirectory: func() string { return a.manifest.Load().Directory },
BlockFileTransfer: a.blockFileTransfer,
})
if err != nil {
panic(err)
@@ -625,7 +630,6 @@ func (a *agent) reportMetadata(ctx context.Context, conn drpc.Conn) error {
// changes are reported in order.
func (a *agent) reportLifecycle(ctx context.Context, conn drpc.Conn) error {
aAPI := proto.NewDRPCAgentClient(conn)
lastReportedIndex := 0 // Start off with the created state without reporting it.
for {
select {
case <-a.lifecycleUpdate:
@@ -636,20 +640,20 @@ func (a *agent) reportLifecycle(ctx context.Context, conn drpc.Conn) error {
for {
a.lifecycleMu.RLock()
lastIndex := len(a.lifecycleStates) - 1
report := a.lifecycleStates[lastReportedIndex]
if len(a.lifecycleStates) > lastReportedIndex+1 {
report = a.lifecycleStates[lastReportedIndex+1]
report := a.lifecycleStates[a.lifecycleLastReportedIndex]
if len(a.lifecycleStates) > a.lifecycleLastReportedIndex+1 {
report = a.lifecycleStates[a.lifecycleLastReportedIndex+1]
}
a.lifecycleMu.RUnlock()
if lastIndex == lastReportedIndex {
if lastIndex == a.lifecycleLastReportedIndex {
break
}
l, err := agentsdk.ProtoFromLifecycle(report)
if err != nil {
a.logger.Critical(ctx, "failed to convert lifecycle state", slog.F("report", report))
// Skip this report; there is no point retrying. Maybe we can successfully convert the next one?
lastReportedIndex++
a.lifecycleLastReportedIndex++
continue
}
payload := &proto.UpdateLifecycleRequest{Lifecycle: l}
@@ -662,13 +666,13 @@ func (a *agent) reportLifecycle(ctx context.Context, conn drpc.Conn) error {
}
logger.Debug(ctx, "successfully reported lifecycle state")
lastReportedIndex++
a.lifecycleLastReportedIndex++
select {
case a.lifecycleReported <- report.State:
case <-a.lifecycleReported:
a.lifecycleReported <- report.State
}
if lastReportedIndex < lastIndex {
if a.lifecycleLastReportedIndex < lastIndex {
// Keep reporting until we've sent all messages, we can't
// rely on the channel triggering us before the backlog is
// consumed.
@@ -709,23 +713,26 @@ func (a *agent) setLifecycle(state codersdk.WorkspaceAgentLifecycle) {
// (and must be done before the session actually starts).
func (a *agent) fetchServiceBannerLoop(ctx context.Context, conn drpc.Conn) error {
aAPI := proto.NewDRPCAgentClient(conn)
ticker := time.NewTicker(a.serviceBannerRefreshInterval)
ticker := time.NewTicker(a.announcementBannersRefreshInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{})
bannersProto, err := aAPI.GetAnnouncementBanners(ctx, &proto.GetAnnouncementBannersRequest{})
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
a.logger.Error(ctx, "failed to update service banner", slog.Error(err))
a.logger.Error(ctx, "failed to update notification banners", slog.Error(err))
return err
}
serviceBanner := agentsdk.ServiceBannerFromProto(sbp)
a.serviceBanner.Store(&serviceBanner)
banners := make([]codersdk.BannerConfig, 0, len(bannersProto.AnnouncementBanners))
for _, bannerProto := range bannersProto.AnnouncementBanners {
banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto))
}
a.announcementBanners.Store(&banners)
}
}
}
@@ -757,15 +764,18 @@ func (a *agent) run() (retErr error) {
// redial the coder server and retry.
connMan := newAPIConnRoutineManager(a.gracefulCtx, a.hardCtx, a.logger, conn)
connMan.start("init service banner", gracefulShutdownBehaviorStop,
connMan.start("init notification banners", gracefulShutdownBehaviorStop,
func(ctx context.Context, conn drpc.Conn) error {
aAPI := proto.NewDRPCAgentClient(conn)
sbp, err := aAPI.GetServiceBanner(ctx, &proto.GetServiceBannerRequest{})
bannersProto, err := aAPI.GetAnnouncementBanners(ctx, &proto.GetAnnouncementBannersRequest{})
if err != nil {
return xerrors.Errorf("fetch service banner: %w", err)
}
serviceBanner := agentsdk.ServiceBannerFromProto(sbp)
a.serviceBanner.Store(&serviceBanner)
banners := make([]codersdk.BannerConfig, 0, len(bannersProto.AnnouncementBanners))
for _, bannerProto := range bannersProto.AnnouncementBanners {
banners = append(banners, agentsdk.BannerConfigFromProto(bannerProto))
}
a.announcementBanners.Store(&banners)
return nil
},
)
@@ -807,23 +817,21 @@ func (a *agent) run() (retErr error) {
// coordination <--------------------------+
// derp map subscriber <----------------+
// stats report loop <---------------+
networkOK := make(chan struct{})
manifestOK := make(chan struct{})
networkOK := newCheckpoint(a.logger)
manifestOK := newCheckpoint(a.logger)
connMan.start("handle manifest", gracefulShutdownBehaviorStop, a.handleManifest(manifestOK))
connMan.start("app health reporter", gracefulShutdownBehaviorStop,
func(ctx context.Context, conn drpc.Conn) error {
select {
case <-ctx.Done():
return nil
case <-manifestOK:
manifest := a.manifest.Load()
NewWorkspaceAppHealthReporter(
a.logger, manifest.Apps, agentsdk.AppHealthPoster(proto.NewDRPCAgentClient(conn)),
)(ctx)
return nil
if err := manifestOK.wait(ctx); err != nil {
return xerrors.Errorf("no manifest: %w", err)
}
manifest := a.manifest.Load()
NewWorkspaceAppHealthReporter(
a.logger, manifest.Apps, agentsdk.AppHealthPoster(proto.NewDRPCAgentClient(conn)),
)(ctx)
return nil
})
connMan.start("create or update network", gracefulShutdownBehaviorStop,
@@ -831,10 +839,8 @@ func (a *agent) run() (retErr error) {
connMan.start("coordination", gracefulShutdownBehaviorStop,
func(ctx context.Context, conn drpc.Conn) error {
select {
case <-ctx.Done():
return nil
case <-networkOK:
if err := networkOK.wait(ctx); err != nil {
return xerrors.Errorf("no network: %w", err)
}
return a.runCoordinator(ctx, conn, a.network)
},
@@ -842,10 +848,8 @@ func (a *agent) run() (retErr error) {
connMan.start("derp map subscriber", gracefulShutdownBehaviorStop,
func(ctx context.Context, conn drpc.Conn) error {
select {
case <-ctx.Done():
return nil
case <-networkOK:
if err := networkOK.wait(ctx); err != nil {
return xerrors.Errorf("no network: %w", err)
}
return a.runDERPMapSubscriber(ctx, conn, a.network)
})
@@ -853,10 +857,8 @@ func (a *agent) run() (retErr error) {
connMan.start("fetch service banner loop", gracefulShutdownBehaviorStop, a.fetchServiceBannerLoop)
connMan.start("stats report loop", gracefulShutdownBehaviorStop, func(ctx context.Context, conn drpc.Conn) error {
select {
case <-ctx.Done():
return nil
case <-networkOK:
if err := networkOK.wait(ctx); err != nil {
return xerrors.Errorf("no network: %w", err)
}
return a.statsReporter.reportLoop(ctx, proto.NewDRPCAgentClient(conn))
})
@@ -865,8 +867,17 @@ func (a *agent) run() (retErr error) {
}
// handleManifest returns a function that fetches and processes the manifest
func (a *agent) handleManifest(manifestOK chan<- struct{}) func(ctx context.Context, conn drpc.Conn) error {
func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, conn drpc.Conn) error {
return func(ctx context.Context, conn drpc.Conn) error {
var (
sentResult = false
err error
)
defer func() {
if !sentResult {
manifestOK.complete(err)
}
}()
aAPI := proto.NewDRPCAgentClient(conn)
mp, err := aAPI.GetManifest(ctx, &proto.GetManifestRequest{})
if err != nil {
@@ -903,14 +914,12 @@ func (a *agent) handleManifest(manifestOK chan<- struct{}) func(ctx context.Cont
Subsystems: subsys,
}})
if err != nil {
if xerrors.Is(err, context.Canceled) {
return nil
}
return xerrors.Errorf("update workspace agent startup: %w", err)
}
oldManifest := a.manifest.Swap(&manifest)
close(manifestOK)
manifestOK.complete(nil)
sentResult = true
// The startup script should only execute on the first run!
if oldManifest == nil {
@@ -971,14 +980,15 @@ func (a *agent) handleManifest(manifestOK chan<- struct{}) func(ctx context.Cont
// createOrUpdateNetwork waits for the manifest to be set using manifestOK, then creates or updates
// the tailnet using the information in the manifest
func (a *agent) createOrUpdateNetwork(manifestOK <-chan struct{}, networkOK chan<- struct{}) func(context.Context, drpc.Conn) error {
return func(ctx context.Context, _ drpc.Conn) error {
select {
case <-ctx.Done():
return nil
case <-manifestOK:
func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(context.Context, drpc.Conn) error {
return func(ctx context.Context, _ drpc.Conn) (retErr error) {
if err := manifestOK.wait(ctx); err != nil {
return xerrors.Errorf("no manifest: %w", err)
}
var err error
defer func() {
networkOK.complete(retErr)
}()
manifest := a.manifest.Load()
a.closeMutex.Lock()
network := a.network
@@ -1014,7 +1024,6 @@ func (a *agent) createOrUpdateNetwork(manifestOK <-chan struct{}, networkOK chan
network.SetDERPForceWebSockets(manifest.DERPForceWebSockets)
network.SetBlockEndpoints(manifest.DisableDirectConnections)
}
close(networkOK)
return nil
}
}
+98 -5
View File
@@ -614,12 +614,12 @@ func TestAgent_Session_TTY_MOTD_Update(t *testing.T) {
// Set new banner func and wait for the agent to call it to update the
// banner.
ready := make(chan struct{}, 2)
client.SetServiceBannerFunc(func() (codersdk.ServiceBannerConfig, error) {
client.SetAnnouncementBannersFunc(func() ([]codersdk.BannerConfig, error) {
select {
case ready <- struct{}{}:
default:
}
return test.banner, nil
return []codersdk.BannerConfig{test.banner}, nil
})
<-ready
<-ready // Wait for two updates to ensure the value has propagated.
@@ -970,6 +970,99 @@ func TestAgent_SCP(t *testing.T) {
require.NoError(t, err)
}
func TestAgent_FileTransferBlocked(t *testing.T) {
t.Parallel()
assertFileTransferBlocked := func(t *testing.T, errorMessage string) {
// NOTE: Checking content of the error message is flaky. Most likely there is a race condition, which results
// in stopping the client in different phases, and returning different errors:
// - client read the full error message: File transfer has been disabled.
// - client's stream was terminated before reading the error message: EOF
// - client just read the error code (Windows): Process exited with status 65
isErr := strings.Contains(errorMessage, agentssh.BlockedFileTransferErrorMessage) ||
strings.Contains(errorMessage, "EOF") ||
strings.Contains(errorMessage, "Process exited with status 65")
require.True(t, isErr, fmt.Sprintf("Message: "+errorMessage))
}
t.Run("SFTP", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
//nolint:dogsled
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.BlockFileTransfer = true
})
sshClient, err := conn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
_, err = sftp.NewClient(sshClient)
require.Error(t, err)
assertFileTransferBlocked(t, err.Error())
})
t.Run("SCP with go-scp package", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
//nolint:dogsled
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.BlockFileTransfer = true
})
sshClient, err := conn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
scpClient, err := scp.NewClientBySSH(sshClient)
require.NoError(t, err)
defer scpClient.Close()
tempFile := filepath.Join(t.TempDir(), "scp")
err = scpClient.CopyFile(context.Background(), strings.NewReader("hello world"), tempFile, "0755")
require.Error(t, err)
assertFileTransferBlocked(t, err.Error())
})
t.Run("Forbidden commands", func(t *testing.T) {
t.Parallel()
for _, c := range agentssh.BlockedFileTransferCommands {
t.Run(c, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
//nolint:dogsled
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
o.BlockFileTransfer = true
})
sshClient, err := conn.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
session, err := sshClient.NewSession()
require.NoError(t, err)
defer session.Close()
stdout, err := session.StdoutPipe()
require.NoError(t, err)
//nolint:govet // we don't need `c := c` in Go 1.22
err = session.Start(c)
require.NoError(t, err)
defer session.Close()
msg, err := io.ReadAll(stdout)
require.NoError(t, err)
assertFileTransferBlocked(t, string(msg))
})
}
})
}
func TestAgent_EnvironmentVariables(t *testing.T) {
t.Parallel()
key := "EXAMPLE"
@@ -2193,15 +2286,15 @@ func setupAgentSSHClient(ctx context.Context, t *testing.T) *ssh.Client {
func setupSSHSession(
t *testing.T,
manifest agentsdk.Manifest,
serviceBanner codersdk.ServiceBannerConfig,
banner codersdk.BannerConfig,
prepareFS func(fs afero.Fs),
opts ...func(*agenttest.Client, *agent.Options),
) *ssh.Session {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
opts = append(opts, func(c *agenttest.Client, o *agent.Options) {
c.SetServiceBannerFunc(func() (codersdk.ServiceBannerConfig, error) {
return serviceBanner, nil
c.SetAnnouncementBannersFunc(func() ([]codersdk.BannerConfig, error) {
return []codersdk.BannerConfig{banner}, nil
})
})
//nolint:dogsled
+67 -11
View File
@@ -52,8 +52,16 @@ const (
// MagicProcessCmdlineJetBrains is a string in a process's command line that
// uniquely identifies it as JetBrains software.
MagicProcessCmdlineJetBrains = "idea.vendor.name=JetBrains"
// BlockedFileTransferErrorCode indicates that SSH server restricted the raw command from performing
// the file transfer.
BlockedFileTransferErrorCode = 65 // Error code: host not allowed to connect
BlockedFileTransferErrorMessage = "File transfer has been disabled."
)
// BlockedFileTransferCommands contains a list of restricted file transfer commands.
var BlockedFileTransferCommands = []string{"nc", "rsync", "scp", "sftp"}
// Config sets configuration parameters for the agent SSH server.
type Config struct {
// MaxTimeout sets the absolute connection timeout, none if empty. If set to
@@ -63,7 +71,7 @@ type Config struct {
// file will be displayed to the user upon login.
MOTDFile func() string
// ServiceBanner returns the configuration for the Coder service banner.
ServiceBanner func() *codersdk.ServiceBannerConfig
AnnouncementBanners func() *[]codersdk.BannerConfig
// UpdateEnv updates the environment variables for the command to be
// executed. It can be used to add, modify or replace environment variables.
UpdateEnv func(current []string) (updated []string, err error)
@@ -74,6 +82,8 @@ type Config struct {
// X11SocketDir is the directory where X11 sockets are created. Default is
// /tmp/.X11-unix.
X11SocketDir string
// BlockFileTransfer restricts use of file transfer applications.
BlockFileTransfer bool
}
type Server struct {
@@ -123,8 +133,8 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
if config.MOTDFile == nil {
config.MOTDFile = func() string { return "" }
}
if config.ServiceBanner == nil {
config.ServiceBanner = func() *codersdk.ServiceBannerConfig { return &codersdk.ServiceBannerConfig{} }
if config.AnnouncementBanners == nil {
config.AnnouncementBanners = func() *[]codersdk.BannerConfig { return &[]codersdk.BannerConfig{} }
}
if config.WorkingDirectory == nil {
config.WorkingDirectory = func() string {
@@ -272,6 +282,18 @@ func (s *Server) sessionHandler(session ssh.Session) {
extraEnv = append(extraEnv, fmt.Sprintf("DISPLAY=:%d.0", x11.ScreenNumber))
}
if s.fileTransferBlocked(session) {
s.logger.Warn(ctx, "file transfer blocked", slog.F("session_subsystem", session.Subsystem()), slog.F("raw_command", session.RawCommand()))
if session.Subsystem() == "" { // sftp does not expect error, otherwise it fails with "package too long"
// Response format: <status_code><message body>\n
errorMessage := fmt.Sprintf("\x02%s\n", BlockedFileTransferErrorMessage)
_, _ = session.Write([]byte(errorMessage))
}
_ = session.Exit(BlockedFileTransferErrorCode)
return
}
switch ss := session.Subsystem(); ss {
case "":
case "sftp":
@@ -322,6 +344,37 @@ func (s *Server) sessionHandler(session ssh.Session) {
_ = session.Exit(0)
}
// fileTransferBlocked method checks if the file transfer commands should be blocked.
//
// Warning: consider this mechanism as "Do not trespass" sign, as a violator can still ssh to the host,
// smuggle the `scp` binary, or just manually send files outside with `curl` or `ftp`.
// If a user needs a more sophisticated and battle-proof solution, consider full endpoint security.
func (s *Server) fileTransferBlocked(session ssh.Session) bool {
if !s.config.BlockFileTransfer {
return false // file transfers are permitted
}
// File transfers are restricted.
if session.Subsystem() == "sftp" {
return true
}
cmd := session.Command()
if len(cmd) == 0 {
return false // no command?
}
c := cmd[0]
c = filepath.Base(c) // in case the binary is absolute path, /usr/sbin/scp
for _, cmd := range BlockedFileTransferCommands {
if cmd == c {
return true
}
}
return false
}
func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, extraEnv []string) (retErr error) {
ctx := session.Context()
env := append(session.Environ(), extraEnv...)
@@ -441,12 +494,15 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
session.DisablePTYEmulation()
if isLoginShell(session.RawCommand()) {
serviceBanner := s.config.ServiceBanner()
if serviceBanner != nil {
err := showServiceBanner(session, serviceBanner)
if err != nil {
logger.Error(ctx, "agent failed to show service banner", slog.Error(err))
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "service_banner").Add(1)
banners := s.config.AnnouncementBanners()
if banners != nil {
for _, banner := range *banners {
err := showAnnouncementBanner(session, banner)
if err != nil {
logger.Error(ctx, "agent failed to show announcement banner", slog.Error(err))
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "announcement_banner").Add(1)
break
}
}
}
}
@@ -891,9 +947,9 @@ func isQuietLogin(fs afero.Fs, rawCommand string) bool {
return err == nil
}
// showServiceBanner will write the service banner if enabled and not blank
// showAnnouncementBanner will write the service banner if enabled and not blank
// along with a blank line for spacing.
func showServiceBanner(session io.Writer, banner *codersdk.ServiceBannerConfig) error {
func showAnnouncementBanner(session io.Writer, banner codersdk.BannerConfig) error {
if banner.Enabled && banner.Message != "" {
// The banner supports Markdown so we might want to parse it but Markdown is
// still fairly readable in its raw form.
+21 -13
View File
@@ -138,8 +138,8 @@ func (c *Client) GetStartupLogs() []agentsdk.Log {
return c.logs
}
func (c *Client) SetServiceBannerFunc(f func() (codersdk.ServiceBannerConfig, error)) {
c.fakeAgentAPI.SetServiceBannerFunc(f)
func (c *Client) SetAnnouncementBannersFunc(f func() ([]codersdk.BannerConfig, error)) {
c.fakeAgentAPI.SetAnnouncementBannersFunc(f)
}
func (c *Client) PushDERPMapUpdate(update *tailcfg.DERPMap) error {
@@ -171,31 +171,39 @@ type FakeAgentAPI struct {
lifecycleStates []codersdk.WorkspaceAgentLifecycle
metadata map[string]agentsdk.Metadata
getServiceBannerFunc func() (codersdk.ServiceBannerConfig, error)
getAnnouncementBannersFunc func() ([]codersdk.BannerConfig, error)
}
func (f *FakeAgentAPI) GetManifest(context.Context, *agentproto.GetManifestRequest) (*agentproto.Manifest, error) {
return f.manifest, nil
}
func (f *FakeAgentAPI) SetServiceBannerFunc(fn func() (codersdk.ServiceBannerConfig, error)) {
f.Lock()
defer f.Unlock()
f.getServiceBannerFunc = fn
f.logger.Info(context.Background(), "updated ServiceBannerFunc")
func (*FakeAgentAPI) GetServiceBanner(context.Context, *agentproto.GetServiceBannerRequest) (*agentproto.ServiceBanner, error) {
return &agentproto.ServiceBanner{}, nil
}
func (f *FakeAgentAPI) GetServiceBanner(context.Context, *agentproto.GetServiceBannerRequest) (*agentproto.ServiceBanner, error) {
func (f *FakeAgentAPI) SetAnnouncementBannersFunc(fn func() ([]codersdk.BannerConfig, error)) {
f.Lock()
defer f.Unlock()
if f.getServiceBannerFunc == nil {
return &agentproto.ServiceBanner{}, nil
f.getAnnouncementBannersFunc = fn
f.logger.Info(context.Background(), "updated notification banners")
}
func (f *FakeAgentAPI) GetAnnouncementBanners(context.Context, *agentproto.GetAnnouncementBannersRequest) (*agentproto.GetAnnouncementBannersResponse, error) {
f.Lock()
defer f.Unlock()
if f.getAnnouncementBannersFunc == nil {
return &agentproto.GetAnnouncementBannersResponse{AnnouncementBanners: []*agentproto.BannerConfig{}}, nil
}
sb, err := f.getServiceBannerFunc()
banners, err := f.getAnnouncementBannersFunc()
if err != nil {
return nil, err
}
return agentsdk.ProtoFromServiceBanner(sb), nil
bannersProto := make([]*agentproto.BannerConfig, 0, len(banners))
for _, banner := range banners {
bannersProto = append(bannersProto, agentsdk.ProtoFromBannerConfig(banner))
}
return &agentproto.GetAnnouncementBannersResponse{AnnouncementBanners: bannersProto}, nil
}
func (f *FakeAgentAPI) UpdateStats(ctx context.Context, req *agentproto.UpdateStatsRequest) (*agentproto.UpdateStatsResponse, error) {
+53 -59
View File
@@ -10,14 +10,11 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/retry"
)
// WorkspaceAgentApps fetches the workspace apps.
type WorkspaceAgentApps func(context.Context) ([]codersdk.WorkspaceApp, error)
// PostWorkspaceAgentAppHealth updates the workspace app health.
type PostWorkspaceAgentAppHealth func(context.Context, agentsdk.PostAppHealthsRequest) error
@@ -26,15 +23,26 @@ type WorkspaceAppHealthReporter func(ctx context.Context)
// NewWorkspaceAppHealthReporter creates a WorkspaceAppHealthReporter that reports app health to coderd.
func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.WorkspaceApp, postWorkspaceAgentAppHealth PostWorkspaceAgentAppHealth) WorkspaceAppHealthReporter {
return NewAppHealthReporterWithClock(logger, apps, postWorkspaceAgentAppHealth, clock.NewReal())
}
// NewAppHealthReporterWithClock is only called directly by test code. Product code should call
// NewAppHealthReporter.
func NewAppHealthReporterWithClock(
logger slog.Logger,
apps []codersdk.WorkspaceApp,
postWorkspaceAgentAppHealth PostWorkspaceAgentAppHealth,
clk clock.Clock,
) WorkspaceAppHealthReporter {
logger = logger.Named("apphealth")
runHealthcheckLoop := func(ctx context.Context) error {
return func(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// no need to run this loop if no apps for this workspace.
if len(apps) == 0 {
return nil
return
}
hasHealthchecksEnabled := false
@@ -49,7 +57,7 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
// no need to run this loop if no health checks are configured.
if !hasHealthchecksEnabled {
return nil
return
}
// run a ticker for each app health check.
@@ -61,25 +69,29 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
}
app := nextApp
go func() {
t := time.NewTicker(time.Duration(app.Healthcheck.Interval) * time.Second)
defer t.Stop()
_ = clk.TickerFunc(ctx, time.Duration(app.Healthcheck.Interval)*time.Second, func() error {
// We time out at the healthcheck interval to prevent getting too backed up, but
// set it 1ms early so that it's not simultaneous with the next tick in testing,
// which makes the test easier to understand.
//
// It would be idiomatic to use the http.Client.Timeout or a context.WithTimeout,
// but we are passing this off to the native http library, which is not aware
// of the clock library we are using. That means in testing, with a mock clock
// it will compare mocked times with real times, and we will get strange results.
// So, we just implement the timeout as a context we cancel with an AfterFunc
reqCtx, reqCancel := context.WithCancel(ctx)
timeout := clk.AfterFunc(
time.Duration(app.Healthcheck.Interval)*time.Second-time.Millisecond,
reqCancel,
"timeout", app.Slug)
defer timeout.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
}
// we set the http timeout to the healthcheck interval to prevent getting too backed up.
client := &http.Client{
Timeout: time.Duration(app.Healthcheck.Interval) * time.Second,
}
err := func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, app.Healthcheck.URL, nil)
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, app.Healthcheck.URL, nil)
if err != nil {
return err
}
res, err := client.Do(req)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
@@ -118,54 +130,36 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
mu.Unlock()
logger.Debug(ctx, "workspace app healthy", slog.F("id", app.ID.String()), slog.F("slug", app.Slug))
}
t.Reset(time.Duration(app.Healthcheck.Interval) * time.Second)
}
return nil
}, "healthcheck", app.Slug)
}()
}
mu.Lock()
lastHealth := copyHealth(health)
mu.Unlock()
reportTicker := time.NewTicker(time.Second)
defer reportTicker.Stop()
// every second we check if the health values of the apps have changed
// and if there is a change we will report the new values.
for {
select {
case <-ctx.Done():
reportTicker := clk.TickerFunc(ctx, time.Second, func() error {
mu.RLock()
changed := healthChanged(lastHealth, health)
mu.RUnlock()
if !changed {
return nil
case <-reportTicker.C:
mu.RLock()
changed := healthChanged(lastHealth, health)
mu.RUnlock()
if !changed {
continue
}
mu.Lock()
lastHealth = copyHealth(health)
mu.Unlock()
err := postWorkspaceAgentAppHealth(ctx, agentsdk.PostAppHealthsRequest{
Healths: lastHealth,
})
if err != nil {
logger.Error(ctx, "failed to report workspace app health", slog.Error(err))
} else {
logger.Debug(ctx, "sent workspace app health", slog.F("health", lastHealth))
}
}
}
}
return func(ctx context.Context) {
for r := retry.New(time.Second, 30*time.Second); r.Wait(ctx); {
err := runHealthcheckLoop(ctx)
if err == nil || xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded) {
return
mu.Lock()
lastHealth = copyHealth(health)
mu.Unlock()
err := postWorkspaceAgentAppHealth(ctx, agentsdk.PostAppHealthsRequest{
Healths: lastHealth,
})
if err != nil {
logger.Error(ctx, "failed to report workspace app health", slog.Error(err))
} else {
logger.Debug(ctx, "sent workspace app health", slog.F("health", lastHealth))
}
logger.Error(ctx, "failed running workspace app reporter", slog.Error(err))
}
return nil
}, "report")
_ = reportTicker.Wait() // only possible error is context done
}
}
+154 -113
View File
@@ -4,14 +4,12 @@ import (
"context"
"net/http"
"net/http/httptest"
"slices"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog"
@@ -19,6 +17,7 @@ import (
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
@@ -27,15 +26,17 @@ import (
func TestAppHealth_Healthy(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
ID: uuid.UUID{1},
Slug: "app1",
Healthcheck: codersdk.Healthcheck{},
Health: codersdk.WorkspaceAppHealthDisabled,
},
{
ID: uuid.UUID{2},
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
@@ -46,6 +47,7 @@ func TestAppHealth_Healthy(t *testing.T) {
Health: codersdk.WorkspaceAppHealthInitializing,
},
{
ID: uuid.UUID{3},
Slug: "app3",
Healthcheck: codersdk.Healthcheck{
Interval: 2,
@@ -54,36 +56,71 @@ func TestAppHealth_Healthy(t *testing.T) {
Health: codersdk.WorkspaceAppHealthInitializing,
},
}
checks2 := 0
checks3 := 0
handlers := []http.Handler{
nil,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
checks2++
httpapi.Write(r.Context(), w, http.StatusOK, nil)
}),
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
checks3++
httpapi.Write(r.Context(), w, http.StatusOK, nil)
}),
}
getApps, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
apps, err := getApps(ctx)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, apps[0].Health)
require.Eventually(t, func() bool {
apps, err := getApps(ctx)
if err != nil {
return false
}
mClock := clock.NewMock(t)
healthcheckTrap := mClock.Trap().TickerFunc("healthcheck")
defer healthcheckTrap.Close()
reportTrap := mClock.Trap().TickerFunc("report")
defer reportTrap.Close()
return apps[1].Health == codersdk.WorkspaceAppHealthHealthy && apps[2].Health == codersdk.WorkspaceAppHealthHealthy
}, testutil.WaitLong, testutil.IntervalSlow)
fakeAPI, closeFn := setupAppReporter(ctx, t, slices.Clone(apps), handlers, mClock)
defer closeFn()
healthchecksStarted := make([]string, 2)
for i := 0; i < 2; i++ {
c := healthcheckTrap.MustWait(ctx)
c.Release()
healthchecksStarted[i] = c.Tags[1]
}
slices.Sort(healthchecksStarted)
require.Equal(t, []string{"app2", "app3"}, healthchecksStarted)
// advance the clock 1ms before the report ticker starts, so that it's not
// simultaneous with the checks.
mClock.Advance(time.Millisecond).MustWait(ctx)
reportTrap.MustWait(ctx).Release()
mClock.Advance(999 * time.Millisecond).MustWait(ctx) // app2 is now healthy
mClock.Advance(time.Millisecond).MustWait(ctx) // report gets triggered
update := testutil.RequireRecvCtx(ctx, t, fakeAPI.AppHealthCh())
require.Len(t, update.GetUpdates(), 2)
applyUpdate(t, apps, update)
require.Equal(t, codersdk.WorkspaceAppHealthHealthy, apps[1].Health)
require.Equal(t, codersdk.WorkspaceAppHealthInitializing, apps[2].Health)
mClock.Advance(999 * time.Millisecond).MustWait(ctx) // app3 is now healthy
mClock.Advance(time.Millisecond).MustWait(ctx) // report gets triggered
update = testutil.RequireRecvCtx(ctx, t, fakeAPI.AppHealthCh())
require.Len(t, update.GetUpdates(), 2)
applyUpdate(t, apps, update)
require.Equal(t, codersdk.WorkspaceAppHealthHealthy, apps[1].Health)
require.Equal(t, codersdk.WorkspaceAppHealthHealthy, apps[2].Health)
// ensure we aren't spamming
require.Equal(t, 2, checks2)
require.Equal(t, 1, checks3)
}
func TestAppHealth_500(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
ID: uuid.UUID{2},
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
@@ -99,59 +136,40 @@ func TestAppHealth_500(t *testing.T) {
httpapi.Write(r.Context(), w, http.StatusInternalServerError, nil)
}),
}
getApps, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
require.Eventually(t, func() bool {
apps, err := getApps(ctx)
if err != nil {
return false
}
return apps[0].Health == codersdk.WorkspaceAppHealthUnhealthy
}, testutil.WaitLong, testutil.IntervalSlow)
mClock := clock.NewMock(t)
healthcheckTrap := mClock.Trap().TickerFunc("healthcheck")
defer healthcheckTrap.Close()
reportTrap := mClock.Trap().TickerFunc("report")
defer reportTrap.Close()
fakeAPI, closeFn := setupAppReporter(ctx, t, slices.Clone(apps), handlers, mClock)
defer closeFn()
healthcheckTrap.MustWait(ctx).Release()
// advance the clock 1ms before the report ticker starts, so that it's not
// simultaneous with the checks.
mClock.Advance(time.Millisecond).MustWait(ctx)
reportTrap.MustWait(ctx).Release()
mClock.Advance(999 * time.Millisecond).MustWait(ctx) // check gets triggered
mClock.Advance(time.Millisecond).MustWait(ctx) // report gets triggered, but unsent since we are at the threshold
mClock.Advance(999 * time.Millisecond).MustWait(ctx) // 2nd check, crosses threshold
mClock.Advance(time.Millisecond).MustWait(ctx) // 2nd report, sends update
update := testutil.RequireRecvCtx(ctx, t, fakeAPI.AppHealthCh())
require.Len(t, update.GetUpdates(), 1)
applyUpdate(t, apps, update)
require.Equal(t, codersdk.WorkspaceAppHealthUnhealthy, apps[0].Health)
}
func TestAppHealth_Timeout(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
Interval: 1,
Threshold: 1,
},
Health: codersdk.WorkspaceAppHealthInitializing,
},
}
handlers := []http.Handler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// sleep longer than the interval to cause the health check to time out
time.Sleep(2 * time.Second)
httpapi.Write(r.Context(), w, http.StatusOK, nil)
}),
}
getApps, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
require.Eventually(t, func() bool {
apps, err := getApps(ctx)
if err != nil {
return false
}
return apps[0].Health == codersdk.WorkspaceAppHealthUnhealthy
}, testutil.WaitLong, testutil.IntervalSlow)
}
func TestAppHealth_NotSpamming(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
ID: uuid.UUID{2},
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
@@ -163,27 +181,65 @@ func TestAppHealth_NotSpamming(t *testing.T) {
},
}
counter := new(int32)
handlers := []http.Handler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(counter, 1)
http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
// allow the request to time out
<-r.Context().Done()
}),
}
_, closeFn := setupAppReporter(ctx, t, apps, handlers)
mClock := clock.NewMock(t)
start := mClock.Now()
// for this test, it's easier to think in the number of milliseconds elapsed
// since start.
ms := func(n int) time.Time {
return start.Add(time.Duration(n) * time.Millisecond)
}
healthcheckTrap := mClock.Trap().TickerFunc("healthcheck")
defer healthcheckTrap.Close()
reportTrap := mClock.Trap().TickerFunc("report")
defer reportTrap.Close()
timeoutTrap := mClock.Trap().AfterFunc("timeout")
defer timeoutTrap.Close()
fakeAPI, closeFn := setupAppReporter(ctx, t, apps, handlers, mClock)
defer closeFn()
// Ensure we haven't made more than 2 (expected 1 + 1 for buffer) requests in the last second.
// if there is a bug where we are spamming the healthcheck route this will catch it.
time.Sleep(time.Second)
require.LessOrEqual(t, atomic.LoadInt32(counter), int32(2))
healthcheckTrap.MustWait(ctx).Release()
// advance the clock 1ms before the report ticker starts, so that it's not
// simultaneous with the checks.
mClock.Set(ms(1)).MustWait(ctx)
reportTrap.MustWait(ctx).Release()
w := mClock.Set(ms(1000)) // 1st check starts
timeoutTrap.MustWait(ctx).Release()
mClock.Set(ms(1001)).MustWait(ctx) // report tick, no change
mClock.Set(ms(1999)) // timeout pops
w.MustWait(ctx) // 1st check finished
w = mClock.Set(ms(2000)) // 2nd check starts
timeoutTrap.MustWait(ctx).Release()
mClock.Set(ms(2001)).MustWait(ctx) // report tick, no change
mClock.Set(ms(2999)) // timeout pops
w.MustWait(ctx) // 2nd check finished
// app is now unhealthy after 2 timeouts
mClock.Set(ms(3000)) // 3rd check starts
timeoutTrap.MustWait(ctx).Release()
mClock.Set(ms(3001)).MustWait(ctx) // report tick, sends changes
update := testutil.RequireRecvCtx(ctx, t, fakeAPI.AppHealthCh())
require.Len(t, update.GetUpdates(), 1)
applyUpdate(t, apps, update)
require.Equal(t, codersdk.WorkspaceAppHealthUnhealthy, apps[0].Health)
}
func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.WorkspaceApp, handlers []http.Handler) (agent.WorkspaceAgentApps, func()) {
func setupAppReporter(
ctx context.Context, t *testing.T,
apps []codersdk.WorkspaceApp,
handlers []http.Handler,
clk clock.Clock,
) (*agenttest.FakeAgentAPI, func()) {
closers := []func(){}
for i, app := range apps {
if app.ID == uuid.Nil {
app.ID = uuid.New()
apps[i] = app
}
for _, app := range apps {
require.NotEqual(t, uuid.Nil, app.ID, "all apps must have ID set")
}
for i, handler := range handlers {
if handler == nil {
@@ -196,14 +252,6 @@ func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.Workspa
closers = append(closers, ts.Close)
}
var mu sync.Mutex
workspaceAgentApps := func(context.Context) ([]codersdk.WorkspaceApp, error) {
mu.Lock()
defer mu.Unlock()
var newApps []codersdk.WorkspaceApp
return append(newApps, apps...), nil
}
// We don't care about manifest or stats in this test since it's not using
// a full agent and these RPCs won't get called.
//
@@ -212,38 +260,31 @@ func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.Workspa
// post function.
fakeAAPI := agenttest.NewFakeAgentAPI(t, slogtest.Make(t, nil), nil, nil)
// Process events from the channel and update the health of the apps.
go func() {
appHealthCh := fakeAAPI.AppHealthCh()
for {
select {
case <-ctx.Done():
return
case req := <-appHealthCh:
mu.Lock()
for _, update := range req.Updates {
updateID, err := uuid.FromBytes(update.Id)
assert.NoError(t, err)
updateHealth := codersdk.WorkspaceAppHealth(strings.ToLower(proto.AppHealth_name[int32(update.Health)]))
go agent.NewAppHealthReporterWithClock(
slogtest.Make(t, nil).Leveled(slog.LevelDebug),
apps, agentsdk.AppHealthPoster(fakeAAPI), clk,
)(ctx)
for i, app := range apps {
if app.ID != updateID {
continue
}
app.Health = updateHealth
apps[i] = app
}
}
mu.Unlock()
}
}
}()
go agent.NewWorkspaceAppHealthReporter(slogtest.Make(t, nil).Leveled(slog.LevelDebug), apps, agentsdk.AppHealthPoster(fakeAAPI))(ctx)
return workspaceAgentApps, func() {
return fakeAAPI, func() {
for _, closeFn := range closers {
closeFn()
}
}
}
func applyUpdate(t *testing.T, apps []codersdk.WorkspaceApp, req *proto.BatchUpdateAppHealthRequest) {
t.Helper()
for _, update := range req.Updates {
updateID, err := uuid.FromBytes(update.Id)
require.NoError(t, err)
updateHealth := codersdk.WorkspaceAppHealth(strings.ToLower(proto.AppHealth_name[int32(update.Health)]))
for i, app := range apps {
if app.ID != updateID {
continue
}
app.Health = updateHealth
apps[i] = app
}
}
}
+51
View File
@@ -0,0 +1,51 @@
package agent
import (
"context"
"runtime"
"sync"
"cdr.dev/slog"
)
// checkpoint allows a goroutine to communicate when it is OK to proceed beyond some async condition
// to other dependent goroutines.
type checkpoint struct {
logger slog.Logger
mu sync.Mutex
called bool
done chan struct{}
err error
}
// complete the checkpoint. Pass nil to indicate the checkpoint was ok. It is an error to call this
// more than once.
func (c *checkpoint) complete(err error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.called {
b := make([]byte, 2048)
n := runtime.Stack(b, false)
c.logger.Critical(context.Background(), "checkpoint complete called more than once", slog.F("stacktrace", b[:n]))
return
}
c.called = true
c.err = err
close(c.done)
}
func (c *checkpoint) wait(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-c.done:
return c.err
}
}
func newCheckpoint(logger slog.Logger) *checkpoint {
return &checkpoint{
logger: logger,
done: make(chan struct{}),
}
}
+49
View File
@@ -0,0 +1,49 @@
package agent
import (
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/testutil"
)
func TestCheckpoint_CompleteWait(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
ctx := testutil.Context(t, testutil.WaitShort)
uut := newCheckpoint(logger)
err := xerrors.New("test")
uut.complete(err)
got := uut.wait(ctx)
require.Equal(t, err, got)
}
func TestCheckpoint_CompleteTwice(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
ctx := testutil.Context(t, testutil.WaitShort)
uut := newCheckpoint(logger)
err := xerrors.New("test")
uut.complete(err)
uut.complete(nil) // drops CRITICAL log
got := uut.wait(ctx)
require.Equal(t, err, got)
}
func TestCheckpoint_WaitComplete(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
ctx := testutil.Context(t, testutil.WaitShort)
uut := newCheckpoint(logger)
err := xerrors.New("test")
errCh := make(chan error, 1)
go func() {
errCh <- uut.wait(ctx)
}()
uut.complete(err)
got := testutil.RequireRecvCtx(ctx, t, errCh)
require.Equal(t, err, got)
}
+342 -129
View File
@@ -1859,6 +1859,154 @@ func (x *BatchCreateLogsResponse) GetLogLimitExceeded() bool {
return false
}
type GetAnnouncementBannersRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *GetAnnouncementBannersRequest) Reset() {
*x = GetAnnouncementBannersRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_proto_agent_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetAnnouncementBannersRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAnnouncementBannersRequest) ProtoMessage() {}
func (x *GetAnnouncementBannersRequest) ProtoReflect() protoreflect.Message {
mi := &file_agent_proto_agent_proto_msgTypes[22]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetAnnouncementBannersRequest.ProtoReflect.Descriptor instead.
func (*GetAnnouncementBannersRequest) Descriptor() ([]byte, []int) {
return file_agent_proto_agent_proto_rawDescGZIP(), []int{22}
}
type GetAnnouncementBannersResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
AnnouncementBanners []*BannerConfig `protobuf:"bytes,1,rep,name=announcement_banners,json=announcementBanners,proto3" json:"announcement_banners,omitempty"`
}
func (x *GetAnnouncementBannersResponse) Reset() {
*x = GetAnnouncementBannersResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_proto_agent_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetAnnouncementBannersResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAnnouncementBannersResponse) ProtoMessage() {}
func (x *GetAnnouncementBannersResponse) ProtoReflect() protoreflect.Message {
mi := &file_agent_proto_agent_proto_msgTypes[23]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetAnnouncementBannersResponse.ProtoReflect.Descriptor instead.
func (*GetAnnouncementBannersResponse) Descriptor() ([]byte, []int) {
return file_agent_proto_agent_proto_rawDescGZIP(), []int{23}
}
func (x *GetAnnouncementBannersResponse) GetAnnouncementBanners() []*BannerConfig {
if x != nil {
return x.AnnouncementBanners
}
return nil
}
type BannerConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
BackgroundColor string `protobuf:"bytes,3,opt,name=background_color,json=backgroundColor,proto3" json:"background_color,omitempty"`
}
func (x *BannerConfig) Reset() {
*x = BannerConfig{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_proto_agent_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BannerConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BannerConfig) ProtoMessage() {}
func (x *BannerConfig) ProtoReflect() protoreflect.Message {
mi := &file_agent_proto_agent_proto_msgTypes[24]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BannerConfig.ProtoReflect.Descriptor instead.
func (*BannerConfig) Descriptor() ([]byte, []int) {
return file_agent_proto_agent_proto_rawDescGZIP(), []int{24}
}
func (x *BannerConfig) GetEnabled() bool {
if x != nil {
return x.Enabled
}
return false
}
func (x *BannerConfig) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
func (x *BannerConfig) GetBackgroundColor() string {
if x != nil {
return x.BackgroundColor
}
return ""
}
type WorkspaceApp_Healthcheck struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1872,7 +2020,7 @@ type WorkspaceApp_Healthcheck struct {
func (x *WorkspaceApp_Healthcheck) Reset() {
*x = WorkspaceApp_Healthcheck{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_proto_agent_proto_msgTypes[22]
mi := &file_agent_proto_agent_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1885,7 +2033,7 @@ func (x *WorkspaceApp_Healthcheck) String() string {
func (*WorkspaceApp_Healthcheck) ProtoMessage() {}
func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message {
mi := &file_agent_proto_agent_proto_msgTypes[22]
mi := &file_agent_proto_agent_proto_msgTypes[25]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1936,7 +2084,7 @@ type WorkspaceAgentMetadata_Result struct {
func (x *WorkspaceAgentMetadata_Result) Reset() {
*x = WorkspaceAgentMetadata_Result{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_proto_agent_proto_msgTypes[23]
mi := &file_agent_proto_agent_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1949,7 +2097,7 @@ func (x *WorkspaceAgentMetadata_Result) String() string {
func (*WorkspaceAgentMetadata_Result) ProtoMessage() {}
func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message {
mi := &file_agent_proto_agent_proto_msgTypes[23]
mi := &file_agent_proto_agent_proto_msgTypes[26]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2008,7 +2156,7 @@ type WorkspaceAgentMetadata_Description struct {
func (x *WorkspaceAgentMetadata_Description) Reset() {
*x = WorkspaceAgentMetadata_Description{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_proto_agent_proto_msgTypes[24]
mi := &file_agent_proto_agent_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2021,7 +2169,7 @@ func (x *WorkspaceAgentMetadata_Description) String() string {
func (*WorkspaceAgentMetadata_Description) ProtoMessage() {}
func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message {
mi := &file_agent_proto_agent_proto_msgTypes[24]
mi := &file_agent_proto_agent_proto_msgTypes[27]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2086,7 +2234,7 @@ type Stats_Metric struct {
func (x *Stats_Metric) Reset() {
*x = Stats_Metric{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_proto_agent_proto_msgTypes[27]
mi := &file_agent_proto_agent_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2099,7 +2247,7 @@ func (x *Stats_Metric) String() string {
func (*Stats_Metric) ProtoMessage() {}
func (x *Stats_Metric) ProtoReflect() protoreflect.Message {
mi := &file_agent_proto_agent_proto_msgTypes[27]
mi := &file_agent_proto_agent_proto_msgTypes[30]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2155,7 +2303,7 @@ type Stats_Metric_Label struct {
func (x *Stats_Metric_Label) Reset() {
*x = Stats_Metric_Label{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_proto_agent_proto_msgTypes[28]
mi := &file_agent_proto_agent_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2168,7 +2316,7 @@ func (x *Stats_Metric_Label) String() string {
func (*Stats_Metric_Label) ProtoMessage() {}
func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message {
mi := &file_agent_proto_agent_proto_msgTypes[28]
mi := &file_agent_proto_agent_proto_msgTypes[31]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2210,7 +2358,7 @@ type BatchUpdateAppHealthRequest_HealthUpdate struct {
func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() {
*x = BatchUpdateAppHealthRequest_HealthUpdate{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_proto_agent_proto_msgTypes[29]
mi := &file_agent_proto_agent_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2223,7 +2371,7 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string {
func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {}
func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message {
mi := &file_agent_proto_agent_proto_msgTypes[29]
mi := &file_agent_proto_agent_proto_msgTypes[32]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2594,64 +2742,87 @@ var file_agent_proto_agent_proto_rawDesc = []byte{
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x69,
0x6d, 0x69, 0x74, 0x5f, 0x65, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x08, 0x52, 0x10, 0x6c, 0x6f, 0x67, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x78, 0x63, 0x65,
0x65, 0x64, 0x65, 0x64, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74,
0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f,
0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a,
0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49,
0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a,
0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e,
0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xf6, 0x05, 0x0a, 0x05, 0x41, 0x67,
0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65,
0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61,
0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61,
0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65,
0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e,
0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f,
0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69,
0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c,
0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61,
0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c,
0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e,
0x65, 0x64, 0x65, 0x64, 0x22, 0x1f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75,
0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f,
0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x14, 0x61, 0x6e, 0x6e, 0x6f, 0x75,
0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x52, 0x13, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e,
0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x6d, 0x0a, 0x0c, 0x42, 0x61, 0x6e, 0x6e,
0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62,
0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10,
0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6f, 0x72,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75,
0x6e, 0x64, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65,
0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c,
0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00,
0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10,
0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02,
0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a,
0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xef, 0x06, 0x0a,
0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e,
0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65,
0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65,
0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66,
0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e,
0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76,
0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12,
0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22,
0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74,
0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64,
0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61,
0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a,
0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48,
0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61,
0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e,
0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76,
0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70,
0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e,
0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12,
0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e,
0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67,
0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61,
0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e,
0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62,
0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67,
0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e,
0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f,
0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65,
0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f,
0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74,
0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74,
0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75,
0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72,
0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75,
0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72,
0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55,
0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65,
0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74,
0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65,
0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63,
0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61,
0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f,
0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12,
0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32,
0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e,
0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e,
0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42,
0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27,
0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64,
0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e,
0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -2667,7 +2838,7 @@ func file_agent_proto_agent_proto_rawDescGZIP() []byte {
}
var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 7)
var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 33)
var file_agent_proto_agent_proto_goTypes = []interface{}{
(AppHealth)(0), // 0: coder.agent.v2.AppHealth
(WorkspaceApp_SharingLevel)(0), // 1: coder.agent.v2.WorkspaceApp.SharingLevel
@@ -2698,73 +2869,79 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{
(*Log)(nil), // 26: coder.agent.v2.Log
(*BatchCreateLogsRequest)(nil), // 27: coder.agent.v2.BatchCreateLogsRequest
(*BatchCreateLogsResponse)(nil), // 28: coder.agent.v2.BatchCreateLogsResponse
(*WorkspaceApp_Healthcheck)(nil), // 29: coder.agent.v2.WorkspaceApp.Healthcheck
(*WorkspaceAgentMetadata_Result)(nil), // 30: coder.agent.v2.WorkspaceAgentMetadata.Result
(*WorkspaceAgentMetadata_Description)(nil), // 31: coder.agent.v2.WorkspaceAgentMetadata.Description
nil, // 32: coder.agent.v2.Manifest.EnvironmentVariablesEntry
nil, // 33: coder.agent.v2.Stats.ConnectionsByProtoEntry
(*Stats_Metric)(nil), // 34: coder.agent.v2.Stats.Metric
(*Stats_Metric_Label)(nil), // 35: coder.agent.v2.Stats.Metric.Label
(*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 36: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
(*durationpb.Duration)(nil), // 37: google.protobuf.Duration
(*proto.DERPMap)(nil), // 38: coder.tailnet.v2.DERPMap
(*timestamppb.Timestamp)(nil), // 39: google.protobuf.Timestamp
(*GetAnnouncementBannersRequest)(nil), // 29: coder.agent.v2.GetAnnouncementBannersRequest
(*GetAnnouncementBannersResponse)(nil), // 30: coder.agent.v2.GetAnnouncementBannersResponse
(*BannerConfig)(nil), // 31: coder.agent.v2.BannerConfig
(*WorkspaceApp_Healthcheck)(nil), // 32: coder.agent.v2.WorkspaceApp.Healthcheck
(*WorkspaceAgentMetadata_Result)(nil), // 33: coder.agent.v2.WorkspaceAgentMetadata.Result
(*WorkspaceAgentMetadata_Description)(nil), // 34: coder.agent.v2.WorkspaceAgentMetadata.Description
nil, // 35: coder.agent.v2.Manifest.EnvironmentVariablesEntry
nil, // 36: coder.agent.v2.Stats.ConnectionsByProtoEntry
(*Stats_Metric)(nil), // 37: coder.agent.v2.Stats.Metric
(*Stats_Metric_Label)(nil), // 38: coder.agent.v2.Stats.Metric.Label
(*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 39: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
(*durationpb.Duration)(nil), // 40: google.protobuf.Duration
(*proto.DERPMap)(nil), // 41: coder.tailnet.v2.DERPMap
(*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp
}
var file_agent_proto_agent_proto_depIdxs = []int32{
1, // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel
29, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck
32, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck
2, // 2: coder.agent.v2.WorkspaceApp.health:type_name -> coder.agent.v2.WorkspaceApp.Health
37, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration
30, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
31, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
32, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry
38, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap
40, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration
33, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
34, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
35, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry
41, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap
8, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript
7, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp
31, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
33, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry
34, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric
34, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description
36, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry
37, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric
14, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats
37, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration
40, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration
4, // 15: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State
39, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp
42, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp
17, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle
36, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
39, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate
5, // 19: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem
21, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup
30, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
33, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result
23, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata
39, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp
42, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp
6, // 24: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level
26, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log
37, // 26: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration
39, // 27: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp
37, // 28: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration
37, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration
3, // 30: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type
35, // 31: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label
0, // 32: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth
11, // 33: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest
13, // 34: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest
15, // 35: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest
18, // 36: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest
19, // 37: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest
22, // 38: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest
24, // 39: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest
27, // 40: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest
10, // 41: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest
12, // 42: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner
16, // 43: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse
17, // 44: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle
20, // 45: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse
21, // 46: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup
25, // 47: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse
28, // 48: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse
41, // [41:49] is the sub-list for method output_type
33, // [33:41] is the sub-list for method input_type
33, // [33:33] is the sub-list for extension type_name
33, // [33:33] is the sub-list for extension extendee
0, // [0:33] is the sub-list for field type_name
31, // 26: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig
40, // 27: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration
42, // 28: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp
40, // 29: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration
40, // 30: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration
3, // 31: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type
38, // 32: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label
0, // 33: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth
11, // 34: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest
13, // 35: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest
15, // 36: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest
18, // 37: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest
19, // 38: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest
22, // 39: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest
24, // 40: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest
27, // 41: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest
29, // 42: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest
10, // 43: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest
12, // 44: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner
16, // 45: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse
17, // 46: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle
20, // 47: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse
21, // 48: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup
25, // 49: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse
28, // 50: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse
30, // 51: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse
43, // [43:52] is the sub-list for method output_type
34, // [34:43] is the sub-list for method input_type
34, // [34:34] is the sub-list for extension type_name
34, // [34:34] is the sub-list for extension extendee
0, // [0:34] is the sub-list for field type_name
}
func init() { file_agent_proto_agent_proto_init() }
@@ -3038,7 +3215,7 @@ func file_agent_proto_agent_proto_init() {
}
}
file_agent_proto_agent_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WorkspaceApp_Healthcheck); i {
switch v := v.(*GetAnnouncementBannersRequest); i {
case 0:
return &v.state
case 1:
@@ -3050,7 +3227,7 @@ func file_agent_proto_agent_proto_init() {
}
}
file_agent_proto_agent_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WorkspaceAgentMetadata_Result); i {
switch v := v.(*GetAnnouncementBannersResponse); i {
case 0:
return &v.state
case 1:
@@ -3062,7 +3239,31 @@ func file_agent_proto_agent_proto_init() {
}
}
file_agent_proto_agent_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WorkspaceAgentMetadata_Description); i {
switch v := v.(*BannerConfig); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_proto_agent_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WorkspaceApp_Healthcheck); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_proto_agent_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WorkspaceAgentMetadata_Result); i {
case 0:
return &v.state
case 1:
@@ -3074,6 +3275,18 @@ func file_agent_proto_agent_proto_init() {
}
}
file_agent_proto_agent_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WorkspaceAgentMetadata_Description); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Stats_Metric); i {
case 0:
return &v.state
@@ -3085,7 +3298,7 @@ func file_agent_proto_agent_proto_init() {
return nil
}
}
file_agent_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
file_agent_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Stats_Metric_Label); i {
case 0:
return &v.state
@@ -3097,7 +3310,7 @@ func file_agent_proto_agent_proto_init() {
return nil
}
}
file_agent_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} {
file_agent_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BatchUpdateAppHealthRequest_HealthUpdate); i {
case 0:
return &v.state
@@ -3116,7 +3329,7 @@ func file_agent_proto_agent_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_agent_proto_agent_proto_rawDesc,
NumEnums: 7,
NumMessages: 30,
NumMessages: 33,
NumExtensions: 0,
NumServices: 1,
},
+13
View File
@@ -251,6 +251,18 @@ message BatchCreateLogsResponse {
bool log_limit_exceeded = 1;
}
message GetAnnouncementBannersRequest {}
message GetAnnouncementBannersResponse {
repeated BannerConfig announcement_banners = 1;
}
message BannerConfig {
bool enabled = 1;
string message = 2;
string background_color = 3;
}
service Agent {
rpc GetManifest(GetManifestRequest) returns (Manifest);
rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner);
@@ -260,4 +272,5 @@ service Agent {
rpc UpdateStartup(UpdateStartupRequest) returns (Startup);
rpc BatchUpdateMetadata(BatchUpdateMetadataRequest) returns (BatchUpdateMetadataResponse);
rpc BatchCreateLogs(BatchCreateLogsRequest) returns (BatchCreateLogsResponse);
rpc GetAnnouncementBanners(GetAnnouncementBannersRequest) returns (GetAnnouncementBannersResponse);
}
+41 -1
View File
@@ -46,6 +46,7 @@ type DRPCAgentClient interface {
UpdateStartup(ctx context.Context, in *UpdateStartupRequest) (*Startup, error)
BatchUpdateMetadata(ctx context.Context, in *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error)
BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error)
GetAnnouncementBanners(ctx context.Context, in *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error)
}
type drpcAgentClient struct {
@@ -130,6 +131,15 @@ func (c *drpcAgentClient) BatchCreateLogs(ctx context.Context, in *BatchCreateLo
return out, nil
}
func (c *drpcAgentClient) GetAnnouncementBanners(ctx context.Context, in *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) {
out := new(GetAnnouncementBannersResponse)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/GetAnnouncementBanners", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
type DRPCAgentServer interface {
GetManifest(context.Context, *GetManifestRequest) (*Manifest, error)
GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error)
@@ -139,6 +149,7 @@ type DRPCAgentServer interface {
UpdateStartup(context.Context, *UpdateStartupRequest) (*Startup, error)
BatchUpdateMetadata(context.Context, *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error)
BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error)
GetAnnouncementBanners(context.Context, *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error)
}
type DRPCAgentUnimplementedServer struct{}
@@ -175,9 +186,13 @@ func (s *DRPCAgentUnimplementedServer) BatchCreateLogs(context.Context, *BatchCr
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) GetAnnouncementBanners(context.Context, *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
type DRPCAgentDescription struct{}
func (DRPCAgentDescription) NumMethods() int { return 8 }
func (DRPCAgentDescription) NumMethods() int { return 9 }
func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
switch n {
@@ -253,6 +268,15 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver,
in1.(*BatchCreateLogsRequest),
)
}, DRPCAgentServer.BatchCreateLogs, true
case 8:
return "/coder.agent.v2.Agent/GetAnnouncementBanners", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
GetAnnouncementBanners(
ctx,
in1.(*GetAnnouncementBannersRequest),
)
}, DRPCAgentServer.GetAnnouncementBanners, true
default:
return "", nil, nil, nil, false
}
@@ -389,3 +413,19 @@ func (x *drpcAgent_BatchCreateLogsStream) SendAndClose(m *BatchCreateLogsRespons
}
return x.CloseSend()
}
type DRPCAgent_GetAnnouncementBannersStream interface {
drpc.Stream
SendAndClose(*GetAnnouncementBannersResponse) error
}
type drpcAgent_GetAnnouncementBannersStream struct {
drpc.Stream
}
func (x *drpcAgent_GetAnnouncementBannersStream) SendAndClose(m *GetAnnouncementBannersResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}
+11
View File
@@ -27,6 +27,7 @@ import (
"cdr.dev/slog/sloggers/slogstackdriver"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agentproc"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/reaper"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/codersdk"
@@ -48,6 +49,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
slogHumanPath string
slogJSONPath string
slogStackdriverPath string
blockFileTransfer bool
)
cmd := &serpent.Command{
Use: "agent",
@@ -314,6 +316,8 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
// Intentionally set this to nil. It's mainly used
// for testing.
ModifiedProcesses: nil,
BlockFileTransfer: blockFileTransfer,
})
promHandler := agent.PrometheusMetricsHandler(prometheusRegistry, logger)
@@ -417,6 +421,13 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
Default: "",
Value: serpent.StringOf(&slogStackdriverPath),
},
{
Flag: "block-file-transfer",
Default: "false",
Env: "CODER_AGENT_BLOCK_FILE_TRANSFER",
Description: fmt.Sprintf("Block file transfer using known applications: %s.", strings.Join(agentssh.BlockedFileTransferCommands, ",")),
Value: serpent.BoolOf(&blockFileTransfer),
},
}
return cmd
+3
View File
@@ -37,6 +37,9 @@ func ExternalAuth(ctx context.Context, writer io.Writer, opts ExternalAuthOption
if auth.Authenticated {
return nil
}
if auth.Optional {
continue
}
_, _ = fmt.Fprintf(writer, "You must authenticate with %s to create a workspace with this template. Visit:\n\n\t%s\n\n", auth.DisplayName, auth.AuthenticateURL)
+6 -1
View File
@@ -7,6 +7,7 @@ import (
"reflect"
"strings"
"github.com/jedib0t/go-pretty/v6/table"
"golang.org/x/xerrors"
"github.com/coder/serpent"
@@ -143,7 +144,11 @@ func (f *tableFormat) AttachOptions(opts *serpent.OptionSet) {
// Format implements OutputFormat.
func (f *tableFormat) Format(_ context.Context, data any) (string, error) {
return DisplayTable(data, f.sort, f.columns)
headers := make(table.Row, len(f.allColumns))
for i, header := range f.allColumns {
headers[i] = header
}
return renderTable(data, f.sort, headers, f.columns)
}
type jsonFormat struct{}
+13 -5
View File
@@ -10,7 +10,7 @@ import (
"github.com/coder/serpent"
)
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter, defaultOverrides map[string]string) (string, error) {
label := templateVersionParameter.Name
if templateVersionParameter.DisplayName != "" {
label = templateVersionParameter.DisplayName
@@ -26,6 +26,11 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
}
defaultValue := templateVersionParameter.DefaultValue
if v, ok := defaultOverrides[templateVersionParameter.Name]; ok {
defaultValue = v
}
var err error
var value string
if templateVersionParameter.Type == "list(string)" {
@@ -38,7 +43,10 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
return "", err
}
values, err := MultiSelect(inv, options)
values, err := MultiSelect(inv, MultiSelectOptions{
Options: options,
Defaults: options,
})
if err == nil {
v, err := json.Marshal(&values)
if err != nil {
@@ -58,7 +66,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
var richParameterOption *codersdk.TemplateVersionParameterOption
richParameterOption, err = RichSelect(inv, RichSelectOptions{
Options: templateVersionParameter.Options,
Default: templateVersionParameter.DefaultValue,
Default: defaultValue,
HideSearch: true,
})
if err == nil {
@@ -69,7 +77,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
} else {
text := "Enter a value"
if !templateVersionParameter.Required {
text += fmt.Sprintf(" (default: %q)", templateVersionParameter.DefaultValue)
text += fmt.Sprintf(" (default: %q)", defaultValue)
}
text += ":"
@@ -87,7 +95,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
// If they didn't specify anything, use the default value if set.
if len(templateVersionParameter.Options) == 0 && value == "" {
value = templateVersionParameter.DefaultValue
value = defaultValue
}
return value, nil
+13 -42
View File
@@ -14,48 +14,11 @@ import (
"github.com/coder/serpent"
)
func init() {
survey.SelectQuestionTemplate = `
{{- define "option"}}
{{- " " }}{{- if eq .SelectedIndex .CurrentIndex }}{{color "green" }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
{{- .CurrentOpt.Value}}
{{- color "reset"}}
{{end}}
{{- if not .ShowAnswer }}
{{- if .Config.Icons.Help.Text }}
{{- if .FilterMessage }}{{ "Search:" }}{{ .FilterMessage }}
{{- else }}
{{- color "black+h"}}{{- "Type to search" }}{{color "reset"}}
{{- end }}
{{- "\n" }}
{{- end }}
{{- "\n" }}
{{- range $ix, $option := .PageEntries}}
{{- template "option" $.IterateOption $ix $option}}
{{- end}}
{{- end }}`
survey.MultiSelectQuestionTemplate = `
{{- define "option"}}
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
{{- if index .Checked .CurrentOpt.Index }}{{color .Config.Icons.MarkedOption.Format }} {{ .Config.Icons.MarkedOption.Text }} {{else}}{{color .Config.Icons.UnmarkedOption.Format }} {{ .Config.Icons.UnmarkedOption.Text }} {{end}}
{{- color "reset"}}
{{- " "}}{{- .CurrentOpt.Value}}
{{end}}
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- if not .ShowAnswer }}
{{- "\n"}}
{{- range $ix, $option := .PageEntries}}
{{- template "option" $.IterateOption $ix $option}}
{{- end}}
{{- end}}`
}
type SelectOptions struct {
Options []string
// Default will be highlighted first if it's a valid option.
Default string
Message string
Size int
HideSearch bool
}
@@ -122,6 +85,7 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
Options: opts.Options,
Default: defaultOption,
PageSize: opts.Size,
Message: opts.Message,
}, &value, survey.WithIcons(func(is *survey.IconSet) {
is.Help.Text = "Type to search"
if opts.HideSearch {
@@ -138,15 +102,22 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
return value, err
}
func MultiSelect(inv *serpent.Invocation, items []string) ([]string, error) {
type MultiSelectOptions struct {
Message string
Options []string
Defaults []string
}
func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, error) {
// Similar hack is applied to Select()
if flag.Lookup("test.v") != nil {
return items, nil
return opts.Defaults, nil
}
prompt := &survey.MultiSelect{
Options: items,
Default: items,
Options: opts.Options,
Default: opts.Defaults,
Message: opts.Message,
}
var values []string
+4 -1
View File
@@ -107,7 +107,10 @@ func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {
var values []string
cmd := &serpent.Command{
Handler: func(inv *serpent.Invocation) error {
selectedItems, err := cliui.MultiSelect(inv, items)
selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
Options: items,
Defaults: items,
})
if err == nil {
values = selectedItems
}
+74 -18
View File
@@ -22,6 +22,13 @@ func Table() table.Writer {
return tableWriter
}
// This type can be supplied as part of a slice to DisplayTable
// or to a `TableFormat` `Format` call to render a separator.
// Leading separators are not supported and trailing separators
// are ignored by the table formatter.
// e.g. `[]any{someRow, TableSeparator, someRow}`
type TableSeparator struct{}
// filterTableColumns returns configurations to hide columns
// that are not provided in the array. If the array is empty,
// no filtering will occur!
@@ -47,8 +54,12 @@ func filterTableColumns(header table.Row, columns []string) []table.ColumnConfig
return columnConfigs
}
// DisplayTable renders a table as a string. The input argument must be a slice
// of structs. At least one field in the struct must have a `table:""` tag
// DisplayTable renders a table as a string. The input argument can be:
// - a struct slice.
// - an interface slice, where the first element is a struct,
// and all other elements are of the same type, or a TableSeparator.
//
// At least one field in the struct must have a `table:""` tag
// containing the name of the column in the outputted table.
//
// If `sort` is not specified, the field with the `table:"$NAME,default_sort"`
@@ -66,11 +77,20 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
v := reflect.Indirect(reflect.ValueOf(out))
if v.Kind() != reflect.Slice {
return "", xerrors.Errorf("DisplayTable called with a non-slice type")
return "", xerrors.New("DisplayTable called with a non-slice type")
}
var tableType reflect.Type
if v.Type().Elem().Kind() == reflect.Interface {
if v.Len() == 0 {
return "", xerrors.New("DisplayTable called with empty interface slice")
}
tableType = reflect.Indirect(reflect.ValueOf(v.Index(0).Interface())).Type()
} else {
tableType = v.Type().Elem()
}
// Get the list of table column headers.
headersRaw, defaultSort, err := typeToTableHeaders(v.Type().Elem(), true)
headersRaw, defaultSort, err := typeToTableHeaders(tableType, true)
if err != nil {
return "", xerrors.Errorf("get table headers recursively for type %q: %w", v.Type().Elem().String(), err)
}
@@ -82,9 +102,8 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
}
headers := make(table.Row, len(headersRaw))
for i, header := range headersRaw {
headers[i] = header
headers[i] = strings.ReplaceAll(header, "_", " ")
}
// Verify that the given sort column and filter columns are valid.
if sort != "" || len(filterColumns) != 0 {
headersMap := make(map[string]string, len(headersRaw))
@@ -130,6 +149,11 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
return "", xerrors.Errorf("specified sort column %q not found in table headers, available columns are %q", sort, strings.Join(headersRaw, `", "`))
}
}
return renderTable(out, sort, headers, filterColumns)
}
func renderTable(out any, sort string, headers table.Row, filterColumns []string) (string, error) {
v := reflect.Indirect(reflect.ValueOf(out))
// Setup the table formatter.
tw := Table()
@@ -143,15 +167,22 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
// Write each struct to the table.
for i := 0; i < v.Len(); i++ {
cur := v.Index(i).Interface()
_, ok := cur.(TableSeparator)
if ok {
tw.AppendSeparator()
continue
}
// Format the row as a slice.
rowMap, err := valueToTableMap(v.Index(i))
// ValueToTableMap does what `reflect.Indirect` does
rowMap, err := valueToTableMap(reflect.ValueOf(cur))
if err != nil {
return "", xerrors.Errorf("get table row map %v: %w", i, err)
}
rowSlice := make([]any, len(headers))
for i, h := range headersRaw {
v, ok := rowMap[h]
for i, h := range headers {
v, ok := rowMap[h.(string)]
if !ok {
v = nil
}
@@ -174,6 +205,24 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
}
}
// Guard against nil dereferences
if v != nil {
rt := reflect.TypeOf(v)
switch rt.Kind() {
case reflect.Slice:
// By default, the behavior is '%v', which just returns a string like
// '[a b c]'. This will add commas in between each value.
strs := make([]string, 0)
vt := reflect.ValueOf(v)
for i := 0; i < vt.Len(); i++ {
strs = append(strs, fmt.Sprintf("%v", vt.Index(i).Interface()))
}
v = "[" + strings.Join(strs, ", ") + "]"
default:
// Leave it as it is
}
}
rowSlice[i] = v
}
@@ -188,25 +237,28 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
// returned. If the table tag is malformed, an error is returned.
//
// The returned name is transformed from "snake_case" to "normal text".
func parseTableStructTag(field reflect.StructField) (name string, defaultSort, recursive bool, skipParentName bool, err error) {
func parseTableStructTag(field reflect.StructField) (name string, defaultSort, noSortOpt, recursive, skipParentName bool, err error) {
tags, err := structtag.Parse(string(field.Tag))
if err != nil {
return "", false, false, false, xerrors.Errorf("parse struct field tag %q: %w", string(field.Tag), err)
return "", false, false, false, false, xerrors.Errorf("parse struct field tag %q: %w", string(field.Tag), err)
}
tag, err := tags.Get("table")
if err != nil || tag.Name == "-" {
// tags.Get only returns an error if the tag is not found.
return "", false, false, false, nil
return "", false, false, false, false, nil
}
defaultSortOpt := false
noSortOpt = false
recursiveOpt := false
skipParentNameOpt := false
for _, opt := range tag.Options {
switch opt {
case "default_sort":
defaultSortOpt = true
case "nosort":
noSortOpt = true
case "recursive":
recursiveOpt = true
case "recursive_inline":
@@ -216,11 +268,11 @@ func parseTableStructTag(field reflect.StructField) (name string, defaultSort, r
recursiveOpt = true
skipParentNameOpt = true
default:
return "", false, false, false, xerrors.Errorf("unknown option %q in struct field tag", opt)
return "", false, false, false, false, xerrors.Errorf("unknown option %q in struct field tag", opt)
}
}
return strings.ReplaceAll(tag.Name, "_", " "), defaultSortOpt, recursiveOpt, skipParentNameOpt, nil
return strings.ReplaceAll(tag.Name, "_", " "), defaultSortOpt, noSortOpt, recursiveOpt, skipParentNameOpt, nil
}
func isStructOrStructPointer(t reflect.Type) bool {
@@ -244,12 +296,16 @@ func typeToTableHeaders(t reflect.Type, requireDefault bool) ([]string, string,
headers := []string{}
defaultSortName := ""
noSortOpt := false
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
name, defaultSort, recursive, skip, err := parseTableStructTag(field)
name, defaultSort, noSort, recursive, skip, err := parseTableStructTag(field)
if err != nil {
return nil, "", xerrors.Errorf("parse struct tags for field %q in type %q: %w", field.Name, t.String(), err)
}
if requireDefault && noSort {
noSortOpt = true
}
if name == "" && (recursive && skip) {
return nil, "", xerrors.Errorf("a name is required for the field %q. "+
@@ -292,8 +348,8 @@ func typeToTableHeaders(t reflect.Type, requireDefault bool) ([]string, string,
headers = append(headers, name)
}
if defaultSortName == "" && requireDefault {
return nil, "", xerrors.Errorf("no field marked as default_sort in type %q", t.String())
if defaultSortName == "" && requireDefault && !noSortOpt {
return nil, "", xerrors.Errorf("no field marked as default_sort or nosort in type %q", t.String())
}
return headers, defaultSortName, nil
@@ -320,7 +376,7 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
for i := 0; i < val.NumField(); i++ {
field := val.Type().Field(i)
fieldVal := val.Field(i)
name, _, recursive, skip, err := parseTableStructTag(field)
name, _, _, recursive, skip, err := parseTableStructTag(field)
if err != nil {
return nil, xerrors.Errorf("parse struct tags for field %q in type %T: %w", field.Name, val, err)
}
+44 -16
View File
@@ -138,10 +138,10 @@ func Test_DisplayTable(t *testing.T) {
t.Parallel()
expected := `
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
foo 10 [a, b, c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
`
// Test with non-pointer values.
@@ -165,10 +165,10 @@ foo 10 [a b c] foo1 11 foo2 12 foo3
t.Parallel()
expected := `
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
foo 10 [a, b, c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
`
out, err := cliui.DisplayTable(in, "age", nil)
@@ -218,6 +218,42 @@ Alice 25
compareTables(t, expected, out)
})
// This test ensures we can display dynamically typed slices
t.Run("Interfaces", func(t *testing.T) {
t.Parallel()
in := []any{tableTest1{}}
out, err := cliui.DisplayTable(in, "", nil)
t.Log("rendered table:\n" + out)
require.NoError(t, err)
other := []tableTest1{{}}
expected, err := cliui.DisplayTable(other, "", nil)
require.NoError(t, err)
compareTables(t, expected, out)
})
t.Run("WithSeparator", func(t *testing.T) {
t.Parallel()
expected := `
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
---------------------------------------------------------------------------------------------------------------------------------------------------------------
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
---------------------------------------------------------------------------------------------------------------------------------------------------------------
foo 10 [a, b, c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
`
var inlineIn []any
for _, v := range in {
inlineIn = append(inlineIn, v)
inlineIn = append(inlineIn, cliui.TableSeparator{})
}
out, err := cliui.DisplayTable(inlineIn, "", nil)
t.Log("rendered table:\n" + out)
require.NoError(t, err)
compareTables(t, expected, out)
})
// This test ensures that safeties against invalid use of `table` tags
// causes errors (even without data).
t.Run("Errors", func(t *testing.T) {
@@ -255,14 +291,6 @@ Alice 25
_, err := cliui.DisplayTable(in, "", nil)
require.Error(t, err)
})
t.Run("WithData", func(t *testing.T) {
t.Parallel()
in := []any{tableTest1{}}
_, err := cliui.DisplayTable(in, "", nil)
require.Error(t, err)
})
})
t.Run("NotStruct", func(t *testing.T) {
+3 -3
View File
@@ -230,12 +230,12 @@ func (r *RootCmd) configSSH() *serpent.Command {
Annotations: workspaceCommand,
Use: "config-ssh",
Short: "Add an SSH Host entry for your workspaces \"ssh coder.workspace\"",
Long: formatExamples(
example{
Long: FormatExamples(
Example{
Description: "You can use -o (or --ssh-option) so set SSH options to be used for all your workspaces",
Command: "coder config-ssh -o ForwardAgent=yes",
},
example{
Example{
Description: "You can use --dry-run (or -n) to see the changes that would be made",
Command: "coder config-ssh --dry-run",
},
+17 -8
View File
@@ -35,8 +35,8 @@ func (r *RootCmd) create() *serpent.Command {
Annotations: workspaceCommand,
Use: "create [name]",
Short: "Create a workspace",
Long: formatExamples(
example{
Long: FormatExamples(
Example{
Description: "Create a workspace for another user (if you have permission)",
Command: "coder create <username>/<workspace_name>",
},
@@ -165,6 +165,11 @@ func (r *RootCmd) create() *serpent.Command {
return xerrors.Errorf("can't parse given parameter values: %w", err)
}
cliBuildParameterDefaults, err := asWorkspaceBuildParameters(parameterFlags.richParameterDefaults)
if err != nil {
return xerrors.Errorf("can't parse given parameter defaults: %w", err)
}
var sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
if copyParametersFrom != "" {
sourceWorkspaceParameters, err = client.WorkspaceBuildParameters(inv.Context(), sourceWorkspace.LatestBuild.ID)
@@ -178,8 +183,9 @@ func (r *RootCmd) create() *serpent.Command {
TemplateVersionID: templateVersionID,
NewWorkspaceName: workspaceName,
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliBuildParameters,
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliBuildParameters,
RichParameterDefaults: cliBuildParameterDefaults,
SourceWorkspaceParameters: sourceWorkspaceParameters,
})
@@ -262,6 +268,7 @@ func (r *RootCmd) create() *serpent.Command {
cliui.SkipPromptOption(),
)
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
cmd.Options = append(cmd.Options, parameterFlags.cliParameterDefaults()...)
return cmd
}
@@ -276,9 +283,10 @@ type prepWorkspaceBuildArgs struct {
PromptBuildOptions bool
BuildOptions []codersdk.WorkspaceBuildParameter
PromptRichParameters bool
RichParameters []codersdk.WorkspaceBuildParameter
RichParameterFile string
PromptRichParameters bool
RichParameters []codersdk.WorkspaceBuildParameter
RichParameterFile string
RichParameterDefaults []codersdk.WorkspaceBuildParameter
}
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
@@ -311,7 +319,8 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
WithBuildOptions(args.BuildOptions).
WithPromptRichParameters(args.PromptRichParameters).
WithRichParameters(args.RichParameters).
WithRichParametersFile(parameterFile)
WithRichParametersFile(parameterFile).
WithRichParametersDefaults(args.RichParameterDefaults)
buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
if err != nil {
return nil, err
+62
View File
@@ -315,6 +315,68 @@ func TestCreateWithRichParameters(t *testing.T) {
<-doneChan
})
t.Run("ParametersDefaults", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name,
"--parameter-default", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
"--parameter-default", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
"--parameter-default", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
matches := []string{
firstParameterDescription, firstParameterValue,
secondParameterDescription, secondParameterValue,
immutableParameterDescription, immutableParameterValue,
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
defaultValue := matches[i+1]
pty.ExpectMatch(match)
pty.ExpectMatch(`Enter a value (default: "` + defaultValue + `")`)
pty.WriteLine("")
}
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
<-doneChan
// Verify that the expected default values were used.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Name: "my-workspace",
})
require.NoError(t, err, "can't list available workspaces")
require.Len(t, workspaces.Workspaces, 1)
workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
require.NoError(t, err)
require.Len(t, buildParameters, 3)
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: secondParameterName, Value: secondParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: immutableParameterName, Value: immutableParameterValue})
})
t.Run("RichParametersFile", func(t *testing.T) {
t.Parallel()
+2 -2
View File
@@ -28,8 +28,8 @@ func (r *RootCmd) dotfiles() *serpent.Command {
Use: "dotfiles <git_repo_url>",
Middleware: serpent.RequireNArgs(1),
Short: "Personalize your workspace by applying a canonical dotfiles repository",
Long: formatExamples(
example{
Long: FormatExamples(
Example{
Description: "Check out and install a dotfiles repository without prompts",
Command: "coder dotfiles --yes git@github.com:example/dotfiles.git",
},
+1
View File
@@ -13,6 +13,7 @@ func (r *RootCmd) expCmd() *serpent.Command {
Children: []*serpent.Command{
r.scaletestCmd(),
r.errorExample(),
r.promptExample(),
},
}
return cmd
+2 -9
View File
@@ -14,7 +14,6 @@ import (
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/google/uuid"
@@ -245,14 +244,8 @@ func (o *scaleTestOutput) write(res harness.Results, stdout io.Writer) error {
// Sync the file to disk if it's a file.
if s, ok := w.(interface{ Sync() error }); ok {
err := s.Sync()
// On Linux, EINVAL is returned when calling fsync on /dev/stdout. We
// can safely ignore this error.
// On macOS, ENOTTY is returned when calling sync on /dev/stdout. We
// can safely ignore this error.
if err != nil && !xerrors.Is(err, syscall.EINVAL) && !xerrors.Is(err, syscall.ENOTTY) {
return xerrors.Errorf("flush output file: %w", err)
}
// Best effort. If we get an error from syncing, just ignore it.
_ = s.Sync()
}
if c != nil {
+3 -3
View File
@@ -35,8 +35,8 @@ func (r *RootCmd) externalAuthAccessToken() *serpent.Command {
Short: "Print auth for an external provider",
Long: "Print an access-token for an external auth provider. " +
"The access-token will be validated and sent to stdout with exit code 0. " +
"If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1\n" + formatExamples(
example{
"If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1\n" + FormatExamples(
Example{
Description: "Ensure that the user is authenticated with GitHub before cloning.",
Command: `#!/usr/bin/env sh
@@ -49,7 +49,7 @@ else
fi
`,
},
example{
Example{
Description: "Obtain an extra property of an access token for additional metadata.",
Command: "coder external-auth access-token slack --extra \"authed_user.id\"",
},
+38 -2
View File
@@ -58,6 +58,21 @@ func promptFirstUsername(inv *serpent.Invocation) (string, error) {
return username, nil
}
func promptFirstName(inv *serpent.Invocation) (string, error) {
name, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "(Optional) What " + pretty.Sprint(cliui.DefaultStyles.Field, "name") + " would you like?",
Default: "",
})
if err != nil {
if errors.Is(err, cliui.Canceled) {
return "", nil
}
return "", err
}
return name, nil
}
func promptFirstPassword(inv *serpent.Invocation) (string, error) {
retry:
password, err := cliui.Prompt(inv, cliui.PromptOptions{
@@ -130,6 +145,7 @@ func (r *RootCmd) login() *serpent.Command {
var (
email string
username string
name string
password string
trial bool
useTokenForSession bool
@@ -191,6 +207,7 @@ func (r *RootCmd) login() *serpent.Command {
_, _ = fmt.Fprintf(inv.Stdout, "Attempting to authenticate with %s URL: '%s'\n", urlSource, serverURL)
// nolint: nestif
if !hasFirstUser {
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Your Coder deployment hasn't been set up!\n")
@@ -212,6 +229,10 @@ func (r *RootCmd) login() *serpent.Command {
if err != nil {
return err
}
name, err = promptFirstName(inv)
if err != nil {
return err
}
}
if email == "" {
@@ -239,7 +260,7 @@ func (r *RootCmd) login() *serpent.Command {
if !inv.ParsedFlags().Changed("first-user-trial") && os.Getenv(firstUserTrialEnv) == "" {
v, _ := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Start a 30-day trial of Enterprise?",
Text: "Start a trial of Enterprise?",
IsConfirm: true,
Default: "yes",
})
@@ -249,6 +270,7 @@ func (r *RootCmd) login() *serpent.Command {
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
Email: email,
Username: username,
Name: name,
Password: password,
Trial: trial,
})
@@ -287,7 +309,8 @@ func (r *RootCmd) login() *serpent.Command {
}
sessionToken, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "Paste your token here:",
Text: "Paste your token here:",
Secret: true,
Validate: func(token string) error {
client.SetSessionToken(token)
_, err := client.User(ctx, codersdk.Me)
@@ -335,6 +358,13 @@ func (r *RootCmd) login() *serpent.Command {
return xerrors.Errorf("write server url: %w", err)
}
// If the current organization cannot be fetched, then reset the organization context.
// Otherwise, organization cli commands will fail.
_, err = CurrentOrganization(r, inv, client)
if err != nil {
_ = config.Organization().Delete()
}
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Welcome to Coder, %s! You're authenticated.\n", pretty.Sprint(cliui.DefaultStyles.Keyword, resp.Username))
return nil
},
@@ -352,6 +382,12 @@ func (r *RootCmd) login() *serpent.Command {
Description: "Specifies a username to use if creating the first user for the deployment.",
Value: serpent.StringOf(&username),
},
{
Flag: "first-user-full-name",
Env: "CODER_FIRST_USER_FULL_NAME",
Description: "Specifies a human-readable name for the first user of the deployment.",
Value: serpent.StringOf(&name),
},
{
Flag: "first-user-password",
Env: "CODER_FIRST_USER_PASSWORD",
+179 -16
View File
@@ -5,9 +5,11 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"runtime"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -18,6 +20,7 @@ import (
"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 TestLogin(t *testing.T) {
@@ -89,10 +92,11 @@ func TestLogin(t *testing.T) {
matches := []string{
"first user?", "yes",
"username", "testuser",
"email", "user@coder.com",
"password", "SomeSecurePassword!",
"password", "SomeSecurePassword!", // Confirm.
"username", coderdtest.FirstUserParams.Username,
"name", coderdtest.FirstUserParams.Name,
"email", coderdtest.FirstUserParams.Email,
"password", coderdtest.FirstUserParams.Password,
"password", coderdtest.FirstUserParams.Password, // confirm
"trial", "yes",
}
for i := 0; i < len(matches); i += 2 {
@@ -103,6 +107,64 @@ func TestLogin(t *testing.T) {
}
pty.ExpectMatch("Welcome to Coder")
<-doneChan
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
})
t.Run("InitialUserTTYNameOptional", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
// The --force-tty flag is required on Windows, because the `isatty` library does not
// accurately detect Windows ptys when they are not attached to a process:
// https://github.com/mattn/go-isatty/issues/59
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
pty := ptytest.New(t).Attach(root)
go func() {
defer close(doneChan)
err := root.Run()
assert.NoError(t, err)
}()
matches := []string{
"first user?", "yes",
"username", coderdtest.FirstUserParams.Username,
"name", "",
"email", coderdtest.FirstUserParams.Email,
"password", coderdtest.FirstUserParams.Password,
"password", coderdtest.FirstUserParams.Password, // confirm
"trial", "yes",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
<-doneChan
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
assert.Empty(t, me.Name)
})
t.Run("InitialUserTTYFlag", func(t *testing.T) {
@@ -119,10 +181,11 @@ func TestLogin(t *testing.T) {
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with flag URL: '%s'", client.URL.String()))
matches := []string{
"first user?", "yes",
"username", "testuser",
"email", "user@coder.com",
"password", "SomeSecurePassword!",
"password", "SomeSecurePassword!", // Confirm.
"username", coderdtest.FirstUserParams.Username,
"name", coderdtest.FirstUserParams.Name,
"email", coderdtest.FirstUserParams.Email,
"password", coderdtest.FirstUserParams.Password,
"password", coderdtest.FirstUserParams.Password, // confirm
"trial", "yes",
}
for i := 0; i < len(matches); i += 2 {
@@ -132,6 +195,18 @@ func TestLogin(t *testing.T) {
pty.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
})
t.Run("InitialUserFlags", func(t *testing.T) {
@@ -139,13 +214,56 @@ func TestLogin(t *testing.T) {
client := coderdtest.New(t, nil)
inv, _ := clitest.New(
t, "login", client.URL.String(),
"--first-user-username", "testuser", "--first-user-email", "user@coder.com",
"--first-user-password", "SomeSecurePassword!", "--first-user-trial",
"--first-user-username", coderdtest.FirstUserParams.Username,
"--first-user-full-name", coderdtest.FirstUserParams.Name,
"--first-user-email", coderdtest.FirstUserParams.Email,
"--first-user-password", coderdtest.FirstUserParams.Password,
"--first-user-trial",
)
pty := ptytest.New(t).Attach(inv)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectMatch("Welcome to Coder")
w.RequireSuccess()
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
})
t.Run("InitialUserFlagsNameOptional", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
inv, _ := clitest.New(
t, "login", client.URL.String(),
"--first-user-username", coderdtest.FirstUserParams.Username,
"--first-user-email", coderdtest.FirstUserParams.Email,
"--first-user-password", coderdtest.FirstUserParams.Password,
"--first-user-trial",
)
pty := ptytest.New(t).Attach(inv)
w := clitest.StartWithWaiter(t, inv)
pty.ExpectMatch("Welcome to Coder")
w.RequireSuccess()
ctx := testutil.Context(t, testutil.WaitShort)
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: coderdtest.FirstUserParams.Email,
Password: coderdtest.FirstUserParams.Password,
})
require.NoError(t, err)
client.SetSessionToken(resp.SessionToken)
me, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
assert.Empty(t, me.Name)
})
t.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) {
@@ -167,10 +285,11 @@ func TestLogin(t *testing.T) {
matches := []string{
"first user?", "yes",
"username", "testuser",
"email", "user@coder.com",
"password", "MyFirstSecurePassword!",
"password", "MyNonMatchingSecurePassword!", // Confirm.
"username", coderdtest.FirstUserParams.Username,
"name", coderdtest.FirstUserParams.Name,
"email", coderdtest.FirstUserParams.Email,
"password", coderdtest.FirstUserParams.Password,
"password", "something completely different",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
@@ -183,9 +302,9 @@ func TestLogin(t *testing.T) {
pty.ExpectMatch("Passwords do not match")
pty.ExpectMatch("Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password"))
pty.WriteLine("SomeSecurePassword!")
pty.WriteLine(coderdtest.FirstUserParams.Password)
pty.ExpectMatch("Confirm")
pty.WriteLine("SomeSecurePassword!")
pty.WriteLine(coderdtest.FirstUserParams.Password)
pty.ExpectMatch("trial")
pty.WriteLine("yes")
pty.ExpectMatch("Welcome to Coder")
@@ -304,4 +423,48 @@ func TestLogin(t *testing.T) {
// This **should not be equal** to the token we passed in.
require.NotEqual(t, client.SessionToken(), sessionFile)
})
// Login should reset the configured organization if the user is not a member
t.Run("ResetOrganization", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
root, cfg := clitest.New(t, "login", client.URL.String(), "--token", client.SessionToken())
notRealOrg := uuid.NewString()
err := cfg.Organization().Write(notRealOrg)
require.NoError(t, err, "write bad org to config")
err = root.Run()
require.NoError(t, err)
sessionFile, err := cfg.Session().Read()
require.NoError(t, err)
require.NotEqual(t, client.SessionToken(), sessionFile)
// Organization config should be deleted since the org does not exist
selected, err := cfg.Organization().Read()
require.ErrorIs(t, err, os.ErrNotExist)
require.NotEqual(t, selected, notRealOrg)
})
t.Run("KeepOrganizationContext", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
first := coderdtest.CreateFirstUser(t, client)
root, cfg := clitest.New(t, "login", client.URL.String(), "--token", client.SessionToken())
err := cfg.Organization().Write(first.OrganizationID.String())
require.NoError(t, err, "write bad org to config")
err = root.Run()
require.NoError(t, err)
sessionFile, err := cfg.Session().Read()
require.NoError(t, err)
require.NotEqual(t, client.SessionToken(), sessionFile)
// Organization config should be deleted since the org does not exist
selected, err := cfg.Organization().Read()
require.NoError(t, err)
require.Equal(t, selected, first.OrganizationID.String())
})
}
+13 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/healthsdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/serpent"
)
@@ -34,11 +35,21 @@ func (r *RootCmd) netcheck() *serpent.Command {
_, _ = fmt.Fprint(inv.Stderr, "Gathering a network report. This may take a few seconds...\n\n")
var report derphealth.Report
report.Run(ctx, &derphealth.ReportOptions{
var derpReport derphealth.Report
derpReport.Run(ctx, &derphealth.ReportOptions{
DERPMap: connInfo.DERPMap,
})
ifReport, err := healthsdk.RunInterfacesReport()
if err != nil {
return xerrors.Errorf("failed to run interfaces report: %w", err)
}
report := healthsdk.ClientNetcheckReport{
DERP: healthsdk.DERPHealthReport(derpReport),
Interfaces: ifReport,
}
raw, err := json.MarshalIndent(report, "", " ")
if err != nil {
return err
+5 -5
View File
@@ -5,7 +5,6 @@ import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
@@ -27,12 +26,13 @@ func TestNetcheck(t *testing.T) {
b := out.Bytes()
t.Log(string(b))
var report healthsdk.DERPHealthReport
var report healthsdk.ClientNetcheckReport
require.NoError(t, json.Unmarshal(b, &report))
assert.True(t, report.Healthy)
require.Len(t, report.Regions, 1+1) // 1 built-in region + 1 test-managed STUN region
for _, v := range report.Regions {
// We do not assert that the report is healthy, just that
// it has the expected number of reports per region.
require.Len(t, report.DERP.Regions, 1+1) // 1 built-in region + 1 test-managed STUN region
for _, v := range report.DERP.Regions {
require.Len(t, v.NodeReports, len(v.Region.Nodes))
}
}
+1 -1
View File
@@ -64,7 +64,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
// need to wait for the agent to start.
workspaceQuery := inv.Args[0]
autostart := true
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, codersdk.Me, workspaceQuery)
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
if err != nil {
return xerrors.Errorf("get workspace and agent: %w", err)
}
+9 -8
View File
@@ -18,11 +18,10 @@ import (
func (r *RootCmd) organizations() *serpent.Command {
cmd := &serpent.Command{
Annotations: workspaceCommand,
Use: "organizations [subcommand]",
Short: "Organization related commands",
Aliases: []string{"organization", "org", "orgs"},
Hidden: true, // Hidden until these commands are complete.
Use: "organizations [subcommand]",
Short: "Organization related commands",
Aliases: []string{"organization", "org", "orgs"},
Hidden: true, // Hidden until these commands are complete.
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
@@ -30,6 +29,8 @@ func (r *RootCmd) organizations() *serpent.Command {
r.currentOrganization(),
r.switchOrganization(),
r.createOrganization(),
r.organizationMembers(),
r.organizationRoles(),
},
}
@@ -43,12 +44,12 @@ func (r *RootCmd) switchOrganization() *serpent.Command {
cmd := &serpent.Command{
Use: "set <organization name | ID>",
Short: "set the organization used by the CLI. Pass an empty string to reset to the default organization.",
Long: "set the organization used by the CLI. Pass an empty string to reset to the default organization.\n" + formatExamples(
example{
Long: "set the organization used by the CLI. Pass an empty string to reset to the default organization.\n" + FormatExamples(
Example{
Description: "Remove the current organization and defer to the default.",
Command: "coder organizations set ''",
},
example{
Example{
Description: "Switch to a custom organization.",
Command: "coder organizations set my-org",
},
+176
View File
@@ -0,0 +1,176 @@
package cli
import (
"fmt"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) organizationMembers() *serpent.Command {
cmd := &serpent.Command{
Use: "members",
Aliases: []string{"member"},
Short: "Manage organization members",
Children: []*serpent.Command{
r.listOrganizationMembers(),
r.assignOrganizationRoles(),
r.addOrganizationMember(),
r.removeOrganizationMember(),
},
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
}
return cmd
}
func (r *RootCmd) removeOrganizationMember() *serpent.Command {
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "remove <username | user_id>",
Short: "Remove a new member to the current organization",
Middleware: serpent.Chain(
r.InitClient(client),
serpent.RequireNArgs(1),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
organization, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
}
user := inv.Args[0]
err = client.DeleteOrganizationMember(ctx, organization.ID, user)
if err != nil {
return xerrors.Errorf("could not remove member from organization %q: %w", organization.HumanName(), err)
}
_, _ = fmt.Fprintf(inv.Stdout, "Organization member removed from %q\n", organization.HumanName())
return nil
},
}
return cmd
}
func (r *RootCmd) addOrganizationMember() *serpent.Command {
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "add <username | user_id>",
Short: "Add a new member to the current organization",
Middleware: serpent.Chain(
r.InitClient(client),
serpent.RequireNArgs(1),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
organization, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
}
user := inv.Args[0]
_, err = client.PostOrganizationMember(ctx, organization.ID, user)
if err != nil {
return xerrors.Errorf("could not add member to organization %q: %w", organization.HumanName(), err)
}
_, _ = fmt.Fprintf(inv.Stdout, "Organization member added to %q\n", organization.HumanName())
return nil
},
}
return cmd
}
func (r *RootCmd) assignOrganizationRoles() *serpent.Command {
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "edit-roles <username | user_id> [roles...]",
Aliases: []string{"edit-role"},
Short: "Edit organization member's roles",
Middleware: serpent.Chain(
r.InitClient(client),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
organization, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
}
if len(inv.Args) < 1 {
return xerrors.Errorf("user_id or username is required as the first argument")
}
userIdentifier := inv.Args[0]
roles := inv.Args[1:]
member, err := client.UpdateOrganizationMemberRoles(ctx, organization.ID, userIdentifier, codersdk.UpdateRoles{
Roles: roles,
})
if err != nil {
return xerrors.Errorf("update member roles: %w", err)
}
updatedTo := make([]string, 0)
for _, role := range member.Roles {
updatedTo = append(updatedTo, role.String())
}
_, _ = fmt.Fprintf(inv.Stdout, "Member roles updated to [%s]\n", strings.Join(updatedTo, ", "))
return nil
},
}
return cmd
}
func (r *RootCmd) listOrganizationMembers() *serpent.Command {
formatter := cliui.NewOutputFormatter(
cliui.TableFormat([]codersdk.OrganizationMemberWithName{}, []string{"username", "organization_roles"}),
cliui.JSONFormat(),
)
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "list",
Short: "List all organization members",
Middleware: serpent.Chain(
serpent.RequireNArgs(0),
r.InitClient(client),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
organization, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
}
res, err := client.OrganizationMembers(ctx, organization.ID)
if err != nil {
return xerrors.Errorf("fetch members: %w", err)
}
out, err := formatter.Format(inv.Context(), res)
if err != nil {
return err
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}
+120
View File
@@ -0,0 +1,120 @@
package cli_test
import (
"bytes"
"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/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
)
func TestListOrganizationMembers(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, &coderdtest.Options{})
owner := coderdtest.CreateFirstUser(t, ownerClient)
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
ctx := testutil.Context(t, testutil.WaitMedium)
inv, root := clitest.New(t, "organization", "members", "list", "-c", "user_id,username,roles")
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.Contains(t, buf.String(), user.Username)
require.Contains(t, buf.String(), owner.UserID.String())
})
}
func TestAddOrganizationMembers(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, &coderdtest.Options{})
owner := coderdtest.CreateFirstUser(t, ownerClient)
_, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitMedium)
//nolint:gocritic // must be an owner, only owners can create orgs
otherOrg, err := ownerClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: "Other",
DisplayName: "",
Description: "",
Icon: "",
})
require.NoError(t, err, "create another organization")
inv, root := clitest.New(t, "organization", "members", "add", "--organization", otherOrg.ID.String(), user.Username)
//nolint:gocritic // must be an owner
clitest.SetupConfig(t, ownerClient, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
//nolint:gocritic // must be an owner
members, err := ownerClient.OrganizationMembers(ctx, otherOrg.ID)
require.NoError(t, err)
require.Len(t, members, 2)
})
}
func TestRemoveOrganizationMembers(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, &coderdtest.Options{})
owner := coderdtest.CreateFirstUser(t, ownerClient)
orgAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
_, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitMedium)
inv, root := clitest.New(t, "organization", "members", "remove", "--organization", owner.OrganizationID.String(), user.Username)
clitest.SetupConfig(t, orgAdminClient, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
members, err := orgAdminClient.OrganizationMembers(ctx, owner.OrganizationID)
require.NoError(t, err)
require.Len(t, members, 2)
})
t.Run("UserNotExists", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, &coderdtest.Options{})
owner := coderdtest.CreateFirstUser(t, ownerClient)
orgAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
ctx := testutil.Context(t, testutil.WaitMedium)
inv, root := clitest.New(t, "organization", "members", "remove", "--organization", owner.OrganizationID.String(), "random_name")
clitest.SetupConfig(t, orgAdminClient, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "must be an existing uuid or username")
})
}
+396
View File
@@ -0,0 +1,396 @@
package cli
import (
"encoding/json"
"fmt"
"io"
"slices"
"strings"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) organizationRoles() *serpent.Command {
cmd := &serpent.Command{
Use: "roles",
Short: "Manage organization roles.",
Aliases: []string{"role"},
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Hidden: true,
Children: []*serpent.Command{
r.showOrganizationRoles(),
r.editOrganizationRole(),
},
}
return cmd
}
func (r *RootCmd) showOrganizationRoles() *serpent.Command {
formatter := cliui.NewOutputFormatter(
cliui.ChangeFormatterData(
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}),
func(data any) (any, error) {
inputs, ok := data.([]codersdk.AssignableRoles)
if !ok {
return nil, xerrors.Errorf("expected []codersdk.AssignableRoles got %T", data)
}
tableRows := make([]roleTableRow, 0)
for _, input := range inputs {
tableRows = append(tableRows, roleToTableView(input.Role))
}
return tableRows, nil
},
),
cliui.JSONFormat(),
)
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "show [role_names ...]",
Short: "Show role(s)",
Middleware: serpent.Chain(
r.InitClient(client),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
org, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
}
roles, err := client.ListOrganizationRoles(ctx, org.ID)
if err != nil {
return xerrors.Errorf("listing roles: %w", err)
}
if len(inv.Args) > 0 {
// filter roles
filtered := make([]codersdk.AssignableRoles, 0)
for _, role := range roles {
if slices.ContainsFunc(inv.Args, func(s string) bool {
return strings.EqualFold(s, role.Name)
}) {
filtered = append(filtered, role)
}
}
roles = filtered
}
out, err := formatter.Format(inv.Context(), roles)
if err != nil {
return err
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}
func (r *RootCmd) editOrganizationRole() *serpent.Command {
formatter := cliui.NewOutputFormatter(
cliui.ChangeFormatterData(
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}),
func(data any) (any, error) {
typed, _ := data.(codersdk.Role)
return []roleTableRow{roleToTableView(typed)}, nil
},
),
cliui.JSONFormat(),
)
var (
dryRun bool
jsonInput bool
)
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "edit <role_name>",
Short: "Edit an organization custom role",
Long: FormatExamples(
Example{
Description: "Run with an input.json file",
Command: "coder roles edit --stdin < role.json",
},
),
Options: []serpent.Option{
cliui.SkipPromptOption(),
{
Name: "dry-run",
Description: "Does all the work, but does not submit the final updated role.",
Flag: "dry-run",
Value: serpent.BoolOf(&dryRun),
},
{
Name: "stdin",
Description: "Reads stdin for the json role definition to upload.",
Flag: "stdin",
Value: serpent.BoolOf(&jsonInput),
},
},
Middleware: serpent.Chain(
serpent.RequireRangeArgs(0, 1),
r.InitClient(client),
),
Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context()
org, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
}
var customRole codersdk.Role
if jsonInput {
// JSON Upload mode
bytes, err := io.ReadAll(inv.Stdin)
if err != nil {
return xerrors.Errorf("reading stdin: %w", err)
}
err = json.Unmarshal(bytes, &customRole)
if err != nil {
return xerrors.Errorf("parsing stdin json: %w", err)
}
if customRole.Name == "" {
arr := make([]json.RawMessage, 0)
err = json.Unmarshal(bytes, &arr)
if err == nil && len(arr) > 0 {
return xerrors.Errorf("the input appears to be an array, only 1 role can be sent at a time")
}
return xerrors.Errorf("json input does not appear to be a valid role")
}
} else {
if len(inv.Args) == 0 {
return xerrors.Errorf("missing role name argument, usage: \"coder organizations roles edit <role_name>\"")
}
interactiveRole, err := interactiveOrgRoleEdit(inv, org.ID, client)
if err != nil {
return xerrors.Errorf("editing role: %w", err)
}
customRole = *interactiveRole
preview := fmt.Sprintf("permissions: %d site, %d org, %d user",
len(customRole.SitePermissions), len(customRole.OrganizationPermissions), len(customRole.UserPermissions))
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "Are you sure you wish to update the role? " + preview,
Default: "yes",
IsConfirm: true,
})
if err != nil {
return xerrors.Errorf("abort: %w", err)
}
}
var updated codersdk.Role
if dryRun {
// Do not actually post
updated = customRole
} else {
updated, err = client.PatchOrganizationRole(ctx, org.ID, customRole)
if err != nil {
return xerrors.Errorf("patch role: %w", err)
}
}
output, err := formatter.Format(ctx, updated)
if err != nil {
return xerrors.Errorf("formatting: %w", err)
}
_, err = fmt.Fprintln(inv.Stdout, output)
return err
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}
func interactiveOrgRoleEdit(inv *serpent.Invocation, orgID uuid.UUID, client *codersdk.Client) (*codersdk.Role, error) {
ctx := inv.Context()
roles, err := client.ListOrganizationRoles(ctx, orgID)
if err != nil {
return nil, xerrors.Errorf("listing roles: %w", err)
}
// Make sure the role actually exists first
var originalRole codersdk.AssignableRoles
for _, r := range roles {
if strings.EqualFold(inv.Args[0], r.Name) {
originalRole = r
break
}
}
if originalRole.Name == "" {
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "No organization role exists with that name, do you want to create one?",
Default: "yes",
IsConfirm: true,
})
if err != nil {
return nil, xerrors.Errorf("abort: %w", err)
}
originalRole.Role = codersdk.Role{
Name: inv.Args[0],
OrganizationID: orgID.String(),
}
}
// Some checks since interactive mode is limited in what it currently sees
if len(originalRole.SitePermissions) > 0 {
return nil, xerrors.Errorf("unable to edit role in interactive mode, it contains site wide permissions")
}
if len(originalRole.UserPermissions) > 0 {
return nil, xerrors.Errorf("unable to edit role in interactive mode, it contains user permissions")
}
role := &originalRole.Role
allowedResources := []codersdk.RBACResource{
codersdk.ResourceTemplate,
codersdk.ResourceWorkspace,
codersdk.ResourceUser,
codersdk.ResourceGroup,
}
const done = "Finish and submit changes"
const abort = "Cancel changes"
// Now starts the role editing "game".
customRoleLoop:
for {
selected, err := cliui.Select(inv, cliui.SelectOptions{
Message: "Select which resources to edit permissions",
Options: append(permissionPreviews(role, allowedResources), done, abort),
})
if err != nil {
return role, xerrors.Errorf("selecting resource: %w", err)
}
switch selected {
case done:
break customRoleLoop
case abort:
return role, xerrors.Errorf("edit role %q aborted", role.Name)
default:
strs := strings.Split(selected, "::")
resource := strings.TrimSpace(strs[0])
actions, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
Message: fmt.Sprintf("Select actions to allow across the whole deployment for resources=%q", resource),
Options: slice.ToStrings(codersdk.RBACResourceActions[codersdk.RBACResource(resource)]),
Defaults: defaultActions(role, resource),
})
if err != nil {
return role, xerrors.Errorf("selecting actions for resource %q: %w", resource, err)
}
applyOrgResourceActions(role, resource, actions)
// back to resources!
}
}
// This println is required because the prompt ends us on the same line as some text.
_, _ = fmt.Println()
return role, nil
}
func applyOrgResourceActions(role *codersdk.Role, resource string, actions []string) {
if role.OrganizationPermissions == nil {
role.OrganizationPermissions = make([]codersdk.Permission, 0)
}
// Construct new site perms with only new perms for the resource
keep := make([]codersdk.Permission, 0)
for _, perm := range role.OrganizationPermissions {
perm := perm
if string(perm.ResourceType) != resource {
keep = append(keep, perm)
}
}
// Add new perms
for _, action := range actions {
keep = append(keep, codersdk.Permission{
Negate: false,
ResourceType: codersdk.RBACResource(resource),
Action: codersdk.RBACAction(action),
})
}
role.OrganizationPermissions = keep
}
func defaultActions(role *codersdk.Role, resource string) []string {
if role.OrganizationPermissions == nil {
role.OrganizationPermissions = []codersdk.Permission{}
}
defaults := make([]string, 0)
for _, perm := range role.OrganizationPermissions {
if string(perm.ResourceType) == resource {
defaults = append(defaults, string(perm.Action))
}
}
return defaults
}
func permissionPreviews(role *codersdk.Role, resources []codersdk.RBACResource) []string {
previews := make([]string, 0, len(resources))
for _, resource := range resources {
previews = append(previews, permissionPreview(role, resource))
}
return previews
}
func permissionPreview(role *codersdk.Role, resource codersdk.RBACResource) string {
if role.OrganizationPermissions == nil {
role.OrganizationPermissions = []codersdk.Permission{}
}
count := 0
for _, perm := range role.OrganizationPermissions {
if perm.ResourceType == resource {
count++
}
}
return fmt.Sprintf("%s :: %d permissions", resource, count)
}
func roleToTableView(role codersdk.Role) roleTableRow {
return roleTableRow{
Name: role.Name,
DisplayName: role.DisplayName,
OrganizationID: role.OrganizationID,
SitePermissions: fmt.Sprintf("%d permissions", len(role.SitePermissions)),
OrganizationPermissions: fmt.Sprintf("%d permissions", len(role.OrganizationPermissions)),
UserPermissions: fmt.Sprintf("%d permissions", len(role.UserPermissions)),
}
}
type roleTableRow struct {
Name string `table:"name,default_sort"`
DisplayName string `table:"display_name"`
OrganizationID string `table:"organization_id"`
SitePermissions string ` table:"site_permissions"`
// map[<org_id>] -> Permissions
OrganizationPermissions string `table:"organization_permissions"`
UserPermissions string `table:"user_permissions"`
}
+51
View File
@@ -0,0 +1,51 @@
package cli_test
import (
"bytes"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/testutil"
)
func TestShowOrganizationRoles(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{})
owner := coderdtest.CreateFirstUser(t, ownerClient)
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
const expectedRole = "test-role"
dbgen.CustomRole(t, db, database.CustomRole{
Name: expectedRole,
DisplayName: "Expected",
SitePermissions: nil,
OrgPermissions: nil,
UserPermissions: nil,
OrganizationID: uuid.NullUUID{
UUID: owner.OrganizationID,
Valid: true,
},
})
ctx := testutil.Context(t, testutil.WaitMedium)
inv, root := clitest.New(t, "organization", "roles", "show")
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.Contains(t, buf.String(), expectedRole)
})
}
+15 -2
View File
@@ -18,14 +18,16 @@ type workspaceParameterFlags struct {
promptBuildOptions bool
buildOptions []string
richParameterFile string
richParameters []string
richParameterFile string
richParameters []string
richParameterDefaults []string
promptRichParameters bool
}
func (wpf *workspaceParameterFlags) allOptions() []serpent.Option {
options := append(wpf.cliBuildOptions(), wpf.cliParameters()...)
options = append(options, wpf.cliParameterDefaults()...)
return append(options, wpf.alwaysPrompt())
}
@@ -62,6 +64,17 @@ func (wpf *workspaceParameterFlags) cliParameters() []serpent.Option {
}
}
func (wpf *workspaceParameterFlags) cliParameterDefaults() []serpent.Option {
return serpent.OptionSet{
serpent.Option{
Flag: "parameter-default",
Env: "CODER_RICH_PARAMETER_DEFAULT",
Description: `Rich parameter default values in the format "name=value".`,
Value: serpent.StringArrayOf(&wpf.richParameterDefaults),
},
}
}
func (wpf *workspaceParameterFlags) alwaysPrompt() serpent.Option {
return serpent.Option{
Flag: "always-prompt",
+15 -4
View File
@@ -26,9 +26,10 @@ type ParameterResolver struct {
lastBuildParameters []codersdk.WorkspaceBuildParameter
sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
richParameters []codersdk.WorkspaceBuildParameter
richParametersFile map[string]string
buildOptions []codersdk.WorkspaceBuildParameter
richParameters []codersdk.WorkspaceBuildParameter
richParametersDefaults map[string]string
richParametersFile map[string]string
buildOptions []codersdk.WorkspaceBuildParameter
promptRichParameters bool
promptBuildOptions bool
@@ -59,6 +60,16 @@ func (pr *ParameterResolver) WithRichParametersFile(fileMap map[string]string) *
return pr
}
func (pr *ParameterResolver) WithRichParametersDefaults(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
if pr.richParametersDefaults == nil {
pr.richParametersDefaults = make(map[string]string)
}
for _, p := range params {
pr.richParametersDefaults[p.Name] = p.Value
}
return pr
}
func (pr *ParameterResolver) WithPromptRichParameters(promptRichParameters bool) *ParameterResolver {
pr.promptRichParameters = promptRichParameters
return pr
@@ -227,7 +238,7 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
(tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
parameterValue, err := cliui.RichParameter(inv, tvp)
parameterValue, err := cliui.RichParameter(inv, tvp, pr.richParametersDefaults)
if err != nil {
return nil, err
}
+6 -8
View File
@@ -42,25 +42,23 @@ func (r *RootCmd) ping() *serpent.Command {
_, workspaceAgent, err := getWorkspaceAndAgent(
ctx, inv, client,
false, // Do not autostart for a ping.
codersdk.Me, workspaceName,
workspaceName,
)
if err != nil {
return err
}
logger := inv.Logger
opts := &workspacesdk.DialAgentOptions{}
if r.verbose {
logger = logger.AppendSinks(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
opts.Logger = inv.Logger.AppendSinks(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
}
if r.disableDirect {
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
opts.BlockEndpoints = true
}
conn, err := workspacesdk.New(client).
DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{
Logger: logger,
BlockEndpoints: r.disableDirect,
})
conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts)
if err != nil {
return err
}
+12 -13
View File
@@ -35,24 +35,24 @@ func (r *RootCmd) portForward() *serpent.Command {
Use: "port-forward <workspace>",
Short: `Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R".`,
Aliases: []string{"tunnel"},
Long: formatExamples(
example{
Long: FormatExamples(
Example{
Description: "Port forward a single TCP port from 1234 in the workspace to port 5678 on your local machine",
Command: "coder port-forward <workspace> --tcp 5678:1234",
},
example{
Example{
Description: "Port forward a single UDP port from port 9000 to port 9000 on your local machine",
Command: "coder port-forward <workspace> --udp 9000",
},
example{
Example{
Description: "Port forward multiple TCP ports and a UDP port",
Command: "coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53",
},
example{
Example{
Description: "Port forward multiple ports (TCP or UDP) in condensed syntax",
Command: "coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012",
},
example{
Example{
Description: "Port forward specifying the local address to bind to",
Command: "coder port-forward <workspace> --tcp 1.2.3.4:8080:8080",
},
@@ -73,7 +73,7 @@ func (r *RootCmd) portForward() *serpent.Command {
return xerrors.New("no port-forwards requested")
}
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, codersdk.Me, inv.Args[0])
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
if err != nil {
return err
}
@@ -95,19 +95,18 @@ func (r *RootCmd) portForward() *serpent.Command {
return xerrors.Errorf("await agent: %w", err)
}
opts := &workspacesdk.DialAgentOptions{}
logger := inv.Logger
if r.verbose {
logger = logger.AppendSinks(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
opts.Logger = logger.AppendSinks(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
}
if r.disableDirect {
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
opts.BlockEndpoints = true
}
conn, err := workspacesdk.New(client).
DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{
Logger: logger,
BlockEndpoints: r.disableDirect,
})
conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts)
if err != nil {
return err
}
+147
View File
@@ -0,0 +1,147 @@
package cli
import (
"fmt"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (RootCmd) promptExample() *serpent.Command {
promptCmd := func(use string, prompt func(inv *serpent.Invocation) error, options ...serpent.Option) *serpent.Command {
return &serpent.Command{
Use: use,
Options: options,
Handler: func(inv *serpent.Invocation) error {
return prompt(inv)
},
}
}
var useSearch bool
useSearchOption := serpent.Option{
Name: "search",
Description: "Show the search.",
Required: false,
Flag: "search",
Value: serpent.BoolOf(&useSearch),
}
cmd := &serpent.Command{
Use: "prompt-example",
Short: "Example of various prompt types used within coder cli.",
Long: "Example of various prompt types used within coder cli. " +
"This command exists to aid in adjusting visuals of command prompts.",
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*serpent.Command{
promptCmd("confirm", func(inv *serpent.Invocation) error {
value, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Basic confirmation prompt.",
Default: "yes",
IsConfirm: true,
})
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", value)
return err
}),
promptCmd("validation", func(inv *serpent.Invocation) error {
value, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Input a string that starts with a capital letter.",
Default: "",
Secret: false,
IsConfirm: false,
Validate: func(s string) error {
if len(s) == 0 {
return xerrors.Errorf("an input string is required")
}
if strings.ToUpper(string(s[0])) != string(s[0]) {
return xerrors.Errorf("input string must start with a capital letter")
}
return nil
},
})
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", value)
return err
}),
promptCmd("secret", func(inv *serpent.Invocation) error {
value, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Input a secret",
Default: "",
Secret: true,
IsConfirm: false,
Validate: func(s string) error {
if len(s) == 0 {
return xerrors.Errorf("an input string is required")
}
return nil
},
})
_, _ = fmt.Fprintf(inv.Stdout, "Your secret of length %d is safe with me\n", len(value))
return err
}),
promptCmd("select", func(inv *serpent.Invocation) error {
value, err := cliui.Select(inv, cliui.SelectOptions{
Options: []string{
"Blue", "Green", "Yellow", "Red", "Something else",
},
Default: "",
Message: "Select your favorite color:",
Size: 5,
HideSearch: !useSearch,
})
if value == "Something else" {
_, _ = fmt.Fprint(inv.Stdout, "I would have picked blue.\n")
} else {
_, _ = fmt.Fprintf(inv.Stdout, "%s is a nice color.\n", value)
}
return err
}, useSearchOption),
promptCmd("multi-select", func(inv *serpent.Invocation) error {
values, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
Message: "Select some things:",
Options: []string{
"Code", "Chair", "Whale", "Diamond", "Carrot",
},
Defaults: []string{"Code"},
})
_, _ = fmt.Fprintf(inv.Stdout, "%q are nice choices.\n", strings.Join(values, ", "))
return err
}),
promptCmd("rich-parameter", func(inv *serpent.Invocation) error {
value, err := cliui.RichSelect(inv, cliui.RichSelectOptions{
Options: []codersdk.TemplateVersionParameterOption{
{
Name: "Blue",
Description: "Like the ocean.",
Value: "blue",
Icon: "/logo/blue.png",
},
{
Name: "Red",
Description: "Like a clown's nose.",
Value: "red",
Icon: "/logo/red.png",
},
{
Name: "Yellow",
Description: "Like a bumblebee. ",
Value: "yellow",
Icon: "/logo/yellow.png",
},
},
Default: "blue",
Size: 5,
HideSearch: useSearch,
})
_, _ = fmt.Fprintf(inv.Stdout, "%s is a good choice.\n", value.Name)
return err
}, useSearchOption),
},
}
return cmd
}
+9 -9
View File
@@ -181,12 +181,12 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err
`
cmd := &serpent.Command{
Use: "coder [global-flags] <subcommand>",
Long: fmt.Sprintf(fmtLong, buildinfo.Version()) + formatExamples(
example{
Long: fmt.Sprintf(fmtLong, buildinfo.Version()) + FormatExamples(
Example{
Description: "Start a Coder server",
Command: "coder server",
},
example{
Example{
Description: "Get started by creating a template from an example",
Command: "coder templates init",
},
@@ -657,7 +657,7 @@ func CurrentOrganization(r *RootCmd, inv *serpent.Invocation, client *codersdk.C
})
if index < 0 {
return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization?", selected)
return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization? If unsure, run 'coder organizations set \"\" ' to reset your current context.", selected)
}
return orgs[index], nil
}
@@ -753,16 +753,16 @@ func isTTYWriter(inv *serpent.Invocation, writer io.Writer) bool {
return isatty.IsTerminal(file.Fd())
}
// example represents a standard example for command usage, to be used
// with formatExamples.
type example struct {
// Example represents a standard example for command usage, to be used
// with FormatExamples.
type Example struct {
Description string
Command string
}
// formatExamples formats the examples as width wrapped bulletpoint
// FormatExamples formats the examples as width wrapped bulletpoint
// descriptions with the command underneath.
func formatExamples(examples ...example) string {
func FormatExamples(examples ...Example) string {
var sb strings.Builder
padStyle := cliui.DefaultStyles.Wrap.With(pretty.XPad(4, 0))
+4 -4
View File
@@ -45,7 +45,7 @@ func Test_formatExamples(t *testing.T) {
tests := []struct {
name string
examples []example
examples []Example
wantMatches []string
}{
{
@@ -55,7 +55,7 @@ func Test_formatExamples(t *testing.T) {
},
{
name: "Output examples",
examples: []example{
examples: []Example{
{
Description: "Hello world.",
Command: "echo hello",
@@ -72,7 +72,7 @@ func Test_formatExamples(t *testing.T) {
},
{
name: "No description outputs commands",
examples: []example{
examples: []Example{
{
Command: "echo hello",
},
@@ -87,7 +87,7 @@ func Test_formatExamples(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := formatExamples(tt.examples...)
got := FormatExamples(tt.examples...)
if len(tt.wantMatches) == 0 {
require.Empty(t, got)
} else {
+6 -6
View File
@@ -140,8 +140,8 @@ func (r *RootCmd) scheduleStart() *serpent.Command {
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "start <workspace-name> { <start-time> [day-of-week] [location] | manual }",
Long: scheduleStartDescriptionLong + "\n" + formatExamples(
example{
Long: scheduleStartDescriptionLong + "\n" + FormatExamples(
Example{
Description: "Set the workspace to start at 9:30am (in Dublin) from Monday to Friday",
Command: "coder schedule start my-workspace 9:30AM Mon-Fri Europe/Dublin",
},
@@ -189,8 +189,8 @@ func (r *RootCmd) scheduleStop() *serpent.Command {
client := new(codersdk.Client)
return &serpent.Command{
Use: "stop <workspace-name> { <duration> | manual }",
Long: scheduleStopDescriptionLong + "\n" + formatExamples(
example{
Long: scheduleStopDescriptionLong + "\n" + FormatExamples(
Example{
Command: "coder schedule stop my-workspace 2h30m",
},
),
@@ -234,8 +234,8 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
overrideCmd := &serpent.Command{
Use: "override-stop <workspace-name> <duration from now>",
Short: "Override the stop time of a currently running workspace instance.",
Long: scheduleOverrideDescriptionLong + "\n" + formatExamples(
example{
Long: scheduleOverrideDescriptionLong + "\n" + FormatExamples(
Example{
Command: "coder schedule override-stop my-workspace 90m",
},
),
+98 -94
View File
@@ -62,7 +62,6 @@ import (
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/autobuild"
"github.com/coder/coder/v2/coderd/batchstats"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/awsiamrds"
"github.com/coder/coder/v2/coderd/database/dbmem"
@@ -87,7 +86,7 @@ import (
stringutil "github.com/coder/coder/v2/coderd/util/strings"
"github.com/coder/coder/v2/coderd/workspaceapps"
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
"github.com/coder/coder/v2/coderd/workspaceusage"
"github.com/coder/coder/v2/coderd/workspacestats"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/drpc"
"github.com/coder/coder/v2/cryptorand"
@@ -169,6 +168,7 @@ func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*co
EmailDomain: vals.OIDC.EmailDomain,
AllowSignups: vals.OIDC.AllowSignups.Value(),
UsernameField: vals.OIDC.UsernameField.String(),
NameField: vals.OIDC.NameField.String(),
EmailField: vals.OIDC.EmailField.String(),
AuthURLParams: vals.OIDC.AuthURLParams.Value,
IgnoreUserInfo: vals.OIDC.IgnoreUserInfo.Value(),
@@ -796,31 +796,18 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
cliui.Infof(inv.Stdout, "\n==> Logs will stream in below (press ctrl+c to gracefully exit):")
if vals.Telemetry.Enable {
gitAuth := make([]telemetry.GitAuth, 0)
// TODO:
var gitAuthConfigs []codersdk.ExternalAuthConfig
for _, cfg := range gitAuthConfigs {
gitAuth = append(gitAuth, telemetry.GitAuth{
Type: cfg.Type,
})
vals, err := vals.WithoutSecrets()
if err != nil {
return xerrors.Errorf("remove secrets from deployment values: %w", err)
}
options.Telemetry, err = telemetry.New(telemetry.Options{
BuiltinPostgres: builtinPostgres,
DeploymentID: deploymentID,
Database: options.Database,
Logger: logger.Named("telemetry"),
URL: vals.Telemetry.URL.Value(),
Wildcard: vals.WildcardAccessURL.String() != "",
DERPServerRelayURL: vals.DERP.Server.RelayURL.String(),
GitAuth: gitAuth,
GitHubOAuth: vals.OAuth2.Github.ClientID != "",
OIDCAuth: vals.OIDC.ClientID != "",
OIDCIssuerURL: vals.OIDC.IssuerURL.String(),
Prometheus: vals.Prometheus.Enable.Value(),
STUN: len(vals.DERP.Server.STUNAddresses) != 0,
Tunnel: tunnel != nil,
Experiments: vals.Experiments.Value(),
BuiltinPostgres: builtinPostgres,
DeploymentID: deploymentID,
Database: options.Database,
Logger: logger.Named("telemetry"),
URL: vals.Telemetry.URL.Value(),
Tunnel: tunnel != nil,
DeploymentConfig: vals,
ParseLicenseJWT: func(lic *telemetry.License) error {
// This will be nil when running in AGPL-only mode.
if options.ParseLicenseClaims == nil {
@@ -869,9 +856,9 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
options.SwaggerEndpoint = vals.Swagger.Enable.Value()
}
batcher, closeBatcher, err := batchstats.New(ctx,
batchstats.WithLogger(options.Logger.Named("batchstats")),
batchstats.WithStore(options.Database),
batcher, closeBatcher, err := workspacestats.NewBatcher(ctx,
workspacestats.BatcherWithLogger(options.Logger.Named("batchstats")),
workspacestats.BatcherWithStore(options.Database),
)
if err != nil {
return xerrors.Errorf("failed to create agent stats batcher: %w", err)
@@ -944,6 +931,13 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
var provisionerdWaitGroup sync.WaitGroup
defer provisionerdWaitGroup.Wait()
provisionerdMetrics := provisionerd.NewMetrics(options.PrometheusRegistry)
// Built in provisioner daemons will support the same types.
// By default, this is the slice {"terraform"}
provisionerTypes := make([]codersdk.ProvisionerType, 0)
for _, pt := range vals.Provisioner.DaemonTypes {
provisionerTypes = append(provisionerTypes, codersdk.ProvisionerType(pt))
}
for i := int64(0); i < vals.Provisioner.Daemons.Value(); i++ {
suffix := fmt.Sprintf("%d", i)
// The suffix is added to the hostname, so we may need to trim to fit into
@@ -952,7 +946,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
name := fmt.Sprintf("%s-%s", hostname, suffix)
daemonCacheDir := filepath.Join(cacheDir, fmt.Sprintf("provisioner-%d", i))
daemon, err := newProvisionerDaemon(
ctx, coderAPI, provisionerdMetrics, logger, vals, daemonCacheDir, errCh, &provisionerdWaitGroup, name,
ctx, coderAPI, provisionerdMetrics, logger, vals, daemonCacheDir, errCh, &provisionerdWaitGroup, name, provisionerTypes,
)
if err != nil {
return xerrors.Errorf("create provisioner daemon: %w", err)
@@ -965,12 +959,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
defer shutdownConns()
// Ensures that old database entries are cleaned up over time!
purger := dbpurge.New(ctx, logger, options.Database)
purger := dbpurge.New(ctx, logger.Named("dbpurge"), options.Database)
defer purger.Close()
// Updates workspace usage
tracker := workspaceusage.New(options.Database,
workspaceusage.WithLogger(logger.Named("workspace_usage_tracker")),
tracker := workspacestats.NewTracker(options.Database,
workspacestats.TrackerWithLogger(logger.Named("workspace_usage_tracker")),
)
options.WorkspaceUsageTracker = tracker
defer tracker.Close()
@@ -1340,6 +1334,7 @@ func newProvisionerDaemon(
errCh chan error,
wg *sync.WaitGroup,
name string,
provisionerTypes []codersdk.ProvisionerType,
) (srv *provisionerd.Server, err error) {
ctx, cancel := context.WithCancel(ctx)
defer func() {
@@ -1359,79 +1354,88 @@ func newProvisionerDaemon(
return nil, xerrors.Errorf("mkdir work dir: %w", err)
}
// Omit any duplicates
provisionerTypes = slice.Unique(provisionerTypes)
// Populate the connector with the supported types.
connector := provisionerd.LocalProvisioners{}
if cfg.Provisioner.DaemonsEcho {
echoClient, echoServer := drpc.MemTransportPipe()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
_ = echoClient.Close()
_ = echoServer.Close()
}()
wg.Add(1)
go func() {
defer wg.Done()
defer cancel()
for _, provisionerType := range provisionerTypes {
switch provisionerType {
case codersdk.ProvisionerTypeEcho:
echoClient, echoServer := drpc.MemTransportPipe()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
_ = echoClient.Close()
_ = echoServer.Close()
}()
wg.Add(1)
go func() {
defer wg.Done()
defer cancel()
err := echo.Serve(ctx, &provisionersdk.ServeOptions{
Listener: echoServer,
WorkDirectory: workDir,
Logger: logger.Named("echo"),
})
if err != nil {
select {
case errCh <- err:
default:
}
}
}()
connector[string(database.ProvisionerTypeEcho)] = sdkproto.NewDRPCProvisionerClient(echoClient)
} else {
tfDir := filepath.Join(cacheDir, "tf")
err = os.MkdirAll(tfDir, 0o700)
if err != nil {
return nil, xerrors.Errorf("mkdir terraform dir: %w", err)
}
tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName)
terraformClient, terraformServer := drpc.MemTransportPipe()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
_ = terraformClient.Close()
_ = terraformServer.Close()
}()
wg.Add(1)
go func() {
defer wg.Done()
defer cancel()
err := terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
Listener: terraformServer,
Logger: logger.Named("terraform"),
err := echo.Serve(ctx, &provisionersdk.ServeOptions{
Listener: echoServer,
WorkDirectory: workDir,
},
CachePath: tfDir,
Tracer: tracer,
})
if err != nil && !xerrors.Is(err, context.Canceled) {
select {
case errCh <- err:
default:
Logger: logger.Named("echo"),
})
if err != nil {
select {
case errCh <- err:
default:
}
}
}()
connector[string(database.ProvisionerTypeEcho)] = sdkproto.NewDRPCProvisionerClient(echoClient)
case codersdk.ProvisionerTypeTerraform:
tfDir := filepath.Join(cacheDir, "tf")
err = os.MkdirAll(tfDir, 0o700)
if err != nil {
return nil, xerrors.Errorf("mkdir terraform dir: %w", err)
}
}()
connector[string(database.ProvisionerTypeTerraform)] = sdkproto.NewDRPCProvisionerClient(terraformClient)
tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName)
terraformClient, terraformServer := drpc.MemTransportPipe()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
_ = terraformClient.Close()
_ = terraformServer.Close()
}()
wg.Add(1)
go func() {
defer wg.Done()
defer cancel()
err := terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
Listener: terraformServer,
Logger: logger.Named("terraform"),
WorkDirectory: workDir,
},
CachePath: tfDir,
Tracer: tracer,
})
if err != nil && !xerrors.Is(err, context.Canceled) {
select {
case errCh <- err:
default:
}
}
}()
connector[string(database.ProvisionerTypeTerraform)] = sdkproto.NewDRPCProvisionerClient(terraformClient)
default:
return nil, xerrors.Errorf("unknown provisioner type %q", provisionerType)
}
}
return provisionerd.New(func(dialCtx context.Context) (proto.DRPCProvisionerDaemonClient, error) {
// This debounces calls to listen every second. Read the comment
// in provisionerdserver.go to learn more!
return coderAPI.CreateInMemoryProvisionerDaemon(dialCtx, name)
return coderAPI.CreateInMemoryProvisionerDaemon(dialCtx, name, provisionerTypes)
}, &provisionerd.Options{
Logger: logger.Named(fmt.Sprintf("provisionerd-%s", name)),
UpdateInterval: time.Second,
+5 -2
View File
@@ -85,6 +85,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
// Use the validator tags so we match the API's validation.
req := codersdk.CreateUserRequest{
Username: "username",
Name: "Admin User",
Email: "email@coder.com",
Password: "ValidPa$$word123!",
OrganizationID: uuid.New(),
@@ -116,6 +117,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
return err
}
}
if newUserEmail == "" {
newUserEmail, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "Email",
@@ -189,10 +191,11 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
ID: uuid.New(),
Email: newUserEmail,
Username: newUserUsername,
Name: "Admin User",
HashedPassword: []byte(hashedPassword),
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
RBACRoles: []string{rbac.RoleOwner()},
RBACRoles: []string{rbac.RoleOwner().String()},
LoginType: database.LoginTypePassword,
})
if err != nil {
@@ -222,7 +225,7 @@ func (r *RootCmd) newCreateAdminUserCommand() *serpent.Command {
UserID: newUser.ID,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
Roles: []string{rbac.RoleOrgAdmin(org.ID)},
Roles: []string{rbac.RoleOrgAdmin()},
})
if err != nil {
return xerrors.Errorf("insert organization member: %w", err)
+5 -4
View File
@@ -17,6 +17,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/userpassword"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
)
@@ -56,7 +57,7 @@ func TestServerCreateAdminUser(t *testing.T) {
require.NoError(t, err)
require.True(t, ok, "password does not match")
require.EqualValues(t, []string{rbac.RoleOwner()}, user.RBACRoles, "user does not have owner role")
require.EqualValues(t, []string{codersdk.RoleOwner}, user.RBACRoles, "user does not have owner role")
// Check that user is admin in every org.
orgs, err := db.GetOrganizations(ctx)
@@ -66,12 +67,12 @@ func TestServerCreateAdminUser(t *testing.T) {
orgIDs[org.ID] = struct{}{}
}
orgMemberships, err := db.GetOrganizationMembershipsByUserID(ctx, user.ID)
orgMemberships, err := db.OrganizationMembers(ctx, database.OrganizationMembersParams{UserID: user.ID})
require.NoError(t, err)
orgIDs2 := make(map[uuid.UUID]struct{}, len(orgMemberships))
for _, membership := range orgMemberships {
orgIDs2[membership.OrganizationID] = struct{}{}
assert.Equal(t, []string{rbac.RoleOrgAdmin(membership.OrganizationID)}, membership.Roles, "user is not org admin")
orgIDs2[membership.OrganizationMember.OrganizationID] = struct{}{}
assert.Equal(t, []string{rbac.RoleOrgAdmin()}, membership.OrganizationMember.Roles, "user is not org admin")
}
require.Equal(t, orgIDs, orgIDs2, "user is not in all orgs")
+2 -2
View File
@@ -141,8 +141,8 @@ func Test_configureCipherSuites(t *testing.T) {
name: "TLSUnsupported",
minTLS: tls.VersionTLS10,
maxTLS: tls.VersionTLS13,
// TLS_RSA_WITH_AES_128_GCM_SHA256 only supports tls 1.2
inputCiphers: []string{"TLS_RSA_WITH_AES_128_GCM_SHA256"},
// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 only supports tls 1.2
inputCiphers: []string{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"},
wantErr: "no tls ciphers supported for tls versions",
},
{
+52 -36
View File
@@ -967,26 +967,32 @@ func TestServer(t *testing.T) {
assert.NoError(t, err)
// nolint:bodyclose
res, err = http.DefaultClient.Do(req)
return err == nil
}, testutil.WaitShort, testutil.IntervalFast)
defer res.Body.Close()
if err != nil {
return false
}
defer res.Body.Close()
scanner := bufio.NewScanner(res.Body)
hasActiveUsers := false
for scanner.Scan() {
// This metric is manually registered to be tracked in the server. That's
// why we test it's tracked here.
if strings.HasPrefix(scanner.Text(), "coderd_api_active_users_duration_hour") {
hasActiveUsers = true
continue
scanner := bufio.NewScanner(res.Body)
hasActiveUsers := false
for scanner.Scan() {
// This metric is manually registered to be tracked in the server. That's
// why we test it's tracked here.
if strings.HasPrefix(scanner.Text(), "coderd_api_active_users_duration_hour") {
hasActiveUsers = true
continue
}
if strings.HasPrefix(scanner.Text(), "coderd_db_query_latencies_seconds") {
t.Fatal("db metrics should not be tracked when --prometheus-collect-db-metrics is not enabled")
}
t.Logf("scanned %s", scanner.Text())
}
if strings.HasPrefix(scanner.Text(), "coderd_db_query_latencies_seconds") {
t.Fatal("db metrics should not be tracked when --prometheus-collect-db-metrics is not enabled")
if scanner.Err() != nil {
t.Logf("scanner err: %s", scanner.Err().Error())
return false
}
t.Logf("scanned %s", scanner.Text())
}
require.NoError(t, scanner.Err())
require.True(t, hasActiveUsers)
return hasActiveUsers
}, testutil.WaitShort, testutil.IntervalFast, "didn't find coderd_api_active_users_duration_hour in time")
})
t.Run("DBMetricsEnabled", func(t *testing.T) {
@@ -1017,20 +1023,25 @@ func TestServer(t *testing.T) {
assert.NoError(t, err)
// nolint:bodyclose
res, err = http.DefaultClient.Do(req)
return err == nil
}, testutil.WaitShort, testutil.IntervalFast)
defer res.Body.Close()
scanner := bufio.NewScanner(res.Body)
hasDBMetrics := false
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "coderd_db_query_latencies_seconds") {
hasDBMetrics = true
if err != nil {
return false
}
t.Logf("scanned %s", scanner.Text())
}
require.NoError(t, scanner.Err())
require.True(t, hasDBMetrics)
defer res.Body.Close()
scanner := bufio.NewScanner(res.Body)
hasDBMetrics := false
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "coderd_db_query_latencies_seconds") {
hasDBMetrics = true
}
t.Logf("scanned %s", scanner.Text())
}
if scanner.Err() != nil {
t.Logf("scanner err: %s", scanner.Err().Error())
return false
}
return hasDBMetrics
}, testutil.WaitShort, testutil.IntervalFast, "didn't find coderd_db_query_latencies_seconds in time")
})
})
t.Run("GitHubOAuth", func(t *testing.T) {
@@ -1347,7 +1358,7 @@ func TestServer(t *testing.T) {
}
return lastStat.Size() > 0
},
testutil.WaitShort,
dur, //nolint:gocritic
testutil.IntervalFast,
"file at %s should exist, last stat: %+v",
fiName, lastStat,
@@ -1367,7 +1378,8 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
"--provisioner-daemons-echo",
"--provisioner-daemons=3",
"--provisioner-types=echo",
"--log-human", fiName,
)
clitest.Start(t, root)
@@ -1385,7 +1397,8 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
"--provisioner-daemons-echo",
"--provisioner-daemons=3",
"--provisioner-types=echo",
"--log-human", fi,
)
clitest.Start(t, root)
@@ -1403,7 +1416,8 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
"--provisioner-daemons-echo",
"--provisioner-daemons=3",
"--provisioner-types=echo",
"--log-json", fi,
)
clitest.Start(t, root)
@@ -1424,7 +1438,8 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
"--provisioner-daemons-echo",
"--provisioner-daemons=3",
"--provisioner-types=echo",
"--log-stackdriver", fi,
)
// Attach pty so we get debug output from the command if this test
@@ -1459,7 +1474,8 @@ func TestServer(t *testing.T) {
"--in-memory",
"--http-address", ":0",
"--access-url", "http://example.com",
"--provisioner-daemons-echo",
"--provisioner-daemons=3",
"--provisioner-types=echo",
"--log-human", fi1,
"--log-json", fi2,
"--log-stackdriver", fi3,
+81 -33
View File
@@ -6,7 +6,6 @@ import (
"os"
"time"
"github.com/jedib0t/go-pretty/v6/table"
"golang.org/x/xerrors"
tsspeedtest "tailscale.com/net/speedtest"
"tailscale.com/wgengine/capture"
@@ -19,12 +18,51 @@ import (
"github.com/coder/serpent"
)
type SpeedtestResult struct {
Overall SpeedtestResultInterval `json:"overall"`
Intervals []SpeedtestResultInterval `json:"intervals"`
}
type SpeedtestResultInterval struct {
StartTimeSeconds float64 `json:"start_time_seconds"`
EndTimeSeconds float64 `json:"end_time_seconds"`
ThroughputMbits float64 `json:"throughput_mbits"`
}
type speedtestTableItem struct {
Interval string `table:"Interval,nosort"`
Throughput string `table:"Throughput"`
}
func (r *RootCmd) speedtest() *serpent.Command {
var (
direct bool
duration time.Duration
direction string
pcapFile string
formatter = cliui.NewOutputFormatter(
cliui.ChangeFormatterData(cliui.TableFormat([]speedtestTableItem{}, []string{"Interval", "Throughput"}), func(data any) (any, error) {
res, ok := data.(SpeedtestResult)
if !ok {
// This should never happen
return "", xerrors.Errorf("expected speedtestResult, got %T", data)
}
tableRows := make([]any, len(res.Intervals)+2)
for i, r := range res.Intervals {
tableRows[i] = speedtestTableItem{
Interval: fmt.Sprintf("%.2f-%.2f sec", r.StartTimeSeconds, r.EndTimeSeconds),
Throughput: fmt.Sprintf("%.4f Mbits/sec", r.ThroughputMbits),
}
}
tableRows[len(res.Intervals)] = cliui.TableSeparator{}
tableRows[len(res.Intervals)+1] = speedtestTableItem{
Interval: fmt.Sprintf("%.2f-%.2f sec", res.Overall.StartTimeSeconds, res.Overall.EndTimeSeconds),
Throughput: fmt.Sprintf("%.4f Mbits/sec", res.Overall.ThroughputMbits),
}
return tableRows, nil
}),
cliui.JSONFormat(),
)
)
client := new(codersdk.Client)
cmd := &serpent.Command{
@@ -39,7 +77,11 @@ func (r *RootCmd) speedtest() *serpent.Command {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, false, codersdk.Me, inv.Args[0])
if direct && r.disableDirect {
return xerrors.Errorf("--direct (-d) is incompatible with --%s", varDisableDirect)
}
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, false, inv.Args[0])
if err != nil {
return err
}
@@ -52,18 +94,27 @@ func (r *RootCmd) speedtest() *serpent.Command {
return xerrors.Errorf("await agent: %w", err)
}
logger := inv.Logger.AppendSinks(sloghuman.Sink(inv.Stderr))
opts := &workspacesdk.DialAgentOptions{}
if r.verbose {
logger = logger.Leveled(slog.LevelDebug)
opts.Logger = inv.Logger.AppendSinks(sloghuman.Sink(inv.Stderr)).Leveled(slog.LevelDebug)
}
if r.disableDirect {
_, _ = fmt.Fprintln(inv.Stderr, "Direct connections disabled.")
opts.BlockEndpoints = true
}
if pcapFile != "" {
s := capture.New()
opts.CaptureHook = s.LogPacket
f, err := os.OpenFile(pcapFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return err
}
defer f.Close()
unregister := s.RegisterOutput(f)
defer unregister()
}
conn, err := workspacesdk.New(client).
DialAgent(ctx, workspaceAgent.ID, &workspacesdk.DialAgentOptions{
Logger: logger,
})
DialAgent(ctx, workspaceAgent.ID, opts)
if err != nil {
return err
}
@@ -88,32 +139,20 @@ func (r *RootCmd) speedtest() *serpent.Command {
}
peer := status.Peer[status.Peers()[0]]
if !p2p && direct {
cliui.Infof(inv.Stdout, "Waiting for a direct connection... (%dms via %s)", dur.Milliseconds(), peer.Relay)
cliui.Infof(inv.Stderr, "Waiting for a direct connection... (%dms via %s)", dur.Milliseconds(), peer.Relay)
continue
}
via := peer.Relay
if via == "" {
via = "direct"
}
cliui.Infof(inv.Stdout, "%dms via %s", dur.Milliseconds(), via)
cliui.Infof(inv.Stderr, "%dms via %s", dur.Milliseconds(), via)
break
}
} else {
conn.AwaitReachable(ctx)
}
if pcapFile != "" {
s := capture.New()
conn.InstallCaptureHook(s.LogPacket)
f, err := os.OpenFile(pcapFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return err
}
defer f.Close()
unregister := s.RegisterOutput(f)
defer unregister()
}
var tsDir tsspeedtest.Direction
switch direction {
case "up":
@@ -123,24 +162,32 @@ func (r *RootCmd) speedtest() *serpent.Command {
default:
return xerrors.Errorf("invalid direction: %q", direction)
}
cliui.Infof(inv.Stdout, "Starting a %ds %s test...", int(duration.Seconds()), tsDir)
cliui.Infof(inv.Stderr, "Starting a %ds %s test...", int(duration.Seconds()), tsDir)
results, err := conn.Speedtest(ctx, tsDir, duration)
if err != nil {
return err
}
tableWriter := cliui.Table()
tableWriter.AppendHeader(table.Row{"Interval", "Throughput"})
var outputResult SpeedtestResult
startTime := results[0].IntervalStart
for _, r := range results {
if r.Total {
tableWriter.AppendSeparator()
outputResult.Intervals = make([]SpeedtestResultInterval, len(results)-1)
for i, r := range results {
interval := SpeedtestResultInterval{
StartTimeSeconds: r.IntervalStart.Sub(startTime).Seconds(),
EndTimeSeconds: r.IntervalEnd.Sub(startTime).Seconds(),
ThroughputMbits: r.MBitsPerSecond(),
}
if r.Total {
interval.StartTimeSeconds = 0
outputResult.Overall = interval
} else {
outputResult.Intervals[i] = interval
}
tableWriter.AppendRow(table.Row{
fmt.Sprintf("%.2f-%.2f sec", r.IntervalStart.Sub(startTime).Seconds(), r.IntervalEnd.Sub(startTime).Seconds()),
fmt.Sprintf("%.4f Mbits/sec", r.MBitsPerSecond()),
})
}
_, err = fmt.Fprintln(inv.Stdout, tableWriter.Render())
out, err := formatter.Format(inv.Context(), outputResult)
if err != nil {
return err
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
}
@@ -172,5 +219,6 @@ func (r *RootCmd) speedtest() *serpent.Command {
Value: serpent.StringOf(&pcapFile),
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}
+45
View File
@@ -1,7 +1,9 @@
package cli_test
import (
"bytes"
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
@@ -10,6 +12,7 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/cli"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
@@ -56,3 +59,45 @@ func TestSpeedtest(t *testing.T) {
})
<-cmdDone
}
func TestSpeedtestJson(t *testing.T) {
t.Parallel()
t.Skip("Potentially flaky test - see https://github.com/coder/coder/issues/6321")
if testing.Short() {
t.Skip("This test takes a minimum of 5ms per a hardcoded value in Tailscale!")
}
client, workspace, agentToken := setupWorkspaceForAgent(t)
_ = agenttest.New(t, client.URL, agentToken)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
require.Eventually(t, func() bool {
ws, err := client.Workspace(ctx, workspace.ID)
if !assert.NoError(t, err) {
return false
}
a := ws.LatestBuild.Resources[0].Agents[0]
return a.Status == codersdk.WorkspaceAgentConnected &&
a.LifecycleState == codersdk.WorkspaceAgentLifecycleReady
}, testutil.WaitLong, testutil.IntervalFast, "agent is not ready")
inv, root := clitest.New(t, "speedtest", "--output=json", workspace.Name)
clitest.SetupConfig(t, client, root)
out := bytes.NewBuffer(nil)
inv.Stdout = out
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
inv.Logger = slogtest.Make(t, nil).Named("speedtest").Leveled(slog.LevelDebug)
cmdDone := tGo(t, func() {
err := inv.WithContext(ctx).Run()
assert.NoError(t, err)
})
<-cmdDone
var result cli.SpeedtestResult
require.NoError(t, json.Unmarshal(out.Bytes(), &result))
require.Len(t, result.Intervals, 5)
}
+82 -15
View File
@@ -6,11 +6,13 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"sync"
"time"
@@ -39,6 +41,10 @@ import (
"github.com/coder/serpent"
)
const (
disableUsageApp = "disable"
)
var (
workspacePollInterval = time.Minute
autostopNotifyCountdown = []time.Duration{30 * time.Minute}
@@ -55,6 +61,8 @@ func (r *RootCmd) ssh() *serpent.Command {
noWait bool
logDirPath string
remoteForwards []string
env []string
usageApp string
disableAutostart bool
)
client := new(codersdk.Client)
@@ -78,6 +86,10 @@ func (r *RootCmd) ssh() *serpent.Command {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Prevent unnecessary logs from the stdlib from messing up the TTY.
// See: https://github.com/coder/coder/issues/13144
log.SetOutput(io.Discard)
logger := inv.Logger
defer func() {
if retErr != nil {
@@ -144,19 +156,26 @@ func (r *RootCmd) ssh() *serpent.Command {
stack := newCloserStack(ctx, logger)
defer stack.close(nil)
if len(remoteForwards) > 0 {
for _, remoteForward := range remoteForwards {
isValid := validateRemoteForward(remoteForward)
if !isValid {
return xerrors.Errorf(`invalid format of remote-forward, expected: remote_port:local_address:local_port`)
}
if isValid && stdio {
return xerrors.Errorf(`remote-forward can't be enabled in the stdio mode`)
}
for _, remoteForward := range remoteForwards {
isValid := validateRemoteForward(remoteForward)
if !isValid {
return xerrors.Errorf(`invalid format of remote-forward, expected: remote_port:local_address:local_port`)
}
if isValid && stdio {
return xerrors.Errorf(`remote-forward can't be enabled in the stdio mode`)
}
}
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, codersdk.Me, inv.Args[0])
var parsedEnv [][2]string
for _, e := range env {
k, v, ok := strings.Cut(e, "=")
if !ok {
return xerrors.Errorf("invalid environment variable setting %q", e)
}
parsedEnv = append(parsedEnv, [2]string{k, v})
}
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
if err != nil {
return err
}
@@ -238,6 +257,15 @@ func (r *RootCmd) ssh() *serpent.Command {
stopPolling := tryPollWorkspaceAutostop(ctx, client, workspace)
defer stopPolling()
usageAppName := getUsageAppName(usageApp)
if usageAppName != "" {
closeUsage := client.UpdateWorkspaceUsageWithBodyContext(ctx, workspace.ID, codersdk.PostWorkspaceUsageRequest{
AgentID: workspaceAgent.ID,
AppName: usageAppName,
})
defer closeUsage()
}
if stdio {
rawSSH, err := conn.SSH(ctx)
if err != nil {
@@ -375,6 +403,12 @@ func (r *RootCmd) ssh() *serpent.Command {
}()
}
for _, kv := range parsedEnv {
if err := sshSession.Setenv(kv[0], kv[1]); err != nil {
return xerrors.Errorf("setenv: %w", err)
}
}
err = sshSession.RequestPty("xterm-256color", 128, 128, gossh.TerminalModes{})
if err != nil {
return xerrors.Errorf("request pty: %w", err)
@@ -483,6 +517,20 @@ func (r *RootCmd) ssh() *serpent.Command {
FlagShorthand: "R",
Value: serpent.StringArrayOf(&remoteForwards),
},
{
Flag: "env",
Description: "Set environment variable(s) for session (key1=value1,key2=value2,...).",
Env: "CODER_SSH_ENV",
FlagShorthand: "e",
Value: serpent.StringArrayOf(&env),
},
{
Flag: "usage-app",
Description: "Specifies the usage app to use for workspace activity tracking.",
Env: "CODER_SSH_USAGE_APP",
Value: serpent.StringOf(&usageApp),
Hidden: true,
},
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)),
}
return cmd
@@ -557,10 +605,12 @@ startWatchLoop:
// getWorkspaceAgent returns the workspace and agent selected using either the
// `<workspace>[.<agent>]` syntax via `in`.
// If autoStart is true, the workspace will be started if it is not already running.
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, userID string, in string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, input string) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
var (
workspace codersdk.Workspace
workspaceParts = strings.Split(in, ".")
workspace codersdk.Workspace
// The input will be `owner/name.agent`
// The agent is optional.
workspaceParts = strings.Split(input, ".")
err error
)
@@ -683,12 +733,12 @@ func tryPollWorkspaceAutostop(ctx context.Context, client *codersdk.Client, work
lock := flock.New(filepath.Join(os.TempDir(), "coder-autostop-notify-"+workspace.ID.String()))
conditionCtx, cancelCondition := context.WithCancel(ctx)
condition := notifyCondition(conditionCtx, client, workspace.ID, lock)
stopFunc := notify.Notify(condition, workspacePollInterval, autostopNotifyCountdown...)
notifier := notify.New(condition, workspacePollInterval, autostopNotifyCountdown)
return func() {
// With many "ssh" processes running, `lock.TryLockContext` can be hanging until the context canceled.
// Without this cancellation, a CLI process with failed remote-forward could be hanging indefinitely.
cancelCondition()
stopFunc()
notifier.Close()
}
}
@@ -1016,3 +1066,20 @@ func (r stdioErrLogReader) Read(_ []byte) (int, error) {
r.l.Error(context.Background(), "reading from stdin in stdio mode is not allowed")
return 0, io.EOF
}
func getUsageAppName(usageApp string) codersdk.UsageAppName {
if usageApp == disableUsageApp {
return ""
}
allowedUsageApps := []string{
string(codersdk.UsageAppNameSSH),
string(codersdk.UsageAppNameVscode),
string(codersdk.UsageAppNameJetbrains),
}
if slices.Contains(allowedUsageApps, usageApp) {
return codersdk.UsageAppName(usageApp)
}
return codersdk.UsageAppNameSSH
}
+154
View File
@@ -36,6 +36,7 @@ import (
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/agenttest"
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/coderdtest"
@@ -43,6 +44,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/workspacestats/workspacestatstest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
@@ -968,6 +970,49 @@ func TestSSH(t *testing.T) {
<-cmdDone
})
t.Run("Env", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Test not supported on windows")
}
t.Parallel()
client, workspace, agentToken := setupWorkspaceForAgent(t)
_ = agenttest.New(t, client.URL, agentToken)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
inv, root := clitest.New(t,
"ssh",
workspace.Name,
"--env",
"foo=bar,baz=qux",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
inv.Stderr = pty.Output()
// Wait super long so this doesn't flake on -race test.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
defer cancel()
w := clitest.StartWithWaiter(t, inv.WithContext(ctx))
defer w.Wait() // We don't care about any exit error (exit code 255: SSH connection ended unexpectedly).
// Since something was output, it should be safe to write input.
// This could show a prompt or "running startup scripts", so it's
// not indicative of the SSH connection being ready.
_ = pty.Peek(ctx, 1)
// Ensure the SSH connection is ready by testing the shell
// input/output.
pty.WriteLine("echo $foo $baz")
pty.ExpectMatchContext(ctx, "bar qux")
// And we're done.
pty.WriteLine("exit")
})
t.Run("RemoteForwardUnixSocket", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Test not supported on windows")
@@ -1249,6 +1294,115 @@ func TestSSH(t *testing.T) {
require.NoError(t, err)
require.Len(t, ents, 1, "expected one file in logdir %s", logDir)
})
t.Run("UpdateUsage", func(t *testing.T) {
t.Parallel()
type testCase struct {
name string
experiment bool
usageAppName string
expectedCalls int
expectedCountSSH int
expectedCountJetbrains int
expectedCountVscode int
}
tcs := []testCase{
{
name: "NoExperiment",
},
{
name: "Empty",
experiment: true,
expectedCalls: 1,
expectedCountSSH: 1,
},
{
name: "SSH",
experiment: true,
usageAppName: "ssh",
expectedCalls: 1,
expectedCountSSH: 1,
},
{
name: "Jetbrains",
experiment: true,
usageAppName: "jetbrains",
expectedCalls: 1,
expectedCountJetbrains: 1,
},
{
name: "Vscode",
experiment: true,
usageAppName: "vscode",
expectedCalls: 1,
expectedCountVscode: 1,
},
{
name: "InvalidDefaultsToSSH",
experiment: true,
usageAppName: "invalid",
expectedCalls: 1,
expectedCountSSH: 1,
},
{
name: "Disable",
experiment: true,
usageAppName: "disable",
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
if tc.experiment {
dv.Experiments = []string{string(codersdk.ExperimentWorkspaceUsage)}
}
batcher := &workspacestatstest.StatsBatcher{
LastStats: &agentproto.Stats{},
}
admin, store := coderdtest.NewWithDatabase(t, &coderdtest.Options{
DeploymentValues: dv,
StatsBatcher: batcher,
})
admin.SetLogger(slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug))
first := coderdtest.CreateFirstUser(t, admin)
client, user := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID)
r := dbfake.WorkspaceBuild(t, store, database.Workspace{
OrganizationID: first.OrganizationID,
OwnerID: user.ID,
}).WithAgent().Do()
workspace := r.Workspace
agentToken := r.AgentToken
inv, root := clitest.New(t, "ssh", workspace.Name, fmt.Sprintf("--usage-app=%s", tc.usageAppName))
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
cmdDone := tGo(t, func() {
err := inv.WithContext(ctx).Run()
assert.NoError(t, err)
})
pty.ExpectMatch("Waiting")
_ = agenttest.New(t, client.URL, agentToken)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
// Shells on Mac, Windows, and Linux all exit shells with the "exit" command.
pty.WriteLine("exit")
<-cmdDone
require.EqualValues(t, tc.expectedCalls, batcher.Called)
require.EqualValues(t, tc.expectedCountSSH, batcher.LastStats.SessionCountSsh)
require.EqualValues(t, tc.expectedCountJetbrains, batcher.LastStats.SessionCountJetbrains)
require.EqualValues(t, tc.expectedCountVscode, batcher.LastStats.SessionCountVscode)
})
}
})
}
//nolint:paralleltest // This test uses t.Setenv, parent test MUST NOT be parallel.
+12 -6
View File
@@ -99,7 +99,12 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse build options: %w", err)
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse rich parameters: %w", err)
}
cliRichParameterDefaults, err := asWorkspaceBuildParameters(parameterFlags.richParameterDefaults)
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse rich parameter defaults: %w", err)
}
buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
@@ -108,11 +113,12 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
NewWorkspaceName: workspace.Name,
LastBuildParameters: lastBuildParameters,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
PromptRichParameters: parameterFlags.promptRichParameters,
RichParameters: cliRichParameters,
RichParameterFile: parameterFlags.richParameterFile,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
PromptRichParameters: parameterFlags.promptRichParameters,
RichParameters: cliRichParameters,
RichParameterFile: parameterFlags.richParameterFile,
RichParameterDefaults: cliRichParameterDefaults,
})
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, err
+59 -36
View File
@@ -13,6 +13,7 @@ import (
"text/tabwriter"
"time"
"github.com/google/uuid"
"golang.org/x/xerrors"
"cdr.dev/slog"
@@ -100,7 +101,7 @@ func (r *RootCmd) supportBundle() *serpent.Command {
// Check if we're running inside a workspace
if val, found := os.LookupEnv("CODER"); found && val == "true" {
_, _ = fmt.Fprintln(inv.Stderr, "Running inside Coder workspace; this can affect results!")
cliui.Warn(inv.Stderr, "Running inside Coder workspace; this can affect results!")
cliLog.Debug(inv.Context(), "running inside coder workspace")
}
@@ -114,32 +115,41 @@ func (r *RootCmd) supportBundle() *serpent.Command {
client.URL = u
}
var (
wsID uuid.UUID
agtID uuid.UUID
)
if len(inv.Args) == 0 {
return xerrors.Errorf("must specify workspace name")
}
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return xerrors.Errorf("invalid workspace: %w", err)
}
cliLog.Debug(inv.Context(), "found workspace",
slog.F("workspace_name", ws.Name),
slog.F("workspace_id", ws.ID),
)
cliLog.Warn(inv.Context(), "no workspace specified")
cliui.Warn(inv.Stderr, "No workspace specified. This will result in incomplete information.")
} else {
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
if err != nil {
return xerrors.Errorf("invalid workspace: %w", err)
}
cliLog.Debug(inv.Context(), "found workspace",
slog.F("workspace_name", ws.Name),
slog.F("workspace_id", ws.ID),
)
wsID = ws.ID
agentName := ""
if len(inv.Args) > 1 {
agentName = inv.Args[1]
}
agentName := ""
if len(inv.Args) > 1 {
agentName = inv.Args[1]
agt, found := findAgent(agentName, ws.LatestBuild.Resources)
if !found {
cliLog.Warn(inv.Context(), "could not find agent in workspace", slog.F("agent_name", agentName))
} else {
cliLog.Debug(inv.Context(), "found workspace agent",
slog.F("agent_name", agt.Name),
slog.F("agent_id", agt.ID),
)
agtID = agt.ID
}
}
agt, found := findAgent(agentName, ws.LatestBuild.Resources)
if !found {
return xerrors.Errorf("could not find agent named %q for workspace", agentName)
}
cliLog.Debug(inv.Context(), "found workspace agent",
slog.F("agent_name", agt.Name),
slog.F("agent_id", agt.ID),
)
if outputPath == "" {
cwd, err := filepath.Abs(".")
if err != nil {
@@ -165,8 +175,8 @@ func (r *RootCmd) supportBundle() *serpent.Command {
Client: client,
// Support adds a sink so we don't need to supply one ourselves.
Log: clientLog,
WorkspaceID: ws.ID,
AgentID: agt.ID,
WorkspaceID: wsID,
AgentID: agtID,
}
bun, err := support.Run(inv.Context(), &deps)
@@ -174,6 +184,16 @@ func (r *RootCmd) supportBundle() *serpent.Command {
_ = os.Remove(outputPath) // best effort
return xerrors.Errorf("create support bundle: %w", err)
}
docsURL := bun.Deployment.Config.Values.DocsURL.String()
deployHealthSummary := bun.Deployment.HealthReport.Summarize(docsURL)
if len(deployHealthSummary) > 0 {
cliui.Warn(inv.Stdout, "Deployment health issues detected:", deployHealthSummary...)
}
clientNetcheckSummary := bun.Network.Netcheck.Summarize("Client netcheck:", docsURL)
if len(clientNetcheckSummary) > 0 {
cliui.Warn(inv.Stdout, "Networking issues detected:", deployHealthSummary...)
}
bun.CLILogs = cliLogBuf.Bytes()
if err := writeBundle(bun, zwr); err != nil {
@@ -181,6 +201,7 @@ func (r *RootCmd) supportBundle() *serpent.Command {
return xerrors.Errorf("write support bundle to %s: %w", outputPath, err)
}
_, _ = fmt.Fprintln(inv.Stderr, "Wrote support bundle to "+outputPath)
return nil
},
}
@@ -222,20 +243,22 @@ func findAgent(agentName string, haystack []codersdk.WorkspaceResource) (*coders
func writeBundle(src *support.Bundle, dest *zip.Writer) error {
// We JSON-encode the following:
for k, v := range map[string]any{
"deployment/buildinfo.json": src.Deployment.BuildInfo,
"deployment/config.json": src.Deployment.Config,
"deployment/experiments.json": src.Deployment.Experiments,
"deployment/health.json": src.Deployment.HealthReport,
"network/netcheck.json": src.Network.Netcheck,
"workspace/workspace.json": src.Workspace.Workspace,
"agent/agent.json": src.Agent.Agent,
"agent/listening_ports.json": src.Agent.ListeningPorts,
"agent/manifest.json": src.Agent.Manifest,
"agent/peer_diagnostics.json": src.Agent.PeerDiagnostics,
"agent/ping_result.json": src.Agent.PingResult,
"deployment/buildinfo.json": src.Deployment.BuildInfo,
"deployment/config.json": src.Deployment.Config,
"deployment/experiments.json": src.Deployment.Experiments,
"deployment/health.json": src.Deployment.HealthReport,
"network/connection_info.json": src.Network.ConnectionInfo,
"network/netcheck.json": src.Network.Netcheck,
"network/interfaces.json": src.Network.Interfaces,
"workspace/template.json": src.Workspace.Template,
"workspace/template_version.json": src.Workspace.TemplateVersion,
"workspace/parameters.json": src.Workspace.Parameters,
"workspace/workspace.json": src.Workspace.Workspace,
} {
f, err := dest.Create(k)
if err != nil {
@@ -255,17 +278,17 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error {
// The below we just write as we have them:
for k, v := range map[string]string{
"network/coordinator_debug.html": src.Network.CoordinatorDebug,
"network/tailnet_debug.html": src.Network.TailnetDebug,
"workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs),
"agent/logs.txt": string(src.Agent.Logs),
"agent/agent_magicsock.html": string(src.Agent.AgentMagicsockHTML),
"agent/client_magicsock.html": string(src.Agent.ClientMagicsockHTML),
"agent/startup_logs.txt": humanizeAgentLogs(src.Agent.StartupLogs),
"agent/prometheus.txt": string(src.Agent.Prometheus),
"workspace/template_file.zip": string(templateVersionBytes),
"logs.txt": strings.Join(src.Logs, "\n"),
"cli_logs.txt": string(src.CLILogs),
"logs.txt": strings.Join(src.Logs, "\n"),
"network/coordinator_debug.html": src.Network.CoordinatorDebug,
"network/tailnet_debug.html": src.Network.TailnetDebug,
"workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs),
"workspace/template_file.zip": string(templateVersionBytes),
} {
f, err := dest.Create(k)
if err != nil {
+136 -45
View File
@@ -23,6 +23,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/codersdk/healthsdk"
@@ -95,33 +96,50 @@ func TestSupportBundle(t *testing.T) {
clitest.SetupConfig(t, client, root)
err = inv.Run()
require.NoError(t, err)
assertBundleContents(t, path, secretValue)
assertBundleContents(t, path, true, true, []string{secretValue})
})
t.Run("NoWorkspace", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
var dc codersdk.DeploymentConfig
secretValue := uuid.NewString()
seedSecretDeploymentOptions(t, &dc, secretValue)
client := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: dc.Values,
})
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "support", "bundle", "--yes")
d := t.TempDir()
path := filepath.Join(d, "bundle.zip")
inv, root := clitest.New(t, "support", "bundle", "--output-file", path, "--yes")
//nolint: gocritic // requires owner privilege
clitest.SetupConfig(t, client, root)
err := inv.Run()
require.ErrorContains(t, err, "must specify workspace name")
require.NoError(t, err)
assertBundleContents(t, path, false, false, []string{secretValue})
})
t.Run("NoAgent", func(t *testing.T) {
t.Parallel()
client, db := coderdtest.NewWithDatabase(t, nil)
var dc codersdk.DeploymentConfig
secretValue := uuid.NewString()
seedSecretDeploymentOptions(t, &dc, secretValue)
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
DeploymentValues: dc.Values,
})
admin := coderdtest.CreateFirstUser(t, client)
r := dbfake.WorkspaceBuild(t, db, database.Workspace{
OrganizationID: admin.OrganizationID,
OwnerID: admin.UserID,
}).Do() // without agent!
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--yes")
d := t.TempDir()
path := filepath.Join(d, "bundle.zip")
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--output-file", path, "--yes")
//nolint: gocritic // requires owner privilege
clitest.SetupConfig(t, client, root)
err := inv.Run()
require.ErrorContains(t, err, "could not find agent")
require.NoError(t, err)
assertBundleContents(t, path, true, false, []string{secretValue})
})
t.Run("NoPrivilege", func(t *testing.T) {
@@ -140,7 +158,8 @@ func TestSupportBundle(t *testing.T) {
})
}
func assertBundleContents(t *testing.T, path string, badValues ...string) {
// nolint:revive // It's a control flag, but this is just a test.
func assertBundleContents(t *testing.T, path string, wantWorkspace bool, wantAgent bool, badValues []string) {
t.Helper()
r, err := zip.OpenReader(path)
require.NoError(t, err, "open zip file")
@@ -164,6 +183,10 @@ func assertBundleContents(t *testing.T, path string, badValues ...string) {
var v healthsdk.HealthcheckReport
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "health report should not be empty")
case "network/connection_info.json":
var v workspacesdk.AgentConnectionInfo
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "agent connection info should not be empty")
case "network/coordinator_debug.html":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "coordinator debug should not be empty")
@@ -171,66 +194,134 @@ func assertBundleContents(t *testing.T, path string, badValues ...string) {
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "tailnet debug should not be empty")
case "network/netcheck.json":
var v workspacesdk.AgentConnectionInfo
var v derphealth.Report
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "connection info should not be empty")
require.NotEmpty(t, v, "netcheck should not be empty")
case "network/interfaces.json":
var v healthsdk.InterfacesReport
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "interfaces should not be empty")
case "workspace/workspace.json":
var v codersdk.Workspace
decodeJSONFromZip(t, f, &v)
if !wantWorkspace {
require.Empty(t, v, "expected workspace to be empty")
continue
}
require.NotEmpty(t, v, "workspace should not be empty")
case "workspace/build_logs.txt":
bs := readBytesFromZip(t, f)
if !wantWorkspace || !wantAgent {
require.Empty(t, bs, "expected workspace build logs to be empty")
continue
}
require.Contains(t, string(bs), "provision done")
case "agent/agent.json":
var v codersdk.WorkspaceAgent
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "agent should not be empty")
case "agent/listening_ports.json":
var v codersdk.WorkspaceAgentListeningPortsResponse
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "agent listening ports should not be empty")
case "agent/logs.txt":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "logs should not be empty")
case "agent/agent_magicsock.html":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "agent magicsock should not be empty")
case "agent/client_magicsock.html":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "client magicsock should not be empty")
case "agent/manifest.json":
var v agentsdk.Manifest
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "agent manifest should not be empty")
case "agent/peer_diagnostics.json":
var v *tailnet.PeerDiagnostics
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "peer diagnostics should not be empty")
case "agent/ping_result.json":
var v *ipnstate.PingResult
decodeJSONFromZip(t, f, &v)
require.NotEmpty(t, v, "ping result should not be empty")
case "agent/prometheus.txt":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "agent prometheus metrics should not be empty")
case "agent/startup_logs.txt":
bs := readBytesFromZip(t, f)
require.Contains(t, string(bs), "started up")
case "workspace/template.json":
var v codersdk.Template
decodeJSONFromZip(t, f, &v)
if !wantWorkspace {
require.Empty(t, v, "expected workspace template to be empty")
continue
}
require.NotEmpty(t, v, "workspace template should not be empty")
case "workspace/template_version.json":
var v codersdk.TemplateVersion
decodeJSONFromZip(t, f, &v)
if !wantWorkspace {
require.Empty(t, v, "expected workspace template version to be empty")
continue
}
require.NotEmpty(t, v, "workspace template version should not be empty")
case "workspace/parameters.json":
var v []codersdk.WorkspaceBuildParameter
decodeJSONFromZip(t, f, &v)
if !wantWorkspace {
require.Empty(t, v, "expected workspace parameters to be empty")
continue
}
require.NotNil(t, v, "workspace parameters should not be nil")
case "workspace/template_file.zip":
bs := readBytesFromZip(t, f)
if !wantWorkspace {
require.Empty(t, bs, "expected template file to be empty")
continue
}
require.NotNil(t, bs, "template file should not be nil")
case "agent/agent.json":
var v codersdk.WorkspaceAgent
decodeJSONFromZip(t, f, &v)
if !wantAgent {
require.Empty(t, v, "expected agent to be empty")
continue
}
require.NotEmpty(t, v, "agent should not be empty")
case "agent/listening_ports.json":
var v codersdk.WorkspaceAgentListeningPortsResponse
decodeJSONFromZip(t, f, &v)
if !wantAgent {
require.Empty(t, v, "expected agent listening ports to be empty")
continue
}
require.NotEmpty(t, v, "agent listening ports should not be empty")
case "agent/logs.txt":
bs := readBytesFromZip(t, f)
if !wantAgent {
require.Empty(t, bs, "expected agent logs to be empty")
continue
}
require.NotEmpty(t, bs, "logs should not be empty")
case "agent/agent_magicsock.html":
bs := readBytesFromZip(t, f)
if !wantAgent {
require.Empty(t, bs, "expected agent magicsock to be empty")
continue
}
require.NotEmpty(t, bs, "agent magicsock should not be empty")
case "agent/client_magicsock.html":
bs := readBytesFromZip(t, f)
if !wantAgent {
require.Empty(t, bs, "expected client magicsock to be empty")
continue
}
require.NotEmpty(t, bs, "client magicsock should not be empty")
case "agent/manifest.json":
var v agentsdk.Manifest
decodeJSONFromZip(t, f, &v)
if !wantAgent {
require.Empty(t, v, "expected agent manifest to be empty")
continue
}
require.NotEmpty(t, v, "agent manifest should not be empty")
case "agent/peer_diagnostics.json":
var v *tailnet.PeerDiagnostics
decodeJSONFromZip(t, f, &v)
if !wantAgent {
require.Empty(t, v, "expected peer diagnostics to be empty")
continue
}
require.NotEmpty(t, v, "peer diagnostics should not be empty")
case "agent/ping_result.json":
var v *ipnstate.PingResult
decodeJSONFromZip(t, f, &v)
if !wantAgent {
require.Empty(t, v, "expected ping result to be empty")
continue
}
require.NotEmpty(t, v, "ping result should not be empty")
case "agent/prometheus.txt":
bs := readBytesFromZip(t, f)
if !wantAgent {
require.Empty(t, bs, "expected agent prometheus metrics to be empty")
continue
}
require.NotEmpty(t, bs, "agent prometheus metrics should not be empty")
case "agent/startup_logs.txt":
bs := readBytesFromZip(t, f)
if !wantAgent {
require.Empty(t, bs, "expected agent startup logs to be empty")
continue
}
require.Contains(t, string(bs), "started up")
case "logs.txt":
bs := readBytesFromZip(t, f)
require.NotEmpty(t, bs, "logs should not be empty")
+12 -3
View File
@@ -100,6 +100,16 @@ func (r *RootCmd) templatePush() *serpent.Command {
return err
}
// If user hasn't provided new provisioner tags, inherit ones from the active template version.
if len(tags) == 0 && template.ActiveVersionID != uuid.Nil {
templateVersion, err := client.TemplateVersion(inv.Context(), template.ActiveVersionID)
if err != nil {
return err
}
tags = templateVersion.Job.Tags
inv.Logger.Info(inv.Context(), "reusing existing provisioner tags", "tags", tags)
}
userVariableValues, err := ParseUserVariableValues(
varsFiles,
variablesFile,
@@ -407,9 +417,8 @@ func createValidTemplateVersion(inv *serpent.Invocation, args createValidTemplat
if errors.As(err, &jobErr) && !codersdk.JobIsMissingParameterErrorCode(jobErr.Code) {
return nil, err
}
if err != nil {
return nil, err
}
return nil, err
}
version, err = client.TemplateVersion(inv.Context(), version.ID)
if err != nil {
+129
View File
@@ -403,6 +403,135 @@ func TestTemplatePush(t *testing.T) {
assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID)
})
t.Run("ProvisionerTags", func(t *testing.T) {
t.Parallel()
t.Run("ChangeTags", func(t *testing.T) {
t.Parallel()
// Start the first provisioner
client, provisionerDocker, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
ProvisionerDaemonTags: map[string]string{
"docker": "true",
},
})
defer provisionerDocker.Close()
// Start the second provisioner
provisionerFoobar := coderdtest.NewTaggedProvisionerDaemon(t, api, "provisioner-foobar", map[string]string{
"foobar": "foobaz",
})
defer provisionerFoobar.Close()
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
// Create the template with initial tagged template version.
templateVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.ProvisionerTags = map[string]string{
"docker": "true",
}
})
templateVersion = coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateVersion.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, templateVersion.ID)
// Push new template version without provisioner tags. CLI should reuse tags from the previous version.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", template.Name,
"--provisioner-tag", "foobar=foobaz")
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
execDone := make(chan error)
go func() {
execDone <- inv.Run()
}()
matches := []struct {
match string
write string
}{
{match: "Upload", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
require.NoError(t, <-execDone)
// Verify template version tags
template, err := client.Template(context.Background(), template.ID)
require.NoError(t, err)
templateVersion, err = client.TemplateVersion(context.Background(), template.ActiveVersionID)
require.NoError(t, err)
require.EqualValues(t, map[string]string{"foobar": "foobaz", "owner": "", "scope": "organization"}, templateVersion.Job.Tags)
})
t.Run("DoNotChangeTags", func(t *testing.T) {
t.Parallel()
// Start the tagged provisioner
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
ProvisionerDaemonTags: map[string]string{
"docker": "true",
},
})
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
// Create the template with initial tagged template version.
templateVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.ProvisionerTags = map[string]string{
"docker": "true",
}
})
templateVersion = coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateVersion.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, templateVersion.ID)
// Push new template version without provisioner tags. CLI should reuse tags from the previous version.
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", template.Name)
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
execDone := make(chan error)
go func() {
execDone <- inv.Run()
}()
matches := []struct {
match string
write string
}{
{match: "Upload", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
require.NoError(t, <-execDone)
// Verify template version tags
template, err := client.Template(context.Background(), template.ID)
require.NoError(t, err)
templateVersion, err = client.TemplateVersion(context.Background(), template.ActiveVersionID)
require.NoError(t, err)
require.EqualValues(t, map[string]string{"docker": "true", "owner": "", "scope": "organization"}, templateVersion.Job.Tags)
})
})
t.Run("Variables", func(t *testing.T) {
t.Parallel()
+3 -3
View File
@@ -16,12 +16,12 @@ func (r *RootCmd) templates() *serpent.Command {
cmd := &serpent.Command{
Use: "templates",
Short: "Manage templates",
Long: "Templates are written in standard Terraform and describe the infrastructure for workspaces\n" + formatExamples(
example{
Long: "Templates are written in standard Terraform and describe the infrastructure for workspaces\n" + FormatExamples(
Example{
Description: "Make changes to your template, and plan the changes",
Command: "coder templates plan my-template",
},
example{
Example{
Description: "Create or push an update to the template. Your developers can update their workspaces",
Command: "coder templates push my-template",
},
+1 -1
View File
@@ -166,7 +166,7 @@ func (r *RootCmd) archiveTemplateVersions() *serpent.Command {
inv.Stdout, fmt.Sprintf("Archived %d versions from "+pretty.Sprint(cliui.DefaultStyles.Keyword, template.Name)+" at "+cliui.Timestamp(time.Now()), len(resp.ArchivedIDs)),
)
if ok, _ := inv.ParsedFlags().GetBool("verbose"); err == nil && ok {
if ok, _ := inv.ParsedFlags().GetBool("verbose"); ok {
data, err := json.Marshal(resp)
if err != nil {
return xerrors.Errorf("marshal verbose response: %w", err)
+2 -2
View File
@@ -19,8 +19,8 @@ func (r *RootCmd) templateVersions() *serpent.Command {
Use: "versions",
Short: "Manage different versions of the specified template",
Aliases: []string{"version"},
Long: formatExamples(
example{
Long: FormatExamples(
Example{
Description: "List versions of a specific template",
Command: "coder templates versions list my-template",
},
+3
View File
@@ -18,6 +18,9 @@ OPTIONS:
--auth string, $CODER_AGENT_AUTH (default: token)
Specify the authentication type to use for the agent.
--block-file-transfer bool, $CODER_AGENT_BLOCK_FILE_TRANSFER (default: false)
Block file transfer using known applications: nc,rsync,scp,sftp.
--debug-address string, $CODER_AGENT_DEBUG_ADDRESS (default: 127.0.0.1:2113)
The bind address to serve a debug HTTP server.
+3
View File
@@ -20,6 +20,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.
+3
View File
@@ -10,6 +10,9 @@ OPTIONS:
Specifies an email address to use if creating the first user for the
deployment.
--first-user-full-name string, $CODER_FIRST_USER_FULL_NAME
Specifies a human-readable name for the first user of the deployment.
--first-user-password string, $CODER_FIRST_USER_PASSWORD
Specifies a password to use if creating the first user for the
deployment.
+3
View File
@@ -19,6 +19,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.
+7
View File
@@ -60,6 +60,10 @@ OPTIONS:
--support-links struct[[]codersdk.LinkConfig], $CODER_SUPPORT_LINKS
Support links to display in the top right drop down menu.
--terms-of-service-url string, $CODER_TERMS_OF_SERVICE_URL
A URL to an external Terms of Service that must be accepted by users
when logging in.
--update-check bool, $CODER_UPDATE_CHECK (default: false)
Periodically check for new releases of Coder and inform the owner. The
check is performed once per day.
@@ -403,6 +407,9 @@ OIDC OPTIONS:
--oidc-issuer-url string, $CODER_OIDC_ISSUER_URL
Issuer URL to use for Login with OIDC.
--oidc-name-field string, $CODER_OIDC_NAME_FIELD (default: name)
OIDC claim field to use as the name.
--oidc-group-regex-filter regexp, $CODER_OIDC_GROUP_REGEX_FILTER (default: .*)
If provided any group name not matching the regex is ignored. This
allows for filtering out groups that are not needed. This filter is
+7
View File
@@ -6,6 +6,10 @@ USAGE:
Run upload and download tests from your machine to a workspace
OPTIONS:
-c, --column string-array (default: Interval,Throughput)
Columns to display in table output. Available columns: Interval,
Throughput.
-d, --direct bool
Specifies whether to wait for a direct connection before testing
speed.
@@ -14,6 +18,9 @@ OPTIONS:
Specifies whether to run in reverse mode where the client receives and
the server sends.
-o, --output string (default: table)
Output format. Available formats: table, json.
--pcap-file string
Specifies a file to write a network capture to.
+3
View File
@@ -9,6 +9,9 @@ OPTIONS:
--disable-autostart bool, $CODER_SSH_DISABLE_AUTOSTART (default: false)
Disable starting the workspace automatically when connecting via SSH.
-e, --env string-array, $CODER_SSH_ENV
Set environment variable(s) for session (key1=value1,key2=value2,...).
-A, --forward-agent bool, $CODER_SSH_FORWARD_AGENT
Specifies whether to forward the SSH agent specified in
$SSH_AUTH_SOCK.
+3
View File
@@ -19,6 +19,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.
+3
View File
@@ -21,6 +21,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.
+3
View File
@@ -7,6 +7,9 @@ OPTIONS:
-e, --email string
Specifies an email address for the new user.
-n, --full-name string
Specifies an optional human-readable name for the new user.
--login-type string
Optionally specify the login type for the user. Valid values are:
password, none, github, oidc. Using 'none' prevents the user from
+1 -1
View File
@@ -3,7 +3,7 @@
"id": "[first user ID]",
"username": "testuser",
"avatar_url": "",
"name": "",
"name": "Test User",
"email": "testuser@coder.com",
"created_at": "[timestamp]",
"last_seen_at": "[timestamp]",
+12 -4
View File
@@ -306,6 +306,9 @@ oidc:
# OIDC claim field to use as the username.
# (default: preferred_username, type: string)
usernameField: preferred_username
# OIDC claim field to use as the name.
# (default: name, type: string)
nameField: name
# OIDC claim field to use as the email.
# (default: email, type: string)
emailField: email
@@ -379,10 +382,11 @@ provisioning:
# state for a long time, consider increasing this.
# (default: 3, type: int)
daemons: 3
# Whether to use echo provisioner daemons instead of Terraform. This is for E2E
# tests.
# (default: false, type: bool)
daemonsEcho: false
# The supported job types for the built-in provisioners. By default, this is only
# the terraform type. Supported types: terraform,echo.
# (default: terraform, type: string-array)
daemonTypes:
- terraform
# Deprecated and ignored.
# (default: 1s, type: duration)
daemonPollInterval: 1s
@@ -414,6 +418,10 @@ inMemoryDatabase: false
# Type of auth to use when connecting to postgres.
# (default: password, type: enum[password\|awsiamrds])
pgAuth: password
# A URL to an external Terms of Service that must be accepted by users when
# logging in.
# (default: <unset>, type: string)
termsOfServiceURL: ""
# The algorithm to use for generating ssh keys. Accepted values are "ed25519",
# "ecdsa", or "rsa4096".
# (default: ed25519, type: string)
+4 -4
View File
@@ -17,16 +17,16 @@ func (r *RootCmd) tokens() *serpent.Command {
cmd := &serpent.Command{
Use: "tokens",
Short: "Manage personal access tokens",
Long: "Tokens are used to authenticate automated clients to Coder.\n" + formatExamples(
example{
Long: "Tokens are used to authenticate automated clients to Coder.\n" + FormatExamples(
Example{
Description: "Create a token for automation",
Command: "coder tokens create",
},
example{
Example{
Description: "List your tokens",
Command: "coder tokens ls",
},
example{
Example{
Description: "Remove a token by ID",
Command: "coder tokens rm WuoWs4ZsMX",
},
+24
View File
@@ -10,6 +10,7 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/serpent"
@@ -19,6 +20,7 @@ func (r *RootCmd) userCreate() *serpent.Command {
var (
email string
username string
name string
password string
disableLogin bool
loginType string
@@ -35,6 +37,9 @@ func (r *RootCmd) userCreate() *serpent.Command {
if err != nil {
return err
}
// We only prompt for the full name if both username and email have not
// been set. This is to avoid breaking existing non-interactive usage.
shouldPromptName := username == "" && email == ""
if username == "" {
username, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "Username:",
@@ -58,6 +63,18 @@ func (r *RootCmd) userCreate() *serpent.Command {
return err
}
}
if name == "" && shouldPromptName {
rawName, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Full name (optional):",
})
if err != nil {
return err
}
name = httpapi.NormalizeRealUsername(rawName)
if !strings.EqualFold(rawName, name) {
cliui.Warnf(inv.Stderr, "Normalized name to %q", name)
}
}
userLoginType := codersdk.LoginTypePassword
if disableLogin && loginType != "" {
return xerrors.New("You cannot specify both --disable-login and --login-type")
@@ -79,6 +96,7 @@ func (r *RootCmd) userCreate() *serpent.Command {
_, err = client.CreateUser(inv.Context(), codersdk.CreateUserRequest{
Email: email,
Username: username,
Name: name,
Password: password,
OrganizationID: organization.ID,
UserLoginType: userLoginType,
@@ -127,6 +145,12 @@ Create a workspace `+pretty.Sprint(cliui.DefaultStyles.Code, "coder create")+`!
Description: "Specifies a username for the new user.",
Value: serpent.StringOf(&username),
},
{
Flag: "full-name",
FlagShorthand: "n",
Description: "Specifies an optional human-readable name for the new user.",
Value: serpent.StringOf(&name),
},
{
Flag: "password",
FlagShorthand: "p",
+88 -1
View File
@@ -4,16 +4,19 @@ import (
"testing"
"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/pty/ptytest"
"github.com/coder/coder/v2/testutil"
)
func TestUserCreate(t *testing.T) {
t.Parallel()
t.Run("Prompts", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "users", "create")
@@ -28,6 +31,7 @@ func TestUserCreate(t *testing.T) {
matches := []string{
"Username", "dean",
"Email", "dean@coder.com",
"Full name (optional):", "Mr. Dean Deanington",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
@@ -35,6 +39,89 @@ func TestUserCreate(t *testing.T) {
pty.ExpectMatch(match)
pty.WriteLine(value)
}
<-doneChan
_ = testutil.RequireRecvCtx(ctx, t, doneChan)
created, err := client.User(ctx, matches[1])
require.NoError(t, err)
assert.Equal(t, matches[1], created.Username)
assert.Equal(t, matches[3], created.Email)
assert.Equal(t, matches[5], created.Name)
})
t.Run("PromptsNoName", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "users", "create")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
matches := []string{
"Username", "noname",
"Email", "noname@coder.com",
"Full name (optional):", "",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
_ = testutil.RequireRecvCtx(ctx, t, doneChan)
created, err := client.User(ctx, matches[1])
require.NoError(t, err)
assert.Equal(t, matches[1], created.Username)
assert.Equal(t, matches[3], created.Email)
assert.Empty(t, created.Name)
})
t.Run("Args", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
args := []string{
"users", "create",
"-e", "dean@coder.com",
"-u", "dean",
"-n", "Mr. Dean Deanington",
"-p", "1n5ecureP4ssw0rd!",
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
err := inv.Run()
require.NoError(t, err)
ctx := testutil.Context(t, testutil.WaitShort)
created, err := client.User(ctx, "dean")
require.NoError(t, err)
assert.Equal(t, args[3], created.Email)
assert.Equal(t, args[5], created.Username)
assert.Equal(t, args[7], created.Name)
})
t.Run("ArgsNoName", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
args := []string{
"users", "create",
"-e", "dean@coder.com",
"-u", "dean",
"-p", "1n5ecureP4ssw0rd!",
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
err := inv.Run()
require.NoError(t, err)
ctx := testutil.Context(t, testutil.WaitShort)
created, err := client.User(ctx, args[5])
require.NoError(t, err)
assert.Equal(t, args[3], created.Email)
assert.Equal(t, args[5], created.Username)
assert.Empty(t, created.Name)
})
}
+3 -2
View File
@@ -57,8 +57,8 @@ func (r *RootCmd) userSingle() *serpent.Command {
cmd := &serpent.Command{
Use: "show <username|user_id|'me'>",
Short: "Show a single user. Use 'me' to indicate the currently authenticated user.",
Long: formatExamples(
example{
Long: FormatExamples(
Example{
Command: "coder users show me",
},
),
@@ -137,6 +137,7 @@ func (*userShowFormat) Format(_ context.Context, out interface{}) (string, error
// Add rows for each of the user's fields.
addRow("ID", user.ID.String())
addRow("Username", user.Username)
addRow("Full name", user.Name)
addRow("Email", user.Email)
addRow("Status", user.Status)
addRow("Created At", user.CreatedAt.Format(time.Stamp))
+9 -1
View File
@@ -57,7 +57,14 @@ func TestUserList(t *testing.T) {
err := json.Unmarshal(buf.Bytes(), &users)
require.NoError(t, err, "unmarshal JSON output")
require.Len(t, users, 2)
require.Contains(t, users[0].Email, "coder.com")
for _, u := range users {
assert.NotEmpty(t, u.ID)
assert.NotEmpty(t, u.Email)
assert.NotEmpty(t, u.Username)
assert.NotEmpty(t, u.Name)
assert.NotEmpty(t, u.CreatedAt)
assert.NotEmpty(t, u.Status)
}
})
t.Run("NoURLFileErrorHasHelperText", func(t *testing.T) {
t.Parallel()
@@ -133,5 +140,6 @@ func TestUserShow(t *testing.T) {
require.Equal(t, otherUser.ID, newUser.ID)
require.Equal(t, otherUser.Username, newUser.Username)
require.Equal(t, otherUser.Email, newUser.Email)
require.Equal(t, otherUser.Name, newUser.Name)
})
}

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